From 08b86784ef3a14619b970f47021c62d0b607e562 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 30 Sep 2019 20:49:15 +0200 Subject: [PATCH 01/53] Expose serverBasePath fixes #45991 (#45995) * Expose serverBasePath fixes #45991 * Review comments * Fix basepath mock types * AppBasePathContract -> IBasePath * Match basepath test description with assertion * Fix eslint errors --- .../kibana-plugin-server.basepath.get.md | 13 +++++ .../server/kibana-plugin-server.basepath.md | 28 +++++++++++ .../kibana-plugin-server.basepath.prepend.md | 13 +++++ .../kibana-plugin-server.basepath.remove.md | 13 +++++ ...a-plugin-server.basepath.serverbasepath.md | 15 ++++++ .../kibana-plugin-server.basepath.set.md | 13 +++++ ...-plugin-server.httpserversetup.basepath.md | 9 ++-- .../kibana-plugin-server.httpserversetup.md | 2 +- .../server/kibana-plugin-server.ibasepath.md | 15 ++++++ .../core/server/kibana-plugin-server.md | 2 + .../server/http/base_path_service.test.ts | 12 +++++ src/core/server/http/base_path_service.ts | 47 ++++++++++++++++--- src/core/server/http/http_server.ts | 24 ++-------- src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/index.ts | 1 + src/core/server/index.ts | 2 + src/core/server/server.api.md | 21 ++++++--- 17 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.basepath.get.md create mode 100644 docs/development/core/server/kibana-plugin-server.basepath.md create mode 100644 docs/development/core/server/kibana-plugin-server.basepath.prepend.md create mode 100644 docs/development/core/server/kibana-plugin-server.basepath.remove.md create mode 100644 docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md create mode 100644 docs/development/core/server/kibana-plugin-server.basepath.set.md create mode 100644 docs/development/core/server/kibana-plugin-server.ibasepath.md diff --git a/docs/development/core/server/kibana-plugin-server.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md new file mode 100644 index 00000000000000..2b3b6c899e8ded --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [get](./kibana-plugin-server.basepath.get.md) + +## BasePath.get property + +returns `basePath` value, specific for an incoming request. + +Signature: + +```typescript +get: (request: KibanaRequest | LegacyRequest) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.md b/docs/development/core/server/kibana-plugin-server.basepath.md new file mode 100644 index 00000000000000..45fb697b329f89 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) + +## BasePath class + +Access or manipulate the Kibana base path + +Signature: + +```typescript +export declare class BasePath +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [get](./kibana-plugin-server.basepath.get.md) | | (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest) => string | returns basePath value, specific for an incoming request. | +| [prepend](./kibana-plugin-server.basepath.prepend.md) | | (path: string) => string | returns a new basePath value, prefixed with passed url. | +| [remove](./kibana-plugin-server.basepath.remove.md) | | (path: string) => string | returns a new basePath value, cleaned up from passed url. | +| [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request | +| [set](./kibana-plugin-server.basepath.set.md) | | (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `BasePath` class. + diff --git a/docs/development/core/server/kibana-plugin-server.basepath.prepend.md b/docs/development/core/server/kibana-plugin-server.basepath.prepend.md new file mode 100644 index 00000000000000..113e8d9bf48803 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.prepend.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [prepend](./kibana-plugin-server.basepath.prepend.md) + +## BasePath.prepend property + +returns a new `basePath` value, prefixed with passed `url`. + +Signature: + +```typescript +prepend: (path: string) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.remove.md b/docs/development/core/server/kibana-plugin-server.basepath.remove.md new file mode 100644 index 00000000000000..c5f1092d2969d9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.remove.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [remove](./kibana-plugin-server.basepath.remove.md) + +## BasePath.remove property + +returns a new `basePath` value, cleaned up from passed `url`. + +Signature: + +```typescript +remove: (path: string) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md b/docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md new file mode 100644 index 00000000000000..d7e45a92dba6d7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) + +## BasePath.serverBasePath property + +returns the server's basePath + +See [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request + +Signature: + +```typescript +readonly serverBasePath: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md new file mode 100644 index 00000000000000..1272a134ef5c44 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [set](./kibana-plugin-server.basepath.set.md) + +## BasePath.set property + +sets `basePath` value, specific for an incoming request. + +Signature: + +```typescript +set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md index 5cfb2f5c4e8b43..173262de10494f 100644 --- a/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md @@ -4,13 +4,10 @@ ## HttpServerSetup.basePath property +[BasePath](./kibana-plugin-server.basepath.md) + Signature: ```typescript -basePath: { - get: (request: KibanaRequest | LegacyRequest) => string; - set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; - prepend: (url: string) => string; - remove: (url: string) => string; - }; +basePath: IBasePath; ``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.md index f495de850aff5e..7a126383116e7b 100644 --- a/docs/development/core/server/kibana-plugin-server.httpserversetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.md @@ -17,7 +17,7 @@ export interface HttpServerSetup | Property | Type | Description | | --- | --- | --- | | [auth](./kibana-plugin-server.httpserversetup.auth.md) | {
get: GetAuthState;
isAuthenticated: IsAuthenticated;
getAuthHeaders: GetAuthHeaders;
} | | -| [basePath](./kibana-plugin-server.httpserversetup.basepath.md) | {
get: (request: KibanaRequest | LegacyRequest) => string;
set: (request: KibanaRequest | LegacyRequest, basePath: string) => void;
prepend: (url: string) => string;
remove: (url: string) => string;
} | | +| [basePath](./kibana-plugin-server.httpserversetup.basepath.md) | IBasePath | [BasePath](./kibana-plugin-server.basepath.md) | | [createCookieSessionStorageFactory](./kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | | [isTlsEnabled](./kibana-plugin-server.httpserversetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. | diff --git a/docs/development/core/server/kibana-plugin-server.ibasepath.md b/docs/development/core/server/kibana-plugin-server.ibasepath.md new file mode 100644 index 00000000000000..2baa8d623ce97b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ibasepath.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IBasePath](./kibana-plugin-server.ibasepath.md) + +## IBasePath type + +Access or manipulate the Kibana base path + +[BasePath](./kibana-plugin-server.basepath.md) + +Signature: + +```typescript +export declare type IBasePath = Pick; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index baecb180096de9..fec2fc4b64019f 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -16,6 +16,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Class | Description | | --- | --- | +| [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path | | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | @@ -122,6 +123,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Headers](./kibana-plugin-server.headers.md) | Http request headers to read. | | [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) | Data send to the client as a response payload. | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | +| [IBasePath](./kibana-plugin-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-server.basepath.md) | | [IContextHandler](./kibana-plugin-server.icontexthandler.md) | A function registered by a plugin to perform some action. | | [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | diff --git a/src/core/server/http/base_path_service.test.ts b/src/core/server/http/base_path_service.test.ts index ffbbe158cb2d4d..01790b7c77e064 100644 --- a/src/core/server/http/base_path_service.test.ts +++ b/src/core/server/http/base_path_service.test.ts @@ -22,6 +22,18 @@ import { KibanaRequest } from './router'; import { httpServerMock } from './http_server.mocks'; describe('BasePath', () => { + describe('serverBasePath', () => { + it('defaults to an empty string', () => { + const basePath = new BasePath(); + expect(basePath.serverBasePath).toBe(''); + }); + + it('returns the server base path', () => { + const basePath = new BasePath('/server'); + expect(basePath.serverBasePath).toBe('/server'); + }); + }); + describe('#get()', () => { it('returns base path associated with an incoming Legacy.Request request', () => { const request = httpServerMock.createRawRequest(); diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index 951463a2c9919f..916419cac212a1 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -20,18 +20,39 @@ import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; import { modifyUrl } from '../../utils'; +/** + * Access or manipulate the Kibana base path + * + * @public + */ export class BasePath { private readonly basePathCache = new WeakMap(); - constructor(private readonly serverBasePath?: string) {} + /** + * returns the server's basePath + * + * See {@link BasePath.get} for getting the basePath value for a specific request + */ + public readonly serverBasePath: string; + + /** @internal */ + constructor(serverBasePath: string = '') { + this.serverBasePath = serverBasePath; + } + /** + * returns `basePath` value, specific for an incoming request. + */ public get = (request: KibanaRequest | LegacyRequest) => { const requestScopePath = this.basePathCache.get(ensureRawRequest(request)) || ''; - const serverBasePath = this.serverBasePath || ''; - return `${serverBasePath}${requestScopePath}`; + return `${this.serverBasePath}${requestScopePath}`; }; - // should work only for KibanaRequest as soon as spaces migrate to NP + /** + * sets `basePath` value, specific for an incoming request. + * + * @privateRemarks should work only for KibanaRequest as soon as spaces migrate to NP + */ public set = (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => { const rawRequest = ensureRawRequest(request); @@ -43,8 +64,11 @@ export class BasePath { this.basePathCache.set(rawRequest, requestSpecificBasePath); }; + /** + * returns a new `basePath` value, prefixed with passed `url`. + */ public prepend = (path: string): string => { - if (!this.serverBasePath) return path; + if (this.serverBasePath === '') return path; return modifyUrl(path, parts => { if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { parts.pathname = `${this.serverBasePath}${parts.pathname}`; @@ -52,8 +76,11 @@ export class BasePath { }); }; + /** + * returns a new `basePath` value, cleaned up from passed `url`. + */ public remove = (path: string): string => { - if (!this.serverBasePath) { + if (this.serverBasePath === '') { return path; } @@ -68,3 +95,11 @@ export class BasePath { return path; }; } + +/** + * Access or manipulate the Kibana base path + * + * {@link BasePath} + * @public + */ +export type IBasePath = Pick; diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index cb6906379c4ef3..b56fef5f65c2a9 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -26,7 +26,7 @@ import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; -import { KibanaRequest, LegacyRequest, ResponseHeaders, IRouter } from './router'; +import { ResponseHeaders, IRouter } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -34,7 +34,7 @@ import { import { SessionStorageFactory } from './session_storage'; import { AuthStateStorage, GetAuthState, IsAuthenticated } from './auth_state_storage'; import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; -import { BasePath } from './base_path_service'; +import { BasePath, IBasePath } from './base_path_service'; /** * Kibana HTTP Service provides own abstraction for work with HTTP stack. @@ -148,24 +148,8 @@ export interface HttpServerSetup { * @param handler {@link OnPostAuthHandler} - function to call. */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; - basePath: { - /** - * returns `basePath` value, specific for an incoming request. - */ - get: (request: KibanaRequest | LegacyRequest) => string; - /** - * sets `basePath` value, specific for an incoming request. - */ - set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; - /** - * returns a new `basePath` value, prefixed with passed `url`. - */ - prepend: (url: string) => string; - /** - * returns a new `basePath` value, cleaned up from passed `url`. - */ - remove: (url: string) => string; - }; + /** {@link BasePath} */ + basePath: IBasePath; auth: { get: GetAuthState; isAuthenticated: IsAuthenticated; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index c5f920dcb360e1..c0658ae8d1e5c4 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -30,6 +30,7 @@ type ServiceSetupMockType = jest.Mocked & { }; const createBasePathMock = (): jest.Mocked => ({ + serverBasePath: '/mock-server-basepath', get: jest.fn(), set: jest.fn(), prepend: jest.fn(), diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 895396b91eb465..4f83cd996deba8 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -58,3 +58,4 @@ export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; export { SessionStorageCookieOptions } from './cookie_session_storage'; export * from './types'; +export { BasePath, IBasePath } from './base_path_service'; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef31804be62b2b..83328d560ee22f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -77,6 +77,8 @@ export { AuthResultParams, AuthStatus, AuthToolkit, + BasePath, + IBasePath, CustomHttpResponseOptions, GetAuthHeaders, GetAuthState, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 94c7f6ec9b3255..0dc1ed50564837 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -54,6 +54,17 @@ export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; } +// @public +export class BasePath { + // @internal + constructor(serverBasePath?: string); + get: (request: KibanaRequest | LegacyRequest) => string; + prepend: (path: string) => string; + remove: (path: string) => string; + readonly serverBasePath: string; + set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +} + // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts // // @internal (undocumented) @@ -230,12 +241,7 @@ export interface HttpServerSetup { getAuthHeaders: GetAuthHeaders; }; // (undocumented) - basePath: { - get: (request: KibanaRequest | LegacyRequest) => string; - set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; - prepend: (url: string) => string; - remove: (url: string) => string; - }; + basePath: IBasePath; createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; isTlsEnabled: boolean; registerAuth: (handler: AuthenticationHandler) => void; @@ -257,6 +263,9 @@ export interface HttpServiceStart { isListening: (port: number) => boolean; } +// @public +export type IBasePath = Pick; + // @public export interface IContextContainer { createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise; From a972486faaf6447c18e4c9d23f4914bb5d182543 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 30 Sep 2019 15:10:52 -0400 Subject: [PATCH 02/53] Added Readme for EuiUtils (#46736) --- src/plugins/eui_utils/README.md | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/plugins/eui_utils/README.md diff --git a/src/plugins/eui_utils/README.md b/src/plugins/eui_utils/README.md new file mode 100644 index 00000000000000..019cca9c045153 --- /dev/null +++ b/src/plugins/eui_utils/README.md @@ -0,0 +1,67 @@ +# EuiUtils + +The EuiUtils plugin is a way to create easier integration of EUI colors, themes, and other utilities with Kibana. They usually take into account the current theme (light or dark) of Kibana and return the correct object that was asked for. + +## EUI plus Elastic-Charts + +EUI provides a light and dark theme object to work with Elastic-Charts. However, every instance of a Chart would need to pass down this the correctly EUI theme depending on Kibana's light or dark mode. There are several ways you can use EuiUtils to grab the correct theme. + +### `useChartsTheme` + +The simple fetching of the correct EUI theme; a **React hook**. + +```js +import { npStart } from 'ui/new_platform'; +import { Chart, Settings } from '@elastic/charts'; + +export const YourComponent = () => ( + + + +); +``` + +### `getChartsTheme$` + +An **observable** of the current charts theme. Use this implementation for more flexible updates to the chart theme without full page refreshes. + +```ts +import { npStart } from 'ui/new_platform'; +import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes'; +import { Subscription } from 'rxjs'; +import { Chart, Settings } from '@elastic/charts'; + +interface YourComponentState { + chartsTheme: EuiChartThemeType['theme']; +} + +export class YourComponent extends Component { + private subscription?: Subscription; + public state = { + chartsTheme: npStart.plugins.eui_utils.getChartsThemeDefault(), + }; + + componentDidMount() { + this.subscription = npStart.plugins.eui_utils + .getChartsTheme$() + .subscribe(chartsTheme => this.setState({ chartsTheme })); + } + + componentWillUnmount() { + if (this.subscription) { + this.subscription.unsubscribe(); + this.subscription = undefined; + } + } + + public render() { + const { chartsTheme } = this.state; + + return ( + + + + ); + } +} +``` From 5af031a604061273783816bb2304eb0831d3b974 Mon Sep 17 00:00:00 2001 From: Mengwei Ding Date: Mon, 30 Sep 2019 12:25:43 -0700 Subject: [PATCH 03/53] [Code] More frontend migrations for New Platform (#46640) * remove dependency on ui/utils/query_string * remove dependency on ui/resize_checker * remove dependency on ui/capabilities * remove ui/autoload/all and ui/autoload/styles * remove dependency on ui/modules * amend the capabilities * remove dependency of chrome.breadcrumbs push/pop * remove type error * Add back 'ui/autoload/all' and 'ui/autoload/styles' --- .../components/admin_page/empty_project.tsx | 5 +- .../components/admin_page/project_tab.tsx | 8 +- .../public/components/codeblock/codeblock.tsx | 3 +- .../code/public/components/main/side_tabs.tsx | 6 +- .../public/components/search_page/search.tsx | 12 +- .../components/shared/resize_checker.tsx | 111 ++++++++++++++++ .../code/public/monaco/monaco_diff_editor.ts | 3 +- .../code/public/monaco/monaco_helper.ts | 3 +- .../plugins/code/public/utils/query_string.ts | 119 ++++++++++++++++++ 9 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 x-pack/legacy/plugins/code/public/components/shared/resize_checker.tsx create mode 100644 x-pack/legacy/plugins/code/public/utils/query_string.ts diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx index b574dc94805c71..f071fd06f84c92 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/empty_project.tsx @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { get } from 'lodash'; import React from 'react'; import { Link } from 'react-router-dom'; import { EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { capabilities } from 'ui/capabilities'; +import { npStart } from 'ui/new_platform'; import { ImportProject } from './import_project'; export const EmptyProject = () => { - const isAdmin = capabilities.get().code.admin as boolean; + const isAdmin = get(npStart.core.application.capabilities, 'code.admin') as boolean; return (
diff --git a/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx b/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx index d5e0f4a52d12df..56f518fc82aa45 100644 --- a/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx +++ b/x-pack/legacy/plugins/code/public/components/admin_page/project_tab.tsx @@ -27,9 +27,11 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; +import { get } from 'lodash'; import React, { ChangeEvent } from 'react'; import { connect } from 'react-redux'; -import { capabilities } from 'ui/capabilities'; +import { npStart } from 'ui/new_platform'; + import { Repository } from '../../../model'; import { closeToast, importRepo, RepoStatus } from '../../actions'; import { RootState } from '../../reducers'; @@ -250,7 +252,7 @@ class CodeProjectTab extends React.PureComponent { project={repo} showStatus={true} status={status[repo.uri]} - enableManagement={capabilities.get().code.admin as boolean} + enableManagement={get(npStart.core.application.capabilities, 'code.admin') as boolean} /> )); @@ -292,7 +294,7 @@ class CodeProjectTab extends React.PureComponent { - {(capabilities.get().code.admin as boolean) && ( + {(get(npStart.core.application.capabilities, 'code.admin') as boolean) && ( // @ts-ignore { public switchTab = (tab: Tabs) => { const { history } = this.props; const { pathname, search } = history.location; - // @ts-ignore - history.push(QueryString.replaceParamInUrl(`${pathname}${search}`, 'sideTab', tab)); + history.push(replaceParamInUrl(`${pathname}${search}`, 'sideTab', tab)); }; public render() { diff --git a/x-pack/legacy/plugins/code/public/components/search_page/search.tsx b/x-pack/legacy/plugins/code/public/components/search_page/search.tsx index 2813fbd389a8aa..8ccc46d09502f8 100644 --- a/x-pack/legacy/plugins/code/public/components/search_page/search.tsx +++ b/x-pack/legacy/plugins/code/public/components/search_page/search.tsx @@ -9,9 +9,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import querystring from 'querystring'; import React from 'react'; import { connect } from 'react-redux'; -import chrome from 'ui/chrome'; import url from 'url'; +import { npStart } from 'ui/new_platform'; +import { APP_TITLE } from '../../../common/constants'; import { DocumentSearchResult, SearchOptions, SearchScope } from '../../../model'; import { changeSearchScope } from '../../actions'; import { RootState } from '../../reducers'; @@ -53,11 +54,16 @@ class SearchPage extends React.PureComponent { public componentDidMount() { // track search page load count trackCodeUiMetric(METRIC_TYPE.LOADED, CodeUIUsageMetrics.SEARCH_PAGE_LOAD_COUNT); - chrome.breadcrumbs.push({ text: `Search` }); + npStart.core.chrome.setBreadcrumbs([ + { text: APP_TITLE, href: '#/' }, + { + text: 'Search', + }, + ]); } public componentWillUnmount() { - chrome.breadcrumbs.pop(); + npStart.core.chrome.setBreadcrumbs([{ text: APP_TITLE, href: '#/' }]); } public onLanguageFilterToggled = (lang: string) => { diff --git a/x-pack/legacy/plugins/code/public/components/shared/resize_checker.tsx b/x-pack/legacy/plugins/code/public/components/shared/resize_checker.tsx new file mode 100644 index 00000000000000..9693f4870c5b89 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/components/shared/resize_checker.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Copied from `ui/resize_checker` because of NP migration. + */ + +import { EventEmitter } from 'events'; +import $ from 'jquery'; +import { isEqual } from 'lodash'; +import ResizeObserver from 'resize-observer-polyfill'; + +function validateElArg(el: HTMLElement) { + // the ResizeChecker historically accepted jquery elements, + // so we wrap in jQuery then extract the element + const $el = $(el); + + if ($el.length !== 1) { + throw new TypeError('ResizeChecker must be constructed with a single DOM element.'); + } + + return $el.get(0); +} + +function getSize(el: HTMLElement): [number, number] { + return [el.clientWidth, el.clientHeight]; +} + +/** + * ResizeChecker receives an element and emits a "resize" event every time it changes size. + */ +export class ResizeChecker extends EventEmitter { + private destroyed: boolean = false; + private el: HTMLElement | null; + private observer: ResizeObserver | null; + private expectedSize: [number, number] | null = null; + + constructor(el: HTMLElement, args: { disabled?: boolean } = {}) { + super(); + + this.el = validateElArg(el); + + this.observer = new ResizeObserver(() => { + if (this.expectedSize) { + const sameSize = isEqual(getSize(el), this.expectedSize); + this.expectedSize = null; + + if (sameSize) { + // don't trigger resize notification if the size is what we expect + return; + } + } + + this.emit('resize'); + }); + + // Only enable the checker immediately if args.disabled wasn't set to true + if (!args.disabled) { + this.enable(); + } + } + + public enable() { + if (this.destroyed) { + // Don't allow enabling an already destroyed resize checker + return; + } + // the width and height of the element that we expect to see + // on the next resize notification. If it matches the size at + // the time of starting observing then it we will be ignored. + // We know that observer and el are not null since we are not yet destroyed. + this.expectedSize = getSize(this.el!); + this.observer!.observe(this.el!); + } + + /** + * Run a function and ignore all resizes that occur + * while it's running. + */ + public modifySizeWithoutTriggeringResize(block: () => void): void { + try { + block(); + } finally { + if (this.el) { + this.expectedSize = getSize(this.el); + } + } + } + + /** + * Tell the ResizeChecker to shutdown, stop listenings, and never + * emit another resize event. + * + * Cleans up it's listeners and timers. + */ + public destroy(): void { + if (this.destroyed) { + return; + } + this.destroyed = true; + + this.observer!.disconnect(); + this.observer = null; + this.expectedSize = null; + this.el = null; + this.removeAllListeners(); + } +} diff --git a/x-pack/legacy/plugins/code/public/monaco/monaco_diff_editor.ts b/x-pack/legacy/plugins/code/public/monaco/monaco_diff_editor.ts index 98486da772d84a..12cb75825cb641 100644 --- a/x-pack/legacy/plugins/code/public/monaco/monaco_diff_editor.ts +++ b/x-pack/legacy/plugins/code/public/monaco/monaco_diff_editor.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ResizeChecker } from 'ui/resize_checker'; + +import { ResizeChecker } from '../components/shared/resize_checker'; import { monaco } from './monaco'; export class MonacoDiffEditor { public diffEditor: monaco.editor.IDiffEditor | null = null; diff --git a/x-pack/legacy/plugins/code/public/monaco/monaco_helper.ts b/x-pack/legacy/plugins/code/public/monaco/monaco_helper.ts index cf185d4eb3e658..3dee78b9be803a 100644 --- a/x-pack/legacy/plugins/code/public/monaco/monaco_helper.ts +++ b/x-pack/legacy/plugins/code/public/monaco/monaco_helper.ts @@ -5,7 +5,8 @@ */ import { editor } from 'monaco-editor'; -import { ResizeChecker } from 'ui/resize_checker'; + +import { ResizeChecker } from '../components/shared/resize_checker'; import { EditorActions } from '../components/editor/editor'; import { toCanonicalUrl } from '../../common/uri_util'; diff --git a/x-pack/legacy/plugins/code/public/utils/query_string.ts b/x-pack/legacy/plugins/code/public/utils/query_string.ts new file mode 100644 index 00000000000000..6fc98f3dd44376 --- /dev/null +++ b/x-pack/legacy/plugins/code/public/utils/query_string.ts @@ -0,0 +1,119 @@ +/* + * 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. + */ + +/** + * Copied from `ui/utils/query_string` because of NP migration. + */ + +function encodeQueryComponent(val: string, pctEncodeSpaces = false) { + return encodeURIComponent(val) + .replace(/%40/gi, '@') + .replace(/%3A/gi, ':') + .replace(/%24/g, '$') + .replace(/%2C/gi, ',') + .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); +} + +function tryDecodeURIComponent(value: string) { + try { + return decodeURIComponent(value); + } catch (e) {} // eslint-disable-line no-empty +} + +/** + * Parses an escaped url query string into key-value pairs. + * @returns {Object.} + */ +const decode = (keyValue: any) => { + const obj: { [s: string]: any } = {}; + let keyValueParts; + let key; + + (keyValue || '').split('&').forEach((keyVal: any) => { + if (keyVal) { + keyValueParts = keyVal.split('='); + key = tryDecodeURIComponent(keyValueParts[0]); + if (key !== void 0) { + const val = keyValueParts[1] !== void 0 ? tryDecodeURIComponent(keyValueParts[1]) : true; + if (!obj[key]) { + obj[key] = val; + } else if (Array.isArray(obj[key])) { + obj[key].push(val); + } else { + obj[key] = [obj[key], val]; + } + } + } + }); + return obj; +}; + +/** + * Creates a queryString out of an object + * @param {Object} obj + * @return {String} + */ +const encode = (obj: any) => { + const parts: any[] = []; + const keys = Object.keys(obj).sort(); + keys.forEach((key: any) => { + const value = obj[key]; + if (Array.isArray(value)) { + value.forEach((arrayValue: any) => { + parts.push(param(key, arrayValue)); + }); + } else { + parts.push(param(key, value)); + } + }); + return parts.length ? parts.join('&') : ''; +}; + +const param = (key: string, val: any) => { + return ( + encodeQueryComponent(key, true) + (val === true ? '' : '=' + encodeQueryComponent(val, true)) + ); +}; + +/** + * Extracts the query string from a url + * @param {String} url + * @return {Object} - returns an object describing the start/end index of the url in the string. The indices will be + * the same if the url does not have a query string + */ +const findInUrl = (url: string) => { + let qsStart = url.indexOf('?'); + let hashStart = url.lastIndexOf('#'); + + if (hashStart === -1) { + // out of bounds + hashStart = url.length; + } + + if (qsStart === -1) { + qsStart = hashStart; + } + + return { + start: qsStart, + end: hashStart, + }; +}; + +export const replaceParamInUrl = (url: string, p: string, newVal: any) => { + const loc = findInUrl(url); + const parsed = decode(url.substring(loc.start + 1, loc.end)); + + if (newVal != null) { + parsed[p] = newVal; + } else { + delete parsed[p]; + } + + const chars = url.split(''); + chars.splice(loc.start, loc.end - loc.start, '?' + encode(parsed)); + return chars.join(''); +}; From 2df5484e8d22865d2b427a621bc4ef4de678debb Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 30 Sep 2019 16:47:21 -0400 Subject: [PATCH 04/53] [Canvas] Translate Arguments and Datasources (#46521) * I18n arguments and datasources and transforms * Fix Key --- .../canvas/canvas_plugin_src/strings/ui.ts | 422 +++++++++++++++++- .../axis_config/extended_template.tsx | 9 +- .../uis/arguments/axis_config/index.ts | 7 +- .../uis/arguments/datacolumn/index.js | 6 +- .../datacolumn/simple_math_function.js | 23 +- .../uis/arguments/date_format/index.ts | 7 +- .../canvas_plugin_src/uis/arguments/number.js | 7 +- .../uis/arguments/number_format/index.ts | 17 +- .../uis/arguments/percentage.js | 7 +- .../canvas_plugin_src/uis/arguments/range.js | 7 +- .../canvas_plugin_src/uis/arguments/select.js | 7 +- .../canvas_plugin_src/uis/arguments/shape.js | 7 +- .../canvas_plugin_src/uis/arguments/string.js | 7 +- .../uis/arguments/textarea.js | 7 +- .../canvas_plugin_src/uis/arguments/toggle.js | 7 +- .../uis/datasources/demodata.js | 25 +- .../uis/datasources/essql.js | 9 +- .../uis/datasources/timelion.js | 44 +- .../uis/models/point_series.js | 25 +- .../uis/transforms/formatdate.ts | 7 +- .../uis/transforms/formatnumber.ts | 7 +- .../uis/transforms/rounddate.ts | 9 +- .../canvas_plugin_src/uis/transforms/sort.js | 9 +- .../legacy/plugins/canvas/i18n/constants.ts | 1 + .../datasource_preview/datasource_preview.js | 2 +- .../lib/template_from_react_component.tsx | 7 +- 26 files changed, 601 insertions(+), 91 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/ui.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/ui.ts index b48846f28b9d62..7082bba3b6b2a0 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/ui.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/ui.ts @@ -5,9 +5,120 @@ */ import { i18n } from '@kbn/i18n'; -import { BOOLEAN_FALSE, BOOLEAN_TRUE, CSS, URL, MARKDOWN, HTML, HEX, RGB } from '../../i18n'; + +import { + BOOLEAN_FALSE, + BOOLEAN_TRUE, + CANVAS, + CSS, + ELASTICSEARCH, + HEX, + HTML, + KIBANA, + LUCENE, + MARKDOWN, + MOMENTJS, + NUMERALJS, + RGB, + SQL, + TIMELION, + URL, +} from '../../i18n'; export const ArgumentStrings = { + AxisConfig: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfigTitle', { + defaultMessage: 'Axis config', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfigLabel', { + defaultMessage: 'Visualization axis configuration', + }), + getPositionLabel: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfig.positionLabel', { + defaultMessage: 'Position', + }), + getPositionTop: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.topDropDown', { + defaultMessage: 'top', + }), + getPositionBottom: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.bottomDropDown', { + defaultMessage: 'bottom', + }), + getPositionRight: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.rightDropDown', { + defaultMessage: 'right', + }), + getPositionLeft: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.leftDropDown', { + defaultMessage: 'left', + }), + }, + DataColumn: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumnTitle', { + defaultMessage: 'Column', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumnLabel', { + defaultMessage: 'Select the data column', + }), + getOptionAverage: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.averageDropDown', { + defaultMessage: 'Average', + }), + getOptionCount: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.countDropDown', { + defaultMessage: 'Count', + }), + getOptionFirst: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.firstDropDown', { + defaultMessage: 'First', + }), + getOptionLast: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.lastDropDown', { + defaultMessage: 'Last', + }), + getOptionMax: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.maxDropDown', { + defaultMessage: 'Max', + }), + getOptionMedian: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.medianDropDown', { + defaultMessage: 'Median', + }), + getOptionMin: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.minDropDown', { + defaultMessage: 'Min', + }), + getOptionSum: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.sumDropDown', { + defaultMessage: 'Sum', + }), + getOptionUnique: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.uniqueDropDown', { + defaultMessage: 'Unique', + }), + getOptionValue: () => + i18n.translate('xpack.canvas.uis.arguments.dataColumn.options.valueDropDown', { + defaultMessage: 'Value', + }), + }, + DateFormat: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.dateFormatTitle', { + defaultMessage: 'Date Format', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.dateFormatLabel', { + defaultMessage: 'Select or enter a {momentJS} format', + values: { + momentJS: MOMENTJS, + }, + }), + }, FilterGroup: { getDisplayName: () => i18n.translate('xpack.canvas.uis.arguments.filterGroupTitle', { @@ -59,6 +170,49 @@ export const ArgumentStrings = { defaultMessage: 'Asset', }), }, + Number: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.numberTitle', { + defaultMessage: 'Number', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.numberLabel', { + defaultMessage: 'Input a number', + }), + }, + NumberFormat: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormatTitle', { + defaultMessage: 'Number Format', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormatLabel', { + defaultMessage: 'Select or enter a valid {numeralJS} format', + values: { + numeralJS: NUMERALJS, + }, + }), + getFormatNumber: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormat.format.numberDropDown', { + defaultMessage: 'Number', + }), + getFormatPercent: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormat.format.percentDropDown', { + defaultMessage: 'Percent', + }), + getFormatCurrency: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormat.format.currencyDropDown', { + defaultMessage: 'Currency', + }), + getFormatDuration: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormat.format.durationDropDown', { + defaultMessage: 'Duration', + }), + getFormatBytes: () => + i18n.translate('xpack.canvas.uis.arguments.numberFormat.format.bytesDropDown', { + defaultMessage: 'Bytes', + }), + }, Palette: { getDisplayName: () => i18n.translate('xpack.canvas.uis.arguments.paletteTitle', { @@ -69,6 +223,272 @@ export const ArgumentStrings = { defaultMessage: 'Choose a color palette', }), }, + Percentage: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.percentageTitle', { + defaultMessage: 'Percentage', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.percentageLabel', { + defaultMessage: 'Slider for percentage ', + }), + }, + Range: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.rangeTitle', { + defaultMessage: 'Range', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.rangeLabel', { + defaultMessage: 'Slider for values within a range', + }), + }, + Select: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.selectTitle', { + defaultMessage: 'Select', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.selectLabel', { + defaultMessage: 'Select from multiple options in a drop down', + }), + }, + Shape: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.shapeTitle', { + defaultMessage: 'Shape', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.shapeLabel', { + defaultMessage: 'Shape picker', + }), + }, + String: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.stringTitle', { + defaultMessage: 'String', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.stringLabel', { + defaultMessage: 'Input short strings', + }), + }, + Textarea: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.textareaTitle', { + defaultMessage: 'Textarea', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.textareaLabel', { + defaultMessage: 'Input long strings', + }), + }, + Toggle: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.arguments.toggleTitle', { + defaultMessage: 'Toggle', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.arguments.toggleLabel', { + defaultMessage: 'A true/false toggle switch', + }), + }, +}; + +export const DataSourceStrings = { + DemoData: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.dataSources.demoDataTitle', { + defaultMessage: 'Demo data', + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.dataSources.demoDataLabel', { + defaultMessage: 'Mock data set with usernames, prices, projects, countries, and phases', + }), + getHeading: () => + i18n.translate('xpack.canvas.uis.dataSources.demoData.headingTitle', { + defaultMessage: 'You are using demo data', + }), + }, + Essql: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.dataSources.essqlTitle', { + defaultMessage: '{elasticsearch} {sql}', + values: { + elasticsearch: ELASTICSEARCH, + sql: SQL, + }, + }), + getHelp: () => + i18n.translate('xpack.canvas.uis.dataSources.essqlLabel', { + defaultMessage: 'Use {elasticsearch} {sql} to get a data table', + values: { + elasticsearch: ELASTICSEARCH, + sql: SQL, + }, + }), + getLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitle', { + defaultMessage: '{elasticsearch} {sql} query', + values: { + elasticsearch: ELASTICSEARCH, + sql: SQL, + }, + }), + }, + Timelion: { + getHelp: () => + i18n.translate('xpack.canvas.uis.dataSources.timelionLabel', { + defaultMessage: 'Use {timelion} syntax to retrieve a timeseries', + values: { + timelion: TIMELION, + }, + }), + getAbout: () => + i18n.translate('xpack.canvas.uis.dataSources.timelion.aboutDetail', { + defaultMessage: + '{canvas} integrates with {kibanaTimelion} application to allow you to use {timelion} queries to pull back timeseries data in a tabular format that can be used with {canvas} elements.', + values: { + timelion: TIMELION, + kibanaTimelion: `${KIBANA}'s ${TIMELION}`, + canvas: CANVAS, + }, + }), + getQueryLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.timelion.queryTitle', { + defaultMessage: 'Query', + }), + getQueryHelp: () => + i18n.translate('xpack.canvas.uis.dataSources.timelion.queryLabel', { + defaultMessage: '{lucene} Query String syntax', + values: { + lucene: LUCENE, + }, + }), + getIntervalLabel: () => + i18n.translate('xpack.canvas.uis.dataSources.timelion.intervalTitle', { + defaultMessage: 'Interval', + }), + getIntervalHelp: () => + i18n.translate('xpack.canvas.uis.dataSources.timelion.intervalLabel', { + defaultMessage: + 'Accepts {elasticsearch} date math: {weeksExample}, {daysExample}, {secondsExample}, or {auto}', + values: { + elasticsearch: ELASTICSEARCH, + secondsExample: '10s', + daysExample: '5d', + weeksExample: '1w', + auto: 'auto', + }, + }), + getTipsHeading: () => + i18n.translate('xpack.canvas.uis.dataSources.timelion.tipsTitle', { + defaultMessage: 'Some tips', + }), + }, +}; + +export const ModelStrings = { + PointSeries: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.models.pointSeriesTitle', { + defaultMessage: 'Dimensions & measures', + }), + getXAxisDisplayName: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.xaxisTitle', { + defaultMessage: 'X-axis', + }), + getXAxisHelp: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.xaxisLabel', { + defaultMessage: 'Data along the horizontal axis. Usually a number, string or date', + }), + getYaxisDisplayName: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.yaxisTitle', { + defaultMessage: 'Y-axis', + }), + getYaxisHelp: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.yaxisLabel', { + defaultMessage: 'Data along the vertical axis. Usually a number', + }), + getColorDisplayName: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.colorTitle', { + defaultMessage: 'Color', + }), + getColorHelp: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.colorLabel', { + defaultMessage: 'Determines the color of a mark or series', + }), + getSizeDisplayName: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.sizeTitle', { + defaultMessage: 'Size', + }), + getSizeHelp: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.sizeLabel', { + defaultMessage: 'Determine the size of a mark', + }), + getTextDisplayName: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.textTitle', { + defaultMessage: 'Text', + }), + getTextHelp: () => + i18n.translate('xpack.canvas.uis.models.pointSeries.args.textLabel', { + defaultMessage: 'Set the text to use as, or around, the mark', + }), + }, +}; + +export const TransformStrings = { + FormatDate: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.formatDateTitle', { + defaultMessage: 'Date format', + }), + getFormatDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.formatDate.args.formatTitle', { + defaultMessage: 'Format', + }), + }, + FormatNumber: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.formatNumberTitle', { + defaultMessage: 'Number format', + }), + getFormatDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.formatNumber.args.formatTitle', { + defaultMessage: 'Format', + }), + }, + RoundDate: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.roundDateTitle', { + defaultMessage: 'Round date', + }), + getFormatDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.roundDate.args.formatTitle', { + defaultMessage: 'Format', + }), + getFormatHelp: () => + i18n.translate('xpack.canvas.uis.transforms.roundDate.args.formatLabel', { + defaultMessage: 'Select or enter a {momentJs} format to round the date', + values: { + momentJs: MOMENTJS, + }, + }), + }, + Sort: { + getDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.sortTitle', { + defaultMessage: 'Datatable sorting', + }), + getSortFieldDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.sort.args.sortFieldTitle', { + defaultMessage: 'Sort field', + }), + getReverseDisplayName: () => + i18n.translate('xpack.canvas.uis.transforms.sort.args.reverseToggleSwitch', { + defaultMessage: 'Descending', + }), + }, }; export const ViewStrings = { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx index 4521a0d113a49d..6061169cbe9f6b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx @@ -10,6 +10,9 @@ import { EuiSelect, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; import immutable from 'object-path-immutable'; import { get } from 'lodash'; import { ExpressionAST } from '../../../../types'; +import { ArgumentStrings } from '../../../strings'; + +const { AxisConfig: strings } = ArgumentStrings; const { set } = immutable; @@ -72,8 +75,8 @@ export class ExtendedTemplate extends PureComponent { } const positions = { - xaxis: ['bottom', 'top'], - yaxis: ['left', 'right'], + xaxis: [strings.getPositionBottom(), strings.getPositionTop()], + yaxis: [strings.getPositionLeft(), strings.getPositionRight()], }; const argName = this.props.typeInstance.name; const position = this.getArgValue('position', positions[argName][0]); @@ -82,7 +85,7 @@ export class ExtendedTemplate extends PureComponent { return ( - + ({ name: 'axisConfig', - displayName: 'Axis config', - help: 'Visualization axis configuration', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(SimpleTemplate), template: templateFromReactComponent(ExtendedTemplate), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js index 8f7907dfeeb46e..ffa0c6983d5733 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js @@ -12,9 +12,11 @@ import { sortBy } from 'lodash'; import { getType } from '@kbn/interpreter/common'; import { createStatefulPropHoc } from '../../../../public/components/enhance/stateful_prop'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../../strings'; import { SimpleMathFunction } from './simple_math_function'; import { getFormObject } from './get_form_object'; +const { DataColumn: strings } = ArgumentStrings; const maybeQuoteValue = val => (val.match(/\s/) ? `'${val}'` : val); // TODO: Garbage, we could make a much nicer math form that can handle way more. @@ -141,8 +143,8 @@ EnhancedDatacolumnArgInput.propTypes = { export const datacolumn = () => ({ name: 'datacolumn', - displayName: 'Column', - help: 'Select the data column', + displayName: strings.getDisplayName(), + help: strings.getHelp(), default: '""', simpleTemplate: templateFromReactComponent(EnhancedDatacolumnArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js index 7f4a333ced00cd..219b3582a7526f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js @@ -7,22 +7,25 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiSelect } from '@elastic/eui'; +import { ArgumentStrings } from '../../../strings'; + +const { DataColumn: strings } = ArgumentStrings; export const SimpleMathFunction = ({ onChange, value, inputRef, onlymath }) => { const options = [ - { text: 'Average', value: 'mean' }, - { text: 'Count', value: 'size' }, - { text: 'First', value: 'first' }, - { text: 'Last', value: 'last' }, - { text: 'Max', value: 'max' }, - { text: 'Median', value: 'median' }, - { text: 'Min', value: 'min' }, - { text: 'Sum', value: 'sum' }, - { text: 'Unique', value: 'unique' }, + { text: strings.getOptionAverage(), value: 'mean' }, + { text: strings.getOptionCount(), value: 'size' }, + { text: strings.getOptionFirst(), value: 'first' }, + { text: strings.getOptionLast(), value: 'last' }, + { text: strings.getOptionMax(), value: 'max' }, + { text: strings.getOptionMedian(), value: 'median' }, + { text: strings.getOptionMin(), value: 'min' }, + { text: strings.getOptionSum(), value: 'sum' }, + { text: strings.getOptionUnique(), value: 'unique' }, ]; if (!onlymath) { - options.unshift({ text: 'Value', value: '' }); + options.unshift({ text: strings.getOptionValue(), value: '' }); } return ( diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts index f0aeecd276b820..6bf6f38f7d3a2c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts @@ -11,6 +11,9 @@ import { AdvancedSettings } from '../../../../public/lib/kibana_advanced_setting // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; +import { ArgumentStrings } from '../../../strings'; + +const { DateFormat: strings } = ArgumentStrings; const formatMap = { DEFAULT: AdvancedSettings.get('dateFormat'), @@ -35,7 +38,7 @@ export const DateFormatArgInput = compose(withProps({ date export const dateFormat: ArgumentFactory = () => ({ name: 'dateFormat', - displayName: 'Date Format', - help: 'Select or enter a MomentJS format', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(DateFormatArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number.js index 49c22e80a2eb60..1ed8f10f6b3103 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number.js @@ -11,6 +11,9 @@ import { EuiFieldNumber, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/e import { get } from 'lodash'; import { createStatefulPropHoc } from '../../../public/components/enhance/stateful_prop'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { Number: strings } = ArgumentStrings; // This is basically a direct copy of the string input, but with some Number() goodness maybe you think that's cheating and it should be // abstracted. If you can think of a 3rd or 4th usage for that abstraction, cool, do it, just don't make it more confusing. Copying is the @@ -62,8 +65,8 @@ EnhancedNumberArgInput.propTypes = { export const number = () => ({ name: 'number', - displayName: 'number', - help: 'Input a number', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(EnhancedNumberArgInput), default: '0', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index 28eed414bcc2b8..8f68042f0dca64 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -10,6 +10,9 @@ import { AdvancedSettings } from '../../../../public/lib/kibana_advanced_setting // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; +import { ArgumentStrings } from '../../../strings'; + +const { NumberFormat: strings } = ArgumentStrings; const formatMap = { NUMBER: AdvancedSettings.get('format:number:defaultPattern'), @@ -20,11 +23,11 @@ const formatMap = { }; const numberFormats = [ - { value: formatMap.NUMBER, text: 'Number' }, - { value: formatMap.PERCENT, text: 'Percent' }, - { value: formatMap.CURRENCY, text: 'Currency' }, - { value: formatMap.DURATION, text: 'Duration' }, - { value: formatMap.BYTES, text: 'Bytes' }, + { value: formatMap.NUMBER, text: strings.getFormatNumber() }, + { value: formatMap.PERCENT, text: strings.getFormatPercent() }, + { value: formatMap.CURRENCY, text: strings.getFormatCurrency() }, + { value: formatMap.DURATION, text: strings.getFormatDuration() }, + { value: formatMap.BYTES, text: strings.getFormatBytes() }, ]; export const NumberFormatArgInput = compose(withProps({ numberFormats }))( @@ -33,7 +36,7 @@ export const NumberFormatArgInput = compose(withProps({ nu export const numberFormat: ArgumentFactory = () => ({ name: 'numberFormat', - displayName: 'Number Format', - help: 'Select or enter a valid NumeralJS format', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(NumberFormatArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js index da4af530b5d121..82a234042f8111 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/percentage.js @@ -8,6 +8,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiRange } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { Percentage: strings } = ArgumentStrings; const PercentageArgInput = ({ onValueChange, argValue }) => { const handleChange = ev => { @@ -35,7 +38,7 @@ PercentageArgInput.propTypes = { export const percentage = () => ({ name: 'percentage', - displayName: 'Percentage', - help: 'Slider for percentage ', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(PercentageArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/range.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/range.js index 598ac2cee5d18a..7b99fc8c5442f9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/range.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/range.js @@ -8,6 +8,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiRange } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { Range: strings } = ArgumentStrings; const RangeArgInput = ({ typeInstance, onValueChange, argValue }) => { const { min, max, step } = typeInstance.options; @@ -44,7 +47,7 @@ RangeArgInput.propTypes = { export const range = () => ({ name: 'range', - displayName: 'Range', - help: 'Slider for values within a range', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(RangeArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/select.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/select.js index 39f0f4da359262..095aa2dc4f3fdc 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/select.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/select.js @@ -8,6 +8,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiSelect } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { Select: strings } = ArgumentStrings; const SelectArgInput = ({ typeInstance, onValueChange, argValue, argId }) => { const choices = typeInstance.options.choices.map(({ value, name }) => ({ value, text: name })); @@ -43,7 +46,7 @@ SelectArgInput.propTypes = { export const select = () => ({ name: 'select', - displayName: 'Select', - help: 'Select from multiple options in a drop down', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(SelectArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js index 26b13e4b4175ec..7a3dad36dcf019 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/shape.js @@ -9,6 +9,9 @@ import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { ShapePickerPopover } from '../../../public/components/shape_picker_popover/'; +import { ArgumentStrings } from '../../strings'; + +const { Shape: strings } = ArgumentStrings; const ShapeArgInput = ({ onValueChange, argValue, typeInstance }) => ( @@ -32,8 +35,8 @@ ShapeArgInput.propTypes = { export const shape = () => ({ name: 'shape', - displayName: 'Shape', - help: 'Shape picker', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(ShapeArgInput), default: '"square"', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js index 9c117e694a1fda..46749cc8562e26 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js @@ -11,6 +11,9 @@ import { EuiFlexItem, EuiFlexGroup, EuiFieldText, EuiButton } from '@elastic/eui import { get } from 'lodash'; import { createStatefulPropHoc } from '../../../public/components/enhance/stateful_prop'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { String: strings } = ArgumentStrings; const StringArgInput = ({ updateValue, value, confirm, commit, argId }) => ( @@ -57,7 +60,7 @@ EnhancedStringArgInput.propTypes = { export const string = () => ({ name: 'string', - displayName: 'String', - help: 'Input short strings', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(EnhancedStringArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js index 61dde4d5187fd6..e7009300b32e63 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/textarea.js @@ -11,6 +11,9 @@ import { EuiFormRow, EuiTextArea, EuiSpacer, EuiButton } from '@elastic/eui'; import { get } from 'lodash'; import { createStatefulPropHoc } from '../../../public/components/enhance/stateful_prop'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { Textarea: strings } = ArgumentStrings; const TextAreaArgInput = ({ updateValue, value, confirm, commit, renderError, argId }) => { if (typeof value !== 'string') { @@ -66,7 +69,7 @@ EnhancedTextAreaArgInput.propTypes = { export const textarea = () => ({ name: 'textarea', - displayName: 'Textarea', - help: 'Input long strings', + displayName: strings.getDisplayName(), + help: strings.getHelp(), template: templateFromReactComponent(EnhancedTextAreaArgInput), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index d3aab4cf811e58..e52679d80e908f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -8,6 +8,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiFormRow, EuiSwitch } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { ArgumentStrings } from '../../strings'; + +const { Toggle: strings } = ArgumentStrings; const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => { const handleChange = () => onValueChange(!argValue); @@ -31,8 +34,8 @@ ToggleArgInput.propTypes = { export const toggle = () => ({ name: 'toggle', - displayName: 'Toggle', - help: 'A true/false toggle switch', + displayName: strings.getDisplayName(), + help: strings.getHelp(), simpleTemplate: templateFromReactComponent(ToggleArgInput), default: 'false', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js index 32be8b79d8fc91..6438cc56897476 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js @@ -5,25 +5,36 @@ */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { DataSourceStrings } from '../../strings'; +import { ComponentStrings, CANVAS } from '../../../i18n'; + +const { DemoData: strings } = DataSourceStrings; const DemodataDatasource = () => ( -

You are using demo data

+

{strings.getHeading()}

- This data source is connected to every Canvas element by default. Its purpose is to give you - some playground data to get started. The demo set contains 4 strings, 3 numbers and a date. - Feel free to experiment and, when you're ready, click Change your data source{' '} - above to connect to your own data. + {ComponentStrings.DatasourceDatasourceComponent.getChangeButtonLabel()} + ), + }} + />

); export const demodata = () => ({ name: 'demodata', - displayName: 'Demo data', - help: 'Mock data set with usernames, prices, projects, countries, and phases', + displayName: strings.getDisplayName(), + help: strings.getHelp(), // Replace this with a better icon when we have time. image: 'logoElasticStack', template: templateFromReactComponent(DemodataDatasource), diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js index 1973737be14792..8b6b8695a74a3a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js @@ -9,6 +9,9 @@ import PropTypes from 'prop-types'; import { EuiFormRow, EuiTextArea } from '@elastic/eui'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { DataSourceStrings } from '../../strings'; + +const { Essql: strings } = DataSourceStrings; class EssqlDatasource extends PureComponent { componentDidMount() { @@ -56,7 +59,7 @@ class EssqlDatasource extends PureComponent { const { isInvalid } = this.props; return ( - + ({ name: 'essql', - displayName: 'Elasticsearch SQL', - help: 'Use Elasticsearch SQL to get a data table', + displayName: strings.getDisplayName(), + help: strings.getHelp(), // Replace this with a SQL logo when we have one in EUI image: 'logoElasticsearch', template: templateFromReactComponent(EssqlDatasource), diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js index 1f7770538e8a0c..3065f0fc713073 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js @@ -15,8 +15,13 @@ import { EuiText, EuiTextArea, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; +import { DataSourceStrings } from '../../strings'; +import { TIMELION, CANVAS } from '../../../i18n'; + +const { Timelion: strings } = DataSourceStrings; const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => { const DEFAULT_QUERY = `.es(index=${defaultIndex})`; @@ -54,16 +59,13 @@ const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => { return (
-

Timelion

-

- Canvas integrates with Kibana's Timelion application to allow you to use Timelion queries - to pull back timeseries data in a tabular format that can be used with Canvas elements. -

+

{TIMELION}

+

{strings.getAbout()}

- + { // TODO: Time timelion interval picker should be a drop down } { - +
  • - Timelion requires a time range, you should add a time filter element to your page - somewhere, or use the code editor to pass in a time filter. +
  • - Some Timelion functions, such as .color(), don't translate to a - Canvas data table. Anything todo with data manipulation should work grand. + .color(), + }} + />
@@ -111,8 +125,8 @@ TimelionDatasource.propTypes = { export const timelion = () => ({ name: 'timelion', - displayName: 'Timelion', - help: 'Use Timelion syntax to retrieve a timeseries', + displayName: TIMELION, + help: strings.getHelp(), image: 'timelionApp', template: templateFromReactComponent(TimelionDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/models/point_series.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/models/point_series.js index 0cc6570b285295..bea7e4d6435680 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/models/point_series.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/models/point_series.js @@ -6,39 +6,42 @@ import { get } from 'lodash'; import { getState, getValue } from '../../../public/lib/resolved_arg'; +import { ModelStrings } from '../../strings'; + +const { PointSeries: strings } = ModelStrings; export const pointseries = () => ({ name: 'pointseries', - displayName: 'Dimensions & measures', + displayName: strings.getDisplayName(), args: [ { name: 'x', - displayName: 'X-axis', - help: 'Data along the horizontal axis. Usually a number, string or date', + displayName: strings.getXAxisDisplayName(), + help: strings.getXAxisHelp(), argType: 'datacolumn', }, { name: 'y', - displayName: 'Y-axis', - help: 'Data along the vertical axis. Usually a number', + displayName: strings.getYaxisDisplayName(), + help: strings.getYaxisHelp(), argType: 'datacolumn', }, { name: 'color', - displayName: 'Color', - help: 'Determines the color of a mark or series', + displayName: strings.getColorDisplayName(), + help: strings.getColorHelp(), argType: 'datacolumn', }, { name: 'size', - displayName: 'Size', - help: 'Determine the size of a mark', + displayName: strings.getSizeDisplayName(), + help: strings.getSizeHelp(), argType: 'datacolumn', }, { name: 'text', - displayName: 'Text', - help: 'Set the text to use as, or around, the mark', + displayName: strings.getTextDisplayName(), + help: strings.getTextHelp(), argType: 'datacolumn', }, ], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatdate.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatdate.ts index 08c8c0e68a1b4c..a2e440dd62e132 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatdate.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatdate.ts @@ -6,14 +6,17 @@ import { TransformFactory } from '../../../types/transforms'; import { Arguments } from '../../functions/common/formatdate'; +import { TransformStrings } from '../../strings'; + +const { FormatDate: strings } = TransformStrings; export const formatdate: TransformFactory = () => ({ name: 'formatdate', - displayName: 'Date format', + displayName: strings.getDisplayName(), args: [ { name: 'format', - displayName: 'Format', + displayName: strings.getFormatDisplayName(), argType: 'dateformat', }, ], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatnumber.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatnumber.ts index c7c1ace456ed2e..a67695e635fb5f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatnumber.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/formatnumber.ts @@ -6,14 +6,17 @@ import { TransformFactory } from '../../../types/transforms'; import { Arguments } from '../../functions/common/formatnumber'; +import { TransformStrings } from '../../strings'; + +const { FormatNumber: strings } = TransformStrings; export const formatnumber: TransformFactory = () => ({ name: 'formatnumber', - displayName: 'Number format', + displayName: strings.getDisplayName(), args: [ { name: 'format', - displayName: 'Format', + displayName: strings.getFormatDisplayName(), argType: 'numberformat', }, ], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/rounddate.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/rounddate.ts index c94b32456f90c0..bc7c8b983bbae9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/rounddate.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/rounddate.ts @@ -6,16 +6,19 @@ import { TransformFactory } from '../../../types/transforms'; import { Arguments } from '../../functions/common/rounddate'; +import { TransformStrings } from '../../strings'; + +const { RoundDate: strings } = TransformStrings; export const rounddate: TransformFactory = () => ({ name: 'rounddate', - displayName: 'Round date', + displayName: strings.getDisplayName(), args: [ { name: 'format', - displayName: 'Format', + displayName: strings.getFormatDisplayName(), argType: 'dateformat', - help: 'Select or enter a MomentJS format to round the date', + help: strings.getFormatHelp(), }, ], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/sort.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/sort.js index e79b81df914288..e15f2166342237 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/sort.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/transforms/sort.js @@ -6,19 +6,22 @@ import { get } from 'lodash'; import { getState, getValue } from '../../../public/lib/resolved_arg'; +import { TransformStrings } from '../../strings'; + +const { Sort: strings } = TransformStrings; export const sort = () => ({ name: 'sort', - displayName: 'Datatable sorting', + displayName: strings.getDisplayName(), args: [ { name: '_', - displayName: 'Sort field', + displayName: strings.getSortFieldDisplayName(), argType: 'datacolumn', }, { name: 'reverse', - displayName: 'Descending', + displayName: strings.getReverseDisplayName(), argType: 'toggle', }, ], diff --git a/x-pack/legacy/plugins/canvas/i18n/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts index 349e77ac3bd537..3cc583f6af8a88 100644 --- a/x-pack/legacy/plugins/canvas/i18n/constants.ts +++ b/x-pack/legacy/plugins/canvas/i18n/constants.ts @@ -31,6 +31,7 @@ export const POST = 'POST'; export const RGB = 'RGB'; export const SQL = 'SQL'; export const SVG = 'SVG'; +export const TIMELION = 'Timelion'; export const TINYMATH = '`TinyMath`'; export const TINYMATH_URL = 'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html'; diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index 6340df8dced760..e6d2fe550a9356 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -19,8 +19,8 @@ import { } from '@elastic/eui'; import { Datatable } from '../../datatable'; import { Error } from '../../error'; - import { ComponentStrings } from '../../../../i18n'; + const { DatasourceDatasourcePreview: strings } = ComponentStrings; const { DatasourceDatasourceComponent: datasourceStrings } = ComponentStrings; diff --git a/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.tsx b/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.tsx index 76dc741a79eb54..f5059dfa04b952 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.tsx +++ b/x-pack/legacy/plugins/canvas/public/lib/template_from_react_component.tsx @@ -7,6 +7,7 @@ import React, { ComponentType, FunctionComponent } from 'react'; import { unmountComponentAtNode, render } from 'react-dom'; import PropTypes from 'prop-types'; +import { I18nProvider } from '@kbn/i18n/react'; import { ErrorBoundary } from '../components/enhance/error_boundary'; import { ArgumentHandlers } from '../../types/arguments'; @@ -23,7 +24,11 @@ export const templateFromReactComponent = (Component: ComponentType) => { return null; } - return ; + return ( + + + + ); }} ); From ee168f2765a19bc5fca52b2ad00bb025c39fa3c9 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 30 Sep 2019 23:16:11 +0200 Subject: [PATCH 05/53] Convert uiSettings tests to TypeScript (#46776) * convert create_or_upgrade_saved_config into TS * convert service tests to TS * convert router tests to TS * simplify stub setup * address comments --- src/legacy/server/kbn_server.d.ts | 2 + ..._stub.js => create_objects_client_stub.ts} | 32 +-- ...=> create_or_upgrade_saved_config.test.ts} | 64 +++--- .../create_or_upgrade.test.ts} | 34 +-- ... => is_config_version_upgradeable.test.ts} | 12 +- .../ui_settings_mixin.test.ts} | 102 +++++---- .../ui_settings/routes/__tests__/lib/index.js | 32 --- .../doc_exists.ts} | 82 ++++--- .../doc_missing.ts} | 56 +++-- .../doc_missing_and_index_read_only.ts} | 52 ++--- .../index.test.ts} | 16 +- .../lib/assert.ts} | 2 +- .../lib/chance.ts} | 0 .../integration_tests/lib/index.ts} | 9 +- .../lib/servers.ts} | 42 ++-- ...service.js => ui_settings_service.test.ts} | 215 ++++++++++-------- 16 files changed, 378 insertions(+), 374 deletions(-) rename src/legacy/ui/ui_settings/{__tests__/lib/create_objects_client_stub.js => create_objects_client_stub.ts} (59%) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{__tests__/create_or_upgrade_saved_config.js => create_or_upgrade_saved_config.test.ts} (82%) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{__tests__/create_or_upgrade_integration.js => integration_tests/create_or_upgrade.test.ts} (88%) rename src/legacy/ui/ui_settings/create_or_upgrade_saved_config/{__tests__/is_config_version_upgradeable.js => is_config_version_upgradeable.test.ts} (87%) rename src/legacy/ui/ui_settings/{__tests__/ui_settings_mixin_integration.js => integration_tests/ui_settings_mixin.test.ts} (58%) delete mode 100644 src/legacy/ui/ui_settings/routes/__tests__/lib/index.js rename src/legacy/ui/ui_settings/routes/{__tests__/doc_exists.js => integration_tests/doc_exists.ts} (82%) rename src/legacy/ui/ui_settings/routes/{__tests__/doc_missing.js => integration_tests/doc_missing.ts} (82%) rename src/legacy/ui/ui_settings/routes/{__tests__/doc_missing_and_index_read_only.js => integration_tests/doc_missing_and_index_read_only.ts} (85%) rename src/legacy/ui/ui_settings/routes/{__tests__/index.js => integration_tests/index.test.ts} (89%) rename src/legacy/ui/ui_settings/routes/{__tests__/lib/assert.js => integration_tests/lib/assert.ts} (93%) rename src/legacy/ui/ui_settings/routes/{__tests__/lib/chance.js => integration_tests/lib/chance.ts} (100%) rename src/legacy/ui/ui_settings/{__tests__/lib/index.js => routes/integration_tests/lib/index.ts} (84%) rename src/legacy/ui/ui_settings/routes/{__tests__/lib/servers.js => integration_tests/lib/servers.ts} (68%) rename src/legacy/ui/ui_settings/{__tests__/ui_settings_service.js => ui_settings_service.test.ts} (77%) diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 126cb8f8b94756..502c1f285e50da 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -77,6 +77,7 @@ declare module 'hapi' { name: string, factoryFn: (request: Request) => Record ) => void; + uiSettingsServiceFactory: (options: any) => any; } interface Request { @@ -118,6 +119,7 @@ export default class KbnServer { public close(): Promise; public afterPluginsInit(callback: () => void): void; public applyLoggingConfiguration(settings: any): void; + public config: KibanaConfig; } // Re-export commonly used hapi types. diff --git a/src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js b/src/legacy/ui/ui_settings/create_objects_client_stub.ts similarity index 59% rename from src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js rename to src/legacy/ui/ui_settings/create_objects_client_stub.ts index 9a6d44c313248f..ebbedb761fae9d 100644 --- a/src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js +++ b/src/legacy/ui/ui_settings/create_objects_client_stub.ts @@ -18,35 +18,23 @@ */ import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { SavedObjectsClient } from '../../../../../core/server'; +import { SavedObjectsClient } from '../../../../src/core/server'; export const savedObjectsClientErrors = SavedObjectsClient.errors; -export function createObjectsClientStub(type, id, esDocSource = {}) { +export interface SavedObjectsClientStub { + update: sinon.SinonStub; + get: sinon.SinonStub; + create: sinon.SinonStub; + errors: typeof savedObjectsClientErrors; +} + +export function createObjectsClientStub(esDocSource = {}): SavedObjectsClientStub { const savedObjectsClient = { update: sinon.stub(), get: sinon.stub().returns({ attributes: esDocSource }), create: sinon.stub(), - errors: savedObjectsClientErrors - }; - - savedObjectsClient.assertGetQuery = () => { - sinon.assert.calledOnce(savedObjectsClient.get); - - const { args } = savedObjectsClient.get.getCall(0); - expect(args[0]).to.be(type); - expect(args[1]).to.eql(id); + errors: savedObjectsClientErrors, }; - - savedObjectsClient.assertUpdateQuery = (expectedChanges) => { - sinon.assert.calledOnce(savedObjectsClient.update); - - const { args } = savedObjectsClient.update.getCall(0); - expect(args[0]).to.be(type); - expect(args[1]).to.eql(id); - expect(args[2]).to.eql(expectedChanges); - }; - return savedObjectsClient; } diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts similarity index 82% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index d047fd8bb25762..9b9a2fad39aca8 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_saved_config.js +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -21,12 +21,14 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import Chance from 'chance'; -import * as getUpgradeableConfigNS from '../get_upgradeable_config'; -import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; +// @ts-ignore +import * as getUpgradeableConfigNS from './get_upgradeable_config'; +// @ts-ignore +import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; const chance = new Chance(); -describe('uiSettings/createOrUpgradeSavedConfig', function () { +describe('uiSettings/createOrUpgradeSavedConfig', function() { const sandbox = sinon.createSandbox(); afterEach(() => sandbox.restore()); @@ -42,7 +44,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { type, id: options.id, version: 'foo', - })) + })), }; async function run(options = {}) { @@ -51,7 +53,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { version, buildNum, logWithMetadata, - ...options + ...options, }); sinon.assert.calledOnce(getUpgradeableConfig); @@ -70,44 +72,45 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { }; } - describe('nothing is upgradeable', function () { + describe('nothing is upgradeable', function() { it('should create config with current version and buildNum', async () => { const { run, savedObjectsClient } = setup(); await run(); sinon.assert.calledOnce(savedObjectsClient.create); - sinon.assert.calledWithExactly(savedObjectsClient.create, 'config', { - buildNum, - }, { - id: version - }); + sinon.assert.calledWithExactly( + savedObjectsClient.create, + 'config', + { + buildNum, + }, + { + id: version, + } + ); }); }); describe('something is upgradeable', () => { it('should merge upgraded attributes with current build number in new config', async () => { - const { - run, - getUpgradeableConfig, - savedObjectsClient - } = setup(); + const { run, getUpgradeableConfig, savedObjectsClient } = setup(); const savedAttributes = { buildNum: buildNum - 100, [chance.word()]: chance.sentence(), [chance.word()]: chance.sentence(), - [chance.word()]: chance.sentence() + [chance.word()]: chance.sentence(), }; - getUpgradeableConfig - .returns({ id: prevVersion, attributes: savedAttributes }); + getUpgradeableConfig.returns({ id: prevVersion, attributes: savedAttributes }); await run(); sinon.assert.calledOnce(getUpgradeableConfig); sinon.assert.calledOnce(savedObjectsClient.create); - sinon.assert.calledWithExactly(savedObjectsClient.create, + sinon.assert.calledWithExactly( + savedObjectsClient.create, 'config', { ...savedAttributes, @@ -122,12 +125,12 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { it('should log a message for upgrades', async () => { const { getUpgradeableConfig, logWithMetadata, run } = setup(); - getUpgradeableConfig - .returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); + getUpgradeableConfig.returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); await run(); sinon.assert.calledOnce(logWithMetadata); - sinon.assert.calledWithExactly(logWithMetadata, + sinon.assert.calledWithExactly( + logWithMetadata, ['plugin', 'elasticsearch'], sinon.match('Upgrade'), sinon.match({ @@ -140,8 +143,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { it('does not log when upgrade fails', async () => { const { getUpgradeableConfig, logWithMetadata, run, savedObjectsClient } = setup(); - getUpgradeableConfig - .returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); + getUpgradeableConfig.returns({ id: prevVersion, attributes: { buildNum: buildNum - 100 } }); savedObjectsClient.create.callsFake(async () => { throw new Error('foo'); @@ -171,7 +173,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { await run({ onWriteError }); sinon.assert.calledOnce(onWriteError); sinon.assert.calledWithExactly(onWriteError, error, { - buildNum + buildNum, }); }); @@ -195,9 +197,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { try { await run({ - onWriteError: (error) => ( - Promise.reject(new Error(`${error.message} bar`)) - ) + onWriteError: (error: Error) => Promise.reject(new Error(`${error.message} bar`)), }); throw new Error('expected run() to reject'); } catch (error) { @@ -214,9 +214,9 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { try { await run({ - onWriteError: (error) => { + onWriteError: (error: Error) => { throw new Error(`${error.message} bar`); - } + }, }); throw new Error('expected run() to reject'); } catch (error) { @@ -233,7 +233,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function () { try { await run({ - onWriteError: undefined + onWriteError: undefined, }); throw new Error('expected run() to reject'); } catch (error) { diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts similarity index 88% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index 307c7f5f8869a0..7d5f4e970638db 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/create_or_upgrade_integration.js +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -19,20 +19,27 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { SavedObjectsClientContract } from 'src/core/server'; +import KbnServer from '../../../../server/kbn_server'; import { createTestServers } from '../../../../../test_utils/kbn_server'; +// @ts-ignore import { createOrUpgradeSavedConfig } from '../create_or_upgrade_saved_config'; describe('createOrUpgradeSavedConfig()', () => { - let savedObjectsClient; - let kbn; - let kbnServer; - let esServer; - let servers; + let savedObjectsClient: SavedObjectsClientContract; + let kbnServer: KbnServer; + let servers: ReturnType; + let esServer: UnwrapPromise>; + let kbn: UnwrapPromise>; - before(async function () { + beforeAll(async function() { servers = createTestServers({ - adjustTimeout: (t) => this.timeout(t), + adjustTimeout: t => { + jest.setTimeout(t); + }, + settings: {}, }); esServer = await servers.startES(); kbn = await servers.startKibana(); @@ -69,14 +76,13 @@ describe('createOrUpgradeSavedConfig()', () => { ]); }); - after(() => { - esServer.stop(); - kbn.stop(); - }); - - it('upgrades the previous version on each increment', async function () { - this.timeout(30000); + afterAll(async () => { + await esServer.stop(); + await kbn.stop(); + }, 30000); + it('upgrades the previous version on each increment', async function() { + jest.setTimeout(30000); // ------------------------------------ // upgrade to 5.4.0 await createOrUpgradeSavedConfig({ diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts similarity index 87% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js rename to src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts index 734d579d234046..91231da968227a 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/__tests__/is_config_version_upgradeable.js +++ b/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts @@ -19,11 +19,13 @@ import expect from '@kbn/expect'; -import { isConfigVersionUpgradeable } from '../is_config_version_upgradeable'; -import { pkg } from '../../../../utils'; +// @ts-ignore +import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; +// @ts-ignore +import { pkg } from '../../../utils'; -describe('savedObjects/health_check/isConfigVersionUpgradeable', function () { - function isUpgradeableTest(savedVersion, kibanaVersion, expected) { +describe('savedObjects/health_check/isConfigVersionUpgradeable', function() { + function isUpgradeableTest(savedVersion: string, kibanaVersion: string, expected: boolean) { it(`should return ${expected} for config version ${savedVersion} and kibana version ${kibanaVersion}`, () => { expect(isConfigVersionUpgradeable(savedVersion, kibanaVersion)).to.be(expected); }); @@ -47,6 +49,6 @@ describe('savedObjects/health_check/isConfigVersionUpgradeable', function () { isUpgradeableTest('4.1.0-rc1-SNAPSHOT', '4.1.0-rc1', false); isUpgradeableTest('5.0.0-alpha11', '5.0.0', false); isUpgradeableTest('50.0.10-rc150-SNAPSHOT', '50.0.9', false); - isUpgradeableTest(undefined, pkg.version, false); + isUpgradeableTest(undefined as any, pkg.version, false); isUpgradeableTest('@@version', pkg.version, false); }); diff --git a/src/legacy/ui/ui_settings/__tests__/ui_settings_mixin_integration.js b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts similarity index 58% rename from src/legacy/ui/ui_settings/__tests__/ui_settings_mixin_integration.js rename to src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts index 9e8413a0f99e0b..f522f119a26ccb 100644 --- a/src/legacy/ui/ui_settings/__tests__/ui_settings_mixin_integration.js +++ b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts @@ -20,17 +20,21 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; +// @ts-ignore import { Config } from '../../../server/config'; -/* eslint-disable import/no-duplicates */ +// @ts-ignore import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; -import { uiSettingsServiceFactory } from '../ui_settings_service_factory'; +// @ts-ignore import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request'; -import { getUiSettingsServiceForRequest } from '../ui_settings_service_for_request'; -/* eslint-enable import/no-duplicates */ - +// @ts-ignore import { uiSettingsMixin } from '../ui_settings_mixin'; +interface Decorators { + server: { [name: string]: any }; + request: { [name: string]: any }; +} + describe('uiSettingsMixin()', () => { const sandbox = sinon.createSandbox(); @@ -38,15 +42,15 @@ describe('uiSettingsMixin()', () => { const config = Config.withDefaultSchema({ uiSettings: { overrides: { - foo: 'bar' - } - } + foo: 'bar', + }, + }, }); // maps of decorations passed to `server.decorate()` - const decorations = { + const decorations: Decorators = { server: {}, - request: {} + request: {}, }; // mock hapi server @@ -54,12 +58,12 @@ describe('uiSettingsMixin()', () => { log: sinon.stub(), route: sinon.stub(), config: () => config, - addMemoizedFactoryToRequest(name, factory) { - this.decorate('request', name, function () { + addMemoizedFactoryToRequest(name: string, factory: (...args: any[]) => any) { + this.decorate('request', name, function(this: typeof server) { return factory(this); }); }, - decorate: sinon.spy((type, name, value) => { + decorate: sinon.spy((type: keyof Decorators, name: string, value: any) => { decorations[type][name] = value; }), }; @@ -91,28 +95,38 @@ describe('uiSettingsMixin()', () => { describe('server.uiSettingsServiceFactory()', () => { it('decorates server with "uiSettingsServiceFactory"', () => { const { decorations } = setup(); - expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function'); - - sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory'); - sinon.assert.notCalled(uiSettingsServiceFactory); + expect(decorations.server) + .to.have.property('uiSettingsServiceFactory') + .a('function'); + + const uiSettingsServiceFactoryStub = sandbox.stub( + uiSettingsServiceFactoryNS, + 'uiSettingsServiceFactory' + ); + sinon.assert.notCalled(uiSettingsServiceFactoryStub); decorations.server.uiSettingsServiceFactory(); - sinon.assert.calledOnce(uiSettingsServiceFactory); + sinon.assert.calledOnce(uiSettingsServiceFactoryStub); }); it('passes `server` and `options` argument to factory', () => { const { decorations, server } = setup(); - expect(decorations.server).to.have.property('uiSettingsServiceFactory').a('function'); - - sandbox.stub(uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory'); - sinon.assert.notCalled(uiSettingsServiceFactory); + expect(decorations.server) + .to.have.property('uiSettingsServiceFactory') + .a('function'); + + const uiSettingsServiceFactoryStub = sandbox.stub( + uiSettingsServiceFactoryNS, + 'uiSettingsServiceFactory' + ); + sinon.assert.notCalled(uiSettingsServiceFactoryStub); decorations.server.uiSettingsServiceFactory({ - foo: 'bar' + foo: 'bar', }); - sinon.assert.calledOnce(uiSettingsServiceFactory); - sinon.assert.calledWithExactly(uiSettingsServiceFactory, server, { + sinon.assert.calledOnce(uiSettingsServiceFactoryStub); + sinon.assert.calledWithExactly(uiSettingsServiceFactoryStub, server, { foo: 'bar', overrides: { - foo: 'bar' + foo: 'bar', }, getDefaults: sinon.match.func, }); @@ -122,33 +136,45 @@ describe('uiSettingsMixin()', () => { describe('request.getUiSettingsService()', () => { it('exposes "getUiSettingsService" on requests', () => { const { decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest'); - sinon.assert.notCalled(getUiSettingsServiceForRequest); + expect(decorations.request) + .to.have.property('getUiSettingsService') + .a('function'); + + const getUiSettingsServiceForRequestStub = sandbox.stub( + getUiSettingsServiceForRequestNS, + 'getUiSettingsServiceForRequest' + ); + sinon.assert.notCalled(getUiSettingsServiceForRequestStub); decorations.request.getUiSettingsService(); - sinon.assert.calledOnce(getUiSettingsServiceForRequest); + sinon.assert.calledOnce(getUiSettingsServiceForRequestStub); }); it('passes request to getUiSettingsServiceForRequest', () => { const { server, decorations } = setup(); - expect(decorations.request).to.have.property('getUiSettingsService').a('function'); - - sandbox.stub(getUiSettingsServiceForRequestNS, 'getUiSettingsServiceForRequest'); - sinon.assert.notCalled(getUiSettingsServiceForRequest); + expect(decorations.request) + .to.have.property('getUiSettingsService') + .a('function'); + + const getUiSettingsServiceForRequestStub = sandbox.stub( + getUiSettingsServiceForRequestNS, + 'getUiSettingsServiceForRequest' + ); + sinon.assert.notCalled(getUiSettingsServiceForRequestStub); const request = {}; decorations.request.getUiSettingsService.call(request); - sinon.assert.calledWith(getUiSettingsServiceForRequest, server, request); + sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server, request); }); }); describe('server.uiSettings()', () => { it('throws an error, links to pr', () => { const { decorations } = setup(); - expect(decorations.server).to.have.property('uiSettings').a('function'); + expect(decorations.server) + .to.have.property('uiSettings') + .a('function'); expect(() => { decorations.server.uiSettings(); - }).to.throwError('http://github.com'); + }).to.throwError('http://github.com' as any); // incorrect typings }); }); }); diff --git a/src/legacy/ui/ui_settings/routes/__tests__/lib/index.js b/src/legacy/ui/ui_settings/routes/__tests__/lib/index.js deleted file mode 100644 index ecc20a09149d30..00000000000000 --- a/src/legacy/ui/ui_settings/routes/__tests__/lib/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { - startServers, - getServices, - stopServers, -} from './servers'; - -export { - chance -} from './chance'; - -export { - assertSinonMatch, -} from './assert'; diff --git a/src/legacy/ui/ui_settings/routes/__tests__/doc_exists.js b/src/legacy/ui/ui_settings/routes/integration_tests/doc_exists.ts similarity index 82% rename from src/legacy/ui/ui_settings/routes/__tests__/doc_exists.js rename to src/legacy/ui/ui_settings/routes/integration_tests/doc_exists.ts index 824194ae1788f7..7783fd99769634 100644 --- a/src/legacy/ui/ui_settings/routes/__tests__/doc_exists.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/doc_exists.ts @@ -20,17 +20,11 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { - getServices, - chance, - assertSinonMatch, -} from './lib'; +import { getServices, chance, assertSinonMatch } from './lib'; export function docExistsSuite() { - async function setup(options = {}) { - const { - initialSettings - } = options; + async function setup(options: any = {}) { + const { initialSettings } = options; const { kbnServer, uiSettings, callCluster } = getServices(); @@ -39,7 +33,7 @@ export function docExistsSuite() { index: kbnServer.config.get('kibana.index'), body: { conflicts: 'proceed', - query: { match_all: {} } + query: { match_all: {} }, }, }); @@ -55,29 +49,29 @@ export function docExistsSuite() { const defaultIndex = chance.word({ length: 10 }); const { kbnServer } = await setup({ initialSettings: { - defaultIndex - } + defaultIndex, + }, }); const { statusCode, result } = await kbnServer.inject({ method: 'GET', - url: '/api/kibana/settings' + url: '/api/kibana/settings', }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, defaultIndex: { - userValue: defaultIndex + userValue: defaultIndex, }, foo: { userValue: 'bar', - isOverridden: true + isOverridden: true, }, - } + }, }); }); }); @@ -91,24 +85,24 @@ export function docExistsSuite() { method: 'POST', url: '/api/kibana/settings/defaultIndex', payload: { - value: defaultIndex - } + value: defaultIndex, + }, }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, defaultIndex: { - userValue: defaultIndex + userValue: defaultIndex, }, foo: { userValue: 'bar', - isOverridden: true + isOverridden: true, }, - } + }, }); }); @@ -119,15 +113,15 @@ export function docExistsSuite() { method: 'POST', url: '/api/kibana/settings/foo', payload: { - value: 'baz' - } + value: 'baz', + }, }); expect(statusCode).to.be(400); assertSinonMatch(result, { error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', - statusCode: 400 + statusCode: 400, }); }); }); @@ -142,25 +136,25 @@ export function docExistsSuite() { url: '/api/kibana/settings', payload: { changes: { - defaultIndex - } - } + defaultIndex, + }, + }, }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, defaultIndex: { - userValue: defaultIndex + userValue: defaultIndex, }, foo: { userValue: 'bar', - isOverridden: true + isOverridden: true, }, - } + }, }); }); @@ -172,16 +166,16 @@ export function docExistsSuite() { url: '/api/kibana/settings', payload: { changes: { - foo: 'baz' - } - } + foo: 'baz', + }, + }, }); expect(statusCode).to.be(400); assertSinonMatch(result, { error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', - statusCode: 400 + statusCode: 400, }); }); }); @@ -191,27 +185,27 @@ export function docExistsSuite() { const defaultIndex = chance.word({ length: 10 }); const { kbnServer, uiSettings } = await setup({ - initialSettings: { defaultIndex } + initialSettings: { defaultIndex }, }); expect(await uiSettings.get('defaultIndex')).to.be(defaultIndex); const { statusCode, result } = await kbnServer.inject({ method: 'DELETE', - url: '/api/kibana/settings/defaultIndex' + url: '/api/kibana/settings/defaultIndex', }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, foo: { userValue: 'bar', - isOverridden: true + isOverridden: true, }, - } + }, }); }); it('returns a 400 if deleting overridden value', async () => { @@ -219,14 +213,14 @@ export function docExistsSuite() { const { statusCode, result } = await kbnServer.inject({ method: 'DELETE', - url: '/api/kibana/settings/foo' + url: '/api/kibana/settings/foo', }); expect(statusCode).to.be(400); assertSinonMatch(result, { error: 'Bad Request', message: 'Unable to update "foo" because it is overridden', - statusCode: 400 + statusCode: 400, }); }); }); diff --git a/src/legacy/ui/ui_settings/routes/__tests__/doc_missing.js b/src/legacy/ui/ui_settings/routes/integration_tests/doc_missing.ts similarity index 82% rename from src/legacy/ui/ui_settings/routes/__tests__/doc_missing.js rename to src/legacy/ui/ui_settings/routes/integration_tests/doc_missing.ts index a1fbec8ad39dfa..580fe04b920878 100644 --- a/src/legacy/ui/ui_settings/routes/__tests__/doc_missing.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/doc_missing.ts @@ -20,11 +20,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { - getServices, - chance, - assertSinonMatch, -} from './lib'; +import { getServices, chance, assertSinonMatch } from './lib'; export function docMissingSuite() { // ensure the kibana index has no documents @@ -35,15 +31,15 @@ export function docMissingSuite() { await kbnServer.inject({ method: 'POST', url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' } + payload: { value: 'abc' }, }); // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { index: kbnServer.config.get('kibana.index'), body: { - query: { match_all: {} } - } + query: { match_all: {} }, + }, }); }); @@ -53,7 +49,7 @@ export function docMissingSuite() { const { statusCode, result } = await kbnServer.inject({ method: 'GET', - url: '/api/kibana/settings' + url: '/api/kibana/settings', }); expect(statusCode).to.be(200); @@ -64,9 +60,9 @@ export function docMissingSuite() { }, foo: { userValue: 'bar', - isOverridden: true - } - } + isOverridden: true, + }, + }, }); }); }); @@ -79,23 +75,23 @@ export function docMissingSuite() { const { statusCode, result } = await kbnServer.inject({ method: 'POST', url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex } + payload: { value: defaultIndex }, }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, defaultIndex: { - userValue: defaultIndex + userValue: defaultIndex, }, foo: { userValue: 'bar', - isOverridden: true - } - } + isOverridden: true, + }, + }, }); }); }); @@ -109,24 +105,24 @@ export function docMissingSuite() { method: 'POST', url: '/api/kibana/settings', payload: { - changes: { defaultIndex } - } + changes: { defaultIndex }, + }, }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, defaultIndex: { - userValue: defaultIndex + userValue: defaultIndex, }, foo: { userValue: 'bar', - isOverridden: true - } - } + isOverridden: true, + }, + }, }); }); }); @@ -137,20 +133,20 @@ export function docMissingSuite() { const { statusCode, result } = await kbnServer.inject({ method: 'DELETE', - url: '/api/kibana/settings/defaultIndex' + url: '/api/kibana/settings/defaultIndex', }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, foo: { userValue: 'bar', - isOverridden: true - } - } + isOverridden: true, + }, + }, }); }); }); diff --git a/src/legacy/ui/ui_settings/routes/__tests__/doc_missing_and_index_read_only.js b/src/legacy/ui/ui_settings/routes/integration_tests/doc_missing_and_index_read_only.ts similarity index 85% rename from src/legacy/ui/ui_settings/routes/__tests__/doc_missing_and_index_read_only.js rename to src/legacy/ui/ui_settings/routes/integration_tests/doc_missing_and_index_read_only.ts index 4ef004843054c0..1a17970081d9cc 100644 --- a/src/legacy/ui/ui_settings/routes/__tests__/doc_missing_and_index_read_only.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/doc_missing_and_index_read_only.ts @@ -20,11 +20,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { - getServices, - chance, - assertSinonMatch, -} from './lib'; +import { getServices, chance, assertSinonMatch } from './lib'; export function docMissingAndIndexReadOnlySuite() { // ensure the kibana index has no documents @@ -35,15 +31,15 @@ export function docMissingAndIndexReadOnlySuite() { await kbnServer.inject({ method: 'POST', url: '/api/kibana/settings/defaultIndex', - payload: { value: 'abc' } + payload: { value: 'abc' }, }); // delete all docs from kibana index to ensure savedConfig is not found await callCluster('deleteByQuery', { index: kbnServer.config.get('kibana.index'), body: { - query: { match_all: {} } - } + query: { match_all: {} }, + }, }); // set the index to read only @@ -52,10 +48,10 @@ export function docMissingAndIndexReadOnlySuite() { body: { index: { blocks: { - read_only: true - } - } - } + read_only: true, + }, + }, + }, }); }); @@ -68,10 +64,10 @@ export function docMissingAndIndexReadOnlySuite() { body: { index: { blocks: { - read_only: false - } - } - } + read_only: false, + }, + }, + }, }); }); @@ -81,20 +77,20 @@ export function docMissingAndIndexReadOnlySuite() { const { statusCode, result } = await kbnServer.inject({ method: 'GET', - url: '/api/kibana/settings' + url: '/api/kibana/settings', }); expect(statusCode).to.be(200); assertSinonMatch(result, { settings: { buildNum: { - userValue: sinon.match.number + userValue: sinon.match.number, }, foo: { userValue: 'bar', - isOverridden: true - } - } + isOverridden: true, + }, + }, }); }); }); @@ -107,14 +103,14 @@ export function docMissingAndIndexReadOnlySuite() { const { statusCode, result } = await kbnServer.inject({ method: 'POST', url: '/api/kibana/settings/defaultIndex', - payload: { value: defaultIndex } + payload: { value: defaultIndex }, }); expect(statusCode).to.be(403); assertSinonMatch(result, { error: 'Forbidden', message: sinon.match('index read-only'), - statusCode: 403 + statusCode: 403, }); }); }); @@ -128,15 +124,15 @@ export function docMissingAndIndexReadOnlySuite() { method: 'POST', url: '/api/kibana/settings', payload: { - changes: { defaultIndex } - } + changes: { defaultIndex }, + }, }); expect(statusCode).to.be(403); assertSinonMatch(result, { error: 'Forbidden', message: sinon.match('index read-only'), - statusCode: 403 + statusCode: 403, }); }); }); @@ -147,14 +143,14 @@ export function docMissingAndIndexReadOnlySuite() { const { statusCode, result } = await kbnServer.inject({ method: 'DELETE', - url: '/api/kibana/settings/defaultIndex' + url: '/api/kibana/settings/defaultIndex', }); expect(statusCode).to.be(403); assertSinonMatch(result, { error: 'Forbidden', message: sinon.match('index read-only'), - statusCode: 403 + statusCode: 403, }); }); }); diff --git a/src/legacy/ui/ui_settings/routes/__tests__/index.js b/src/legacy/ui/ui_settings/routes/integration_tests/index.test.ts similarity index 89% rename from src/legacy/ui/ui_settings/routes/__tests__/index.js rename to src/legacy/ui/ui_settings/routes/integration_tests/index.test.ts index bf4db3602eee34..db271761b39ea6 100644 --- a/src/legacy/ui/ui_settings/routes/__tests__/index.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/index.test.ts @@ -17,16 +17,13 @@ * under the License. */ -import { - startServers, - stopServers, -} from './lib'; +import { startServers, stopServers } from './lib'; import { docExistsSuite } from './doc_exists'; import { docMissingSuite } from './doc_missing'; import { docMissingAndIndexReadOnlySuite } from './doc_missing_and_index_read_only'; -describe('uiSettings/routes', function () { +describe('uiSettings/routes', function() { /** * The "doc missing" and "index missing" tests verify how the uiSettings * API behaves in between healthChecks, so they interact with the healthCheck @@ -43,12 +40,13 @@ describe('uiSettings/routes', function () { * stupidly fragile and timing sensitive. #14163 should fix that, but until then * this is the most stable way I've been able to get this to work. */ - this.slow(2000); - this.timeout(10000); + jest.setTimeout(10000); - before(startServers); + beforeAll(startServers); + /* eslint-disable jest/valid-describe */ describe('doc missing', docMissingSuite); describe('doc missing and index readonly', docMissingAndIndexReadOnlySuite); describe('doc exists', docExistsSuite); - after(stopServers); + /* eslint-enable jest/valid-describe */ + afterAll(stopServers); }); diff --git a/src/legacy/ui/ui_settings/routes/__tests__/lib/assert.js b/src/legacy/ui/ui_settings/routes/integration_tests/lib/assert.ts similarity index 93% rename from src/legacy/ui/ui_settings/routes/__tests__/lib/assert.js rename to src/legacy/ui/ui_settings/routes/integration_tests/lib/assert.ts index c7b8e057092643..62533b7ae734da 100644 --- a/src/legacy/ui/ui_settings/routes/__tests__/lib/assert.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/lib/assert.ts @@ -19,7 +19,7 @@ import sinon from 'sinon'; -export function assertSinonMatch(value, match) { +export function assertSinonMatch(value: any, match: any) { const stub = sinon.stub(); stub(value); sinon.assert.calledWithExactly(stub, match); diff --git a/src/legacy/ui/ui_settings/routes/__tests__/lib/chance.js b/src/legacy/ui/ui_settings/routes/integration_tests/lib/chance.ts similarity index 100% rename from src/legacy/ui/ui_settings/routes/__tests__/lib/chance.js rename to src/legacy/ui/ui_settings/routes/integration_tests/lib/chance.ts diff --git a/src/legacy/ui/ui_settings/__tests__/lib/index.js b/src/legacy/ui/ui_settings/routes/integration_tests/lib/index.ts similarity index 84% rename from src/legacy/ui/ui_settings/__tests__/lib/index.js rename to src/legacy/ui/ui_settings/routes/integration_tests/lib/index.ts index 29b1adbcba5760..33a1cbd4d780b4 100644 --- a/src/legacy/ui/ui_settings/__tests__/lib/index.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/lib/index.ts @@ -17,7 +17,8 @@ * under the License. */ -export { - createObjectsClientStub, - savedObjectsClientErrors, -} from './create_objects_client_stub'; +export { startServers, getServices, stopServers } from './servers'; + +export { chance } from './chance'; + +export { assertSinonMatch } from './assert'; diff --git a/src/legacy/ui/ui_settings/routes/__tests__/lib/servers.js b/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts similarity index 68% rename from src/legacy/ui/ui_settings/routes/__tests__/lib/servers.js rename to src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts index 5db6b01750a70f..5b0fbf5a5f2565 100644 --- a/src/legacy/ui/ui_settings/routes/__tests__/lib/servers.js +++ b/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts @@ -17,23 +17,37 @@ * under the License. */ +import { UnwrapPromise } from '@kbn/utility-types'; +import { SavedObjectsClientContract } from 'src/core/server'; + +import KbnServer from '../../../../../server/kbn_server'; import { createTestServers } from '../../../../../../test_utils/kbn_server'; +import { CallCluster } from '../../../../../../legacy/core_plugins/elasticsearch'; + +let kbnServer: KbnServer; +let servers: ReturnType; +let esServer: UnwrapPromise>; +let kbn: UnwrapPromise>; + +interface AllServices { + kbnServer: KbnServer; + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallCluster; + uiSettings: any; + deleteKibanaIndex: typeof deleteKibanaIndex; +} -let kbnServer; -let services; -let servers; -let esServer; -let kbn; +let services: AllServices; export async function startServers() { servers = createTestServers({ - adjustTimeout: (t) => this.timeout(t), + adjustTimeout: t => jest.setTimeout(t), settings: { kbn: { uiSettings: { overrides: { foo: 'bar', - } + }, }, }, }, @@ -43,9 +57,9 @@ export async function startServers() { kbnServer = kbn.kbnServer; } -async function deleteKibanaIndex(callCluster) { +async function deleteKibanaIndex(callCluster: CallCluster) { const kibanaIndices = await callCluster('cat.indices', { index: '.kibana*', format: 'json' }); - const indexNames = kibanaIndices.map(x => x.index); + const indexNames = kibanaIndices.map((x: any) => x.index); if (!indexNames.length) { return; } @@ -65,7 +79,7 @@ export function getServices() { const callCluster = esServer.es.getCallCluster(); const savedObjects = kbnServer.server.savedObjects; - const savedObjectsClient = savedObjects.getScopedSavedObjectsClient(); + const savedObjectsClient = savedObjects.getScopedSavedObjectsClient({}); const uiSettings = kbnServer.server.uiSettingsServiceFactory({ savedObjectsClient, @@ -83,10 +97,10 @@ export function getServices() { } export async function stopServers() { - services = null; - kbnServer = null; + services = null!; + kbnServer = null!; if (servers) { - esServer.stop(); - kbn.stop(); + await esServer.stop(); + await kbn.stop(); } } diff --git a/src/legacy/ui/ui_settings/__tests__/ui_settings_service.js b/src/legacy/ui/ui_settings/ui_settings_service.test.ts similarity index 77% rename from src/legacy/ui/ui_settings/__tests__/ui_settings_service.js rename to src/legacy/ui/ui_settings/ui_settings_service.test.ts index 3106ba429f7cea..bb407d7b3a91b2 100644 --- a/src/legacy/ui/ui_settings/__tests__/ui_settings_service.js +++ b/src/legacy/ui/ui_settings/ui_settings_service.test.ts @@ -18,33 +18,34 @@ */ import expect from '@kbn/expect'; -import { errors as esErrors } from 'elasticsearch'; import Chance from 'chance'; import sinon from 'sinon'; -import { UiSettingsService } from '../ui_settings_service'; -import * as createOrUpgradeSavedConfigNS from '../create_or_upgrade_saved_config/create_or_upgrade_saved_config'; -import { - createObjectsClientStub, - savedObjectsClientErrors, -} from './lib'; +// @ts-ignore +import { UiSettingsService } from './ui_settings_service'; +// @ts-ignore +import * as createOrUpgradeSavedConfigNS from './create_or_upgrade_saved_config/create_or_upgrade_saved_config'; +import { createObjectsClientStub, savedObjectsClientErrors } from './create_objects_client_stub'; const TYPE = 'config'; const ID = 'kibana-version'; const BUILD_NUM = 1234; const chance = new Chance(); +interface SetupOptions { + getDefaults?: () => Record; + defaults?: Record; + esDocSource?: Record; + overrides?: Record; +} + describe('ui settings', () => { const sandbox = sinon.createSandbox(); - function setup(options = {}) { - const { - getDefaults, - defaults = {}, - overrides, - esDocSource = {}, - savedObjectsClient = createObjectsClientStub(TYPE, ID, esDocSource) - } = options; + function setup(options: SetupOptions = {}) { + const { getDefaults, defaults = {}, overrides, esDocSource = {} } = options; + + const savedObjectsClient = createObjectsClientStub(esDocSource); const uiSettings = new UiSettingsService({ type: TYPE, @@ -55,14 +56,34 @@ describe('ui settings', () => { overrides, }); - const createOrUpgradeSavedConfig = sandbox.stub(createOrUpgradeSavedConfigNS, 'createOrUpgradeSavedConfig'); + const createOrUpgradeSavedConfig = sandbox.stub( + createOrUpgradeSavedConfigNS, + 'createOrUpgradeSavedConfig' + ); + + function assertGetQuery() { + sinon.assert.calledOnce(savedObjectsClient.get); + + const { args } = savedObjectsClient.get.getCall(0); + expect(args[0]).to.be(TYPE); + expect(args[1]).to.eql(ID); + } + + function assertUpdateQuery(expectedChanges: unknown) { + sinon.assert.calledOnce(savedObjectsClient.update); + + const { args } = savedObjectsClient.update.getCall(0); + expect(args[0]).to.be(TYPE); + expect(args[1]).to.eql(ID); + expect(args[2]).to.eql(expectedChanges); + } return { uiSettings, savedObjectsClient, createOrUpgradeSavedConfig, - assertGetQuery: savedObjectsClient.assertGetQuery, - assertUpdateQuery: savedObjectsClient.assertUpdateQuery, + assertGetQuery, + assertUpdateQuery, }; } @@ -75,15 +96,15 @@ describe('ui settings', () => { }); it('updates a single value in one operation', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertUpdateQuery } = setup(); await uiSettings.setMany({ one: 'value' }); - savedObjectsClient.assertUpdateQuery({ one: 'value' }); + assertUpdateQuery({ one: 'value' }); }); it('updates several values in one operation', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertUpdateQuery } = setup(); await uiSettings.setMany({ one: 'value', another: 'val' }); - savedObjectsClient.assertUpdateQuery({ one: 'value', another: 'val' }); + assertUpdateQuery({ one: 'value', another: 'val' }); }); it('automatically creates the savedConfig if it is missing', async () => { @@ -117,14 +138,14 @@ describe('ui settings', () => { it('throws an error if any key is overridden', async () => { const { uiSettings } = setup({ overrides: { - foo: 'bar' - } + foo: 'bar', + }, }); try { await uiSettings.setMany({ bar: 'box', - foo: 'baz' + foo: 'baz', }); } catch (error) { expect(error.message).to.be('Unable to update "foo" because it is overridden'); @@ -139,16 +160,16 @@ describe('ui settings', () => { }); it('updates single values by (key, value)', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertUpdateQuery } = setup(); await uiSettings.set('one', 'value'); - savedObjectsClient.assertUpdateQuery({ one: 'value' }); + assertUpdateQuery({ one: 'value' }); }); it('throws an error if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { - foo: 'bar' - } + foo: 'bar', + }, }); try { @@ -166,16 +187,16 @@ describe('ui settings', () => { }); it('removes single values by key', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertUpdateQuery } = setup(); await uiSettings.remove('one'); - savedObjectsClient.assertUpdateQuery({ one: null }); + assertUpdateQuery({ one: null }); }); it('throws an error if the key is overridden', async () => { const { uiSettings } = setup({ overrides: { - foo: 'bar' - } + foo: 'bar', + }, }); try { @@ -193,22 +214,22 @@ describe('ui settings', () => { }); it('removes a single value', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertUpdateQuery } = setup(); await uiSettings.removeMany(['one']); - savedObjectsClient.assertUpdateQuery({ one: null }); + assertUpdateQuery({ one: null }); }); it('updates several values in one operation', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertUpdateQuery } = setup(); await uiSettings.removeMany(['one', 'two', 'three']); - savedObjectsClient.assertUpdateQuery({ one: null, two: null, three: null }); + assertUpdateQuery({ one: null, two: null, three: null }); }); it('throws an error if any key is overridden', async () => { const { uiSettings } = setup({ overrides: { - foo: 'bar' - } + foo: 'bar', + }, }); try { @@ -239,16 +260,16 @@ describe('ui settings', () => { const value = chance.word(); const { uiSettings } = setup({ defaults: { key: { value } } }); expect(await uiSettings.getDefaults()).to.eql({ - key: { value } + key: { value }, }); }); }); describe('#getUserProvided()', () => { it('pulls user configuration from ES', async () => { - const { uiSettings, savedObjectsClient } = setup(); + const { uiSettings, assertGetQuery } = setup(); await uiSettings.getUserProvided(); - savedObjectsClient.assertGetQuery(); + assertGetQuery(); }); it('returns user configuration', async () => { @@ -258,7 +279,7 @@ describe('ui settings', () => { expect(result).to.eql({ user: { userValue: 'customized', - } + }, }); }); @@ -268,89 +289,76 @@ describe('ui settings', () => { const result = await uiSettings.getUserProvided(); expect(result).to.eql({ user: { - userValue: 'customized' + userValue: 'customized', }, something: { - userValue: 'else' - } + userValue: 'else', + }, }); }); - it('returns an empty object on 404 responses', async () => { - const { uiSettings } = setup({ - async callCluster() { - throw new esErrors[404](); - } - }); + it.skip('returns an empty object on NotFound responses', async () => { + const { uiSettings, savedObjectsClient } = setup(); - expect(await uiSettings.getUserProvided()).to.eql({}); + const error = savedObjectsClientErrors.createGenericNotFoundError(); + savedObjectsClient.get.throws(error); + + expect(await uiSettings.getUserProvided({})).to.eql({}); }); - it('returns an empty object on 403 responses', async () => { - const { uiSettings } = setup({ - async callCluster() { - throw new esErrors[403](); - } - }); + it('returns an empty object on Forbidden responses', async () => { + const { uiSettings, savedObjectsClient } = setup(); + + const error = savedObjectsClientErrors.decorateForbiddenError(new Error()); + savedObjectsClient.get.throws(error); expect(await uiSettings.getUserProvided()).to.eql({}); }); - it('returns an empty object on NoConnections responses', async () => { - const { uiSettings } = setup({ - async callCluster() { - throw new esErrors.NoConnections(); - } - }); + it('returns an empty object on EsUnavailable responses', async () => { + const { uiSettings, savedObjectsClient } = setup(); + + const error = savedObjectsClientErrors.decorateEsUnavailableError(new Error()); + savedObjectsClient.get.throws(error); expect(await uiSettings.getUserProvided()).to.eql({}); }); - it('throws 401 errors', async () => { - const { uiSettings } = setup({ - savedObjectsClient: { - errors: savedObjectsClientErrors, - async get() { - throw new esErrors[401](); - } - } - }); + it('throws Unauthorized errors', async () => { + const { uiSettings, savedObjectsClient } = setup(); + + const error = savedObjectsClientErrors.decorateNotAuthorizedError(new Error()); + savedObjectsClient.get.throws(error); try { await uiSettings.getUserProvided(); throw new Error('expect getUserProvided() to throw'); } catch (err) { - expect(err).to.be.a(esErrors[401]); + expect(err).to.be(error); } }); - it('throw when callCluster fails in some unexpected way', async () => { - const expectedUnexpectedError = new Error('unexpected'); + it('throw when SavedObjectsClient throws in some unexpected way', async () => { + const { uiSettings, savedObjectsClient } = setup(); - const { uiSettings } = setup({ - savedObjectsClient: { - errors: savedObjectsClientErrors, - async get() { - throw expectedUnexpectedError; - } - } - }); + const error = new Error('unexpected'); + savedObjectsClient.get.throws(error); try { await uiSettings.getUserProvided(); throw new Error('expect getUserProvided() to throw'); } catch (err) { - expect(err).to.be(expectedUnexpectedError); + expect(err).to.be(error); } }); it('includes overridden values for overridden keys', async () => { const esDocSource = { - user: 'customized' + user: 'customized', }; const overrides = { - foo: 'bar' + foo: 'bar', }; const { uiSettings } = setup({ esDocSource, overrides }); @@ -369,9 +377,9 @@ describe('ui settings', () => { describe('#getRaw()', () => { it('pulls user configuration from ES', async () => { const esDocSource = {}; - const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + const { uiSettings, assertGetQuery } = setup({ esDocSource }); await uiSettings.getRaw(); - savedObjectsClient.assertGetQuery(); + assertGetQuery(); }); it(`without user configuration it's equal to the defaults`, async () => { @@ -420,17 +428,17 @@ describe('ui settings', () => { describe('#getAll()', () => { it('pulls user configuration from ES', async () => { const esDocSource = {}; - const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + const { uiSettings, assertGetQuery } = setup({ esDocSource }); await uiSettings.getAll(); - savedObjectsClient.assertGetQuery(); + assertGetQuery(); }); it(`returns defaults when es doc is empty`, async () => { - const esDocSource = { }; + const esDocSource = {}; const defaults = { foo: { value: 'bar' } }; const { uiSettings } = setup({ esDocSource, defaults }); expect(await uiSettings.getAll()).to.eql({ - foo: 'bar' + foo: 'bar', }); }); @@ -442,7 +450,7 @@ describe('ui settings', () => { const defaults = { foo: { - value: 'default' + value: 'default', }, }; @@ -461,12 +469,12 @@ describe('ui settings', () => { const defaults = { foo: { - value: 'default' + value: 'default', }, }; const overrides = { - foo: 'bax' + foo: 'bax', }; const { uiSettings } = setup({ esDocSource, defaults, overrides }); @@ -480,9 +488,9 @@ describe('ui settings', () => { describe('#get()', () => { it('pulls user configuration from ES', async () => { const esDocSource = {}; - const { uiSettings, savedObjectsClient } = setup({ esDocSource }); + const { uiSettings, assertGetQuery } = setup({ esDocSource }); await uiSettings.get(); - savedObjectsClient.assertGetQuery(); + assertGetQuery(); }); it(`returns the promised value for a key`, async () => { @@ -525,7 +533,9 @@ describe('ui settings', () => { it('returns the overridden value if the document does not exist', async () => { const overrides = { dateFormat: 'foo' }; const { uiSettings, savedObjectsClient } = setup({ overrides }); - savedObjectsClient.get.onFirstCall().throws(savedObjectsClientErrors.createGenericNotFoundError()); + savedObjectsClient.get + .onFirstCall() + .throws(savedObjectsClientErrors.createGenericNotFoundError()); expect(await uiSettings.get('dateFormat')).to.be('foo'); }); }); @@ -557,7 +567,10 @@ describe('ui settings', () => { it('throws 400 Boom error when keys is overridden', () => { const { uiSettings } = setup({ overrides: { foo: true } }); expect(() => uiSettings.assertUpdateAllowed('foo')).to.throwError(error => { - expect(error).to.have.property('message', 'Unable to update "foo" because it is overridden'); + expect(error).to.have.property( + 'message', + 'Unable to update "foo" because it is overridden' + ); expect(error).to.have.property('isBoom', true); expect(error.output).to.have.property('statusCode', 400); }); From 5266349fee17fe1ab5108fed691400e28fedc4b9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 30 Sep 2019 22:52:07 -0700 Subject: [PATCH 06/53] =?UTF-8?q?[dev-utils]=20implement=20basic=20KbnClie?= =?UTF-8?q?nt=20util=20for=20talking=20to=20Kiba=E2=80=A6=20(#46673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [dev-utils] implement basic KbnClient util for talking to Kibana server * update KbnClient to expose full KibanaServerService API * expose request() function and uriencode helper * [uiSettings] retry read on conflicts auto upgrading * expose function for resolving a Kibana server url * only use apis in test hooks * run x-pack-ciGroup2 60 times * log retries as errors so they are included in console output for job * bump * Revert "run x-pack-ciGroup2 60 times" This reverts commit 6b6f392edfe10009a4c623ecc2e02bd09cf0e699. * refactor urlencode tag to be a little clearer * support customizing maxAttempts in request method --- packages/kbn-dev-utils/src/index.ts | 1 + .../kbn-dev-utils/src/kbn_client/errors.ts | 42 ++++++ .../kbn-dev-utils/src/kbn_client/index.ts | 21 +++ .../src/kbn_client/kbn_client.ts | 64 +++++++++ .../src/kbn_client/kbn_client_plugins.ts | 33 +++-- .../src/kbn_client/kbn_client_requester.ts | 124 ++++++++++++++++++ .../kbn_client/kbn_client_saved_objects.ts | 65 ++++----- .../src/kbn_client/kbn_client_status.ts | 68 ++++++++++ .../src/kbn_client/kbn_client_ui_settings.ts | 113 ++++++++++++++++ .../src/kbn_client/kbn_client_version.ts | 26 ++-- src/es_archiver/actions/empty_kibana_index.js | 5 +- src/es_archiver/actions/load.js | 5 +- src/es_archiver/actions/unload.js | 5 +- src/es_archiver/es_archiver.js | 10 +- src/es_archiver/lib/index.js | 4 - src/es_archiver/lib/kibana_plugins.ts | 55 -------- .../ui/ui_settings/ui_settings_service.js | 12 +- .../services/kibana_server/kibana_server.ts | 32 ++--- .../services/kibana_server/ui_settings.js | 84 ------------ .../actions/builtin_action_types/pagerduty.ts | 9 +- .../actions/builtin_action_types/slack.ts | 9 +- .../actions/builtin_action_types/webhook.ts | 8 +- 22 files changed, 528 insertions(+), 267 deletions(-) create mode 100644 packages/kbn-dev-utils/src/kbn_client/errors.ts create mode 100644 packages/kbn-dev-utils/src/kbn_client/index.ts create mode 100644 packages/kbn-dev-utils/src/kbn_client/kbn_client.ts rename test/common/services/kibana_server/version.js => packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts (57%) create mode 100644 packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts rename test/common/services/kibana_server/saved_objects.ts => packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts (64%) create mode 100644 packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts create mode 100644 packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts rename test/common/services/kibana_server/status.js => packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts (62%) delete mode 100644 src/es_archiver/lib/kibana_plugins.ts delete mode 100644 test/common/services/kibana_server/ui_settings.js diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 6d3914eb56218b..5c69036a4b13ae 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -23,3 +23,4 @@ export { createAbsolutePathSerializer } from './serializers'; export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './constants'; +export { KbnClient } from './kbn_client'; diff --git a/packages/kbn-dev-utils/src/kbn_client/errors.ts b/packages/kbn-dev-utils/src/kbn_client/errors.ts new file mode 100644 index 00000000000000..068c68555b62ae --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/errors.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AxiosError, AxiosResponse } from 'axios'; + +export interface AxiosRequestError extends AxiosError { + response: undefined; +} + +export interface AxiosResponseError extends AxiosError { + response: AxiosResponse; +} + +export const isAxiosRequestError = (error: any): error is AxiosRequestError => { + return error && error.code === undefined && error.response === undefined; +}; + +export const isAxiosResponseError = (error: any): error is AxiosResponseError => { + return error && error.code !== undefined && error.response !== undefined; +}; + +export const isConcliftOnGetError = (error: any) => { + return ( + isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409 + ); +}; diff --git a/packages/kbn-dev-utils/src/kbn_client/index.ts b/packages/kbn-dev-utils/src/kbn_client/index.ts new file mode 100644 index 00000000000000..72214b6c617462 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { KbnClient } from './kbn_client'; +export { uriencode } from './kbn_client_requester'; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts new file mode 100644 index 00000000000000..2eb6c6cc5aac6b --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '../tooling_log'; +import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; +import { KbnClientStatus } from './kbn_client_status'; +import { KbnClientPlugins } from './kbn_client_plugins'; +import { KbnClientVersion } from './kbn_client_version'; +import { KbnClientSavedObjects } from './kbn_client_saved_objects'; +import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; + +export class KbnClient { + private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls); + readonly status = new KbnClientStatus(this.requester); + readonly plugins = new KbnClientPlugins(this.status); + readonly version = new KbnClientVersion(this.status); + readonly savedObjects = new KbnClientSavedObjects(this.log, this.requester); + readonly uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + + /** + * Basic Kibana server client that implements common behaviors for talking + * to the Kibana server from dev tooling. + * + * @param log ToolingLog + * @param kibanaUrls Array of kibana server urls to send requests to + * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets + */ + constructor( + private readonly log: ToolingLog, + private readonly kibanaUrls: string[], + private readonly uiSettingDefaults?: UiSettingValues + ) { + if (!kibanaUrls.length) { + throw new Error('missing Kibana urls'); + } + } + + /** + * Make a direct request to the Kibana server + */ + async request(options: ReqOptions) { + return await this.requester.request(options); + } + + resolveUrl(relativeUrl: string) { + return this.requester.resolveUrl(relativeUrl); + } +} diff --git a/test/common/services/kibana_server/version.js b/packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts similarity index 57% rename from test/common/services/kibana_server/version.js rename to packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts index b7efb01c63449a..80285caf365a0b 100644 --- a/test/common/services/kibana_server/version.js +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts @@ -17,23 +17,28 @@ * under the License. */ -export class KibanaServerVersion { - constructor(kibanaStatus) { - this.kibanaStatus = kibanaStatus; - this._cachedVersionNumber; - } +import { KbnClientStatus } from './kbn_client_status'; - async get() { - if (this._cachedVersionNumber) { - return this._cachedVersionNumber; - } +const PLUGIN_STATUS_ID = /^plugin:(.+?)@/; + +export class KbnClientPlugins { + constructor(private readonly status: KbnClientStatus) {} + /** + * Get a list of plugin ids that are enabled on the server + */ + public async getEnabledIds() { + const pluginIds: string[] = []; + const apiResp = await this.status.get(); - const status = await this.kibanaStatus.get(); - if (status && status.version && status.version.number) { - this._cachedVersionNumber = status.version.number + (status.version.build_snapshot ? '-SNAPSHOT' : ''); - return this._cachedVersionNumber; + for (const status of apiResp.status.statuses) { + if (status.id) { + const match = status.id.match(PLUGIN_STATUS_ID); + if (match) { + pluginIds.push(match[1]); + } + } } - throw new Error(`Unable to fetch Kibana Server status, received ${JSON.stringify(status)}`); + return pluginIds; } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts new file mode 100644 index 00000000000000..56d4d7f99e0b80 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Url from 'url'; + +import Axios from 'axios'; + +import { isAxiosRequestError, isConcliftOnGetError } from './errors'; +import { ToolingLog } from '../tooling_log'; + +export const uriencode = ( + strings: TemplateStringsArray, + ...values: Array +) => { + const queue = strings.slice(); + + if (queue.length === 0) { + throw new Error('how could strings passed to `uriencode` template tag be empty?'); + } + + if (queue.length !== values.length + 1) { + throw new Error('strings and values passed to `uriencode` template tag are unbalanced'); + } + + // pull the first string off the queue, there is one less item in `values` + // since the values are always wrapped in strings, so we shift the extra string + // off the queue to balance the queue and values array. + const leadingString = queue.shift()!; + return queue.reduce( + (acc, string, i) => `${acc}${encodeURIComponent(values[i])}${string}`, + leadingString + ); +}; + +const DEFAULT_MAX_ATTEMPTS = 5; + +export interface ReqOptions { + description?: string; + path: string; + query?: Record; + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + body?: any; + attempt?: number; + maxAttempts?: number; +} + +const delay = (ms: number) => + new Promise(resolve => { + setTimeout(resolve, ms); + }); + +export class KbnClientRequester { + constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {} + + private pickUrl() { + const url = this.kibanaUrls.shift()!; + this.kibanaUrls.push(url); + return url; + } + + public resolveUrl(relativeUrl: string = '/') { + return Url.resolve(this.pickUrl(), relativeUrl); + } + + async request(options: ReqOptions): Promise { + const url = Url.resolve(this.pickUrl(), options.path); + const description = options.description || `${options.method} ${url}`; + const attempt = options.attempt === undefined ? 1 : options.attempt; + const maxAttempts = + options.maxAttempts === undefined ? DEFAULT_MAX_ATTEMPTS : options.maxAttempts; + + try { + const response = await Axios.request({ + method: options.method, + url, + data: options.body, + params: options.query, + headers: { + 'kbn-xsrf': 'kbn-client', + }, + }); + + return response.data; + } catch (error) { + let retryErrorMsg: string | undefined; + if (isAxiosRequestError(error)) { + retryErrorMsg = `[${description}] request failed (attempt=${attempt})`; + } else if (isConcliftOnGetError(error)) { + retryErrorMsg = `Conflict on GET (path=${options.path}, attempt=${attempt})`; + } + + if (retryErrorMsg) { + if (attempt < maxAttempts) { + this.log.error(retryErrorMsg); + await delay(1000 * attempt); + return await this.request({ + ...options, + attempt: attempt + 1, + }); + } + + throw new Error(retryErrorMsg + ' and ran out of retries'); + } + + throw error; + } + } +} diff --git a/test/common/services/kibana_server/saved_objects.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts similarity index 64% rename from test/common/services/kibana_server/saved_objects.ts rename to packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts index 0e4a9a34bf2e40..51fa19c140bf05 100644 --- a/test/common/services/kibana_server/saved_objects.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts @@ -17,16 +17,9 @@ * under the License. */ -import Url from 'url'; +import { ToolingLog } from '../tooling_log'; -import Axios, { AxiosRequestConfig } from 'axios'; -import { ToolingLog } from '@kbn/dev-utils'; - -const joinPath = (...components: Array) => - `/${components - .filter((s): s is string => !!s) - .map(c => encodeURIComponent(c)) - .join('/')}`; +import { KbnClientRequester, uriencode } from './kbn_client_requester'; type MigrationVersion = Record; @@ -64,15 +57,8 @@ interface UpdateOptions extends IndexOptions { id: string; } -export class KibanaServerSavedObjects { - private readonly x = Axios.create({ - baseURL: Url.resolve(this.url, '/api/saved_objects/'), - headers: { - 'kbn-xsrf': 'KibanaServerSavedObjects', - }, - }); - - constructor(private readonly url: string, private readonly log: ToolingLog) {} +export class KbnClientSavedObjects { + constructor(private readonly log: ToolingLog, private readonly requester: KbnClientRequester) {} /** * Get an object @@ -80,8 +66,9 @@ export class KibanaServerSavedObjects { public async get>(options: GetOptions) { this.log.debug('Gettings saved object: %j', options); - return await this.request>('get saved object', { - url: joinPath(options.type, options.id), + return await this.requester.request>({ + description: 'get saved object', + path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'GET', }); } @@ -92,13 +79,16 @@ export class KibanaServerSavedObjects { public async create>(options: IndexOptions) { this.log.debug('Creating saved object: %j', options); - return await this.request>('update saved object', { - url: joinPath(options.type, options.id), - params: { + return await this.requester.request>({ + description: 'update saved object', + path: options.id + ? uriencode`/api/saved_objects/${options.type}/${options.id}` + : uriencode`/api/saved_objects/${options.type}`, + query: { overwrite: options.overwrite, }, method: 'POST', - data: { + body: { attributes: options.attributes, migrationVersion: options.migrationVersion, references: options.references, @@ -112,13 +102,14 @@ export class KibanaServerSavedObjects { public async update>(options: UpdateOptions) { this.log.debug('Updating saved object: %j', options); - return await this.request>('update saved object', { - url: joinPath(options.type, options.id), - params: { + return await this.requester.request>({ + description: 'update saved object', + path: uriencode`/api/saved_objects/${options.type}/${options.id}`, + query: { overwrite: options.overwrite, }, method: 'PUT', - data: { + body: { attributes: options.attributes, migrationVersion: options.migrationVersion, references: options.references, @@ -132,22 +123,10 @@ export class KibanaServerSavedObjects { public async delete(options: GetOptions) { this.log.debug('Deleting saved object %s/%s', options); - return await this.request('delete saved object', { - url: joinPath(options.type, options.id), + return await this.requester.request({ + description: 'delete saved object', + path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'DELETE', }); } - - private async request(desc: string, options: AxiosRequestConfig) { - try { - const resp = await this.x.request(options); - return resp.data; - } catch (error) { - if (error.response) { - throw new Error(`Failed to ${desc}:\n${JSON.stringify(error.response.data, null, 2)}`); - } - - throw error; - } - } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts new file mode 100644 index 00000000000000..22baf4a3304168 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KbnClientRequester } from './kbn_client_requester'; + +interface Status { + state: 'green' | 'red' | 'yellow'; + title?: string; + id?: string; + icon: string; + message: string; + uiColor: string; + since: string; +} + +interface ApiResponseStatus { + name: string; + uuid: string; + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + status: { + overall: Status; + statuses: Status[]; + }; + metrics: unknown; +} + +export class KbnClientStatus { + constructor(private readonly requester: KbnClientRequester) {} + + /** + * Get the full server status + */ + async get() { + return await this.requester.request({ + method: 'GET', + path: 'api/status', + }); + } + + /** + * Get the overall/merged state + */ + public async getOverallState() { + const status = await this.get(); + return status.status.overall.state; + } +} diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts new file mode 100644 index 00000000000000..03033bc5c2ccc4 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ToolingLog } from '../tooling_log'; + +import { KbnClientRequester, uriencode } from './kbn_client_requester'; + +export type UiSettingValues = Record; +interface UiSettingsApiResponse { + settings: { + [key: string]: { + userValue: string | number | boolean; + isOverridden: boolean | undefined; + }; + }; +} + +export class KbnClientUiSettings { + constructor( + private readonly log: ToolingLog, + private readonly requester: KbnClientRequester, + private readonly defaults?: UiSettingValues + ) {} + + async get(setting: string) { + const all = await this.getAll(); + const value = all.settings[setting] ? all.settings[setting].userValue : undefined; + + this.log.verbose('uiSettings.value: %j', value); + return value; + } + + /** + * Gets defaultIndex from the config doc. + */ + async getDefaultIndex() { + return await this.get('defaultIndex'); + } + + /** + * Unset a uiSetting + */ + async unset(setting: string) { + return await this.requester.request({ + path: uriencode`/api/kibana/settings/${setting}`, + method: 'DELETE', + }); + } + + /** + * Replace all uiSettings with the `doc` values, `doc` is merged + * with some defaults + */ + async replace(doc: UiSettingValues) { + const all = await this.getAll(); + for (const [name, { isOverridden }] of Object.entries(all.settings)) { + if (!isOverridden) { + await this.unset(name); + } + } + + this.log.debug('replacing kibana config doc: %j', doc); + + await this.requester.request({ + method: 'POST', + path: '/api/kibana/settings', + body: { + changes: { + ...this.defaults, + ...doc, + }, + }, + }); + } + + /** + * Add fields to the config doc (like setting timezone and defaultIndex) + */ + async update(updates: UiSettingValues) { + this.log.debug('applying update to kibana config: %j', updates); + + await this.requester.request({ + path: '/api/kibana/settings', + method: 'POST', + body: { + changes: updates, + }, + }); + } + + private async getAll() { + return await this.requester.request({ + path: '/api/kibana/settings', + method: 'GET', + }); + } +} diff --git a/test/common/services/kibana_server/status.js b/packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts similarity index 62% rename from test/common/services/kibana_server/status.js rename to packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts index 3988bab185fcca..1aacb857f12f67 100644 --- a/test/common/services/kibana_server/status.js +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts @@ -17,26 +17,20 @@ * under the License. */ -import { resolve as resolveUrl } from 'url'; +import { KbnClientStatus } from './kbn_client_status'; -import Wreck from '@hapi/wreck'; +export class KbnClientVersion { + private versionCache: string | undefined; -const get = async url => { - const { payload } = await Wreck.get(url, { json: 'force' }); - return payload; -}; - -export class KibanaServerStatus { - constructor(kibanaServerUrl) { - this.kibanaServerUrl = kibanaServerUrl; - } + constructor(private readonly status: KbnClientStatus) {} async get() { - return await get(resolveUrl(this.kibanaServerUrl, './api/status')); - } + if (this.versionCache !== undefined) { + return this.versionCache; + } - async getOverallState() { - const status = await this.get(); - return status.status.overall.state; + const status = await this.status.get(); + this.versionCache = status.version.number + (status.version.build_snapshot ? '-SNAPSHOT' : ''); + return this.versionCache; } } diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.js index 411a8b18038a78..adcf65711a4889 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.js @@ -20,12 +20,11 @@ import { migrateKibanaIndex, deleteKibanaIndices, createStats, - getEnabledKibanaPluginIds } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kibanaUrl }) { +export async function emptyKibanaIndexAction({ client, log, kbnClient }) { const stats = createStats('emptyKibanaIndex', log); - const kibanaPluginIds = await getEnabledKibanaPluginIds(kibanaUrl); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); await deleteKibanaIndices({ client, stats }); await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.js index b68bc38096d745..c54fe8c99b7253 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.js @@ -35,7 +35,6 @@ import { createIndexDocRecordsStream, migrateKibanaIndex, Progress, - getEnabledKibanaPluginIds, createDefaultSpace, } from '../lib'; @@ -49,11 +48,11 @@ const pipeline = (...streams) => streams .pipe(dest) )); -export async function loadAction({ name, skipExisting, client, dataDir, log, kibanaUrl }) { +export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); - const kibanaPluginIds = await getEnabledKibanaPluginIds(kibanaUrl); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); // a single stream that emits records from all archive files, in // order, so that createIndexStream can track the state of indexes diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.js index b6aead8c25f21f..c8ed868f9ff172 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.js @@ -32,13 +32,12 @@ import { createParseArchiveStreams, createFilterRecordsStream, createDeleteIndexStream, - getEnabledKibanaPluginIds, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kibanaUrl }) { +export async function unloadAction({ name, client, dataDir, log, kbnClient }) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); - const kibanaPluginIds = await getEnabledKibanaPluginIds(kibanaUrl); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); const files = prioritizeMappings(await readDirectory(inputDir)); for (const filename of files) { diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.js index 9ffff2b28d127f..c4871ad7867915 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.js @@ -17,6 +17,8 @@ * under the License. */ +import { KbnClient } from '@kbn/dev-utils'; + import { saveAction, loadAction, @@ -31,7 +33,7 @@ export class EsArchiver { this.client = client; this.dataDir = dataDir; this.log = log; - this.kibanaUrl = kibanaUrl; + this.kbnClient = new KbnClient(log, [kibanaUrl]); } /** @@ -73,7 +75,7 @@ export class EsArchiver { client: this.client, dataDir: this.dataDir, log: this.log, - kibanaUrl: this.kibanaUrl, + kbnClient: this.kbnClient, }); } @@ -89,7 +91,7 @@ export class EsArchiver { client: this.client, dataDir: this.dataDir, log: this.log, - kibanaUrl: this.kibanaUrl, + kbnClient: this.kbnClient, }); } @@ -144,7 +146,7 @@ export class EsArchiver { await emptyKibanaIndexAction({ client: this.client, log: this.log, - kibanaUrl: this.kibanaUrl, + kbnClient: this.kbnClient, }); } } diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.js index 9a21201152b966..8632a493dd5341 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.js @@ -53,7 +53,3 @@ export { export { Progress } from './progress'; - -export { - getEnabledKibanaPluginIds, -} from './kibana_plugins'; diff --git a/src/es_archiver/lib/kibana_plugins.ts b/src/es_archiver/lib/kibana_plugins.ts deleted file mode 100644 index 44552d5ab20396..00000000000000 --- a/src/es_archiver/lib/kibana_plugins.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Axios from 'axios'; - -const PLUGIN_STATUS_ID = /^plugin:(.+?)@/; -const isString = (v: any): v is string => typeof v === 'string'; - -/** - * Get the list of enabled plugins from Kibana, used to determine which - * uiExports to collect, whether we should clean or clean the kibana index, - * and if we need to inject the default space document in new versions of - * the index. - * - * This must be called before touching the Kibana index as Kibana becomes - * unstable when the .kibana index is deleted/cleaned and the status API - * will fail in situations where status.allowAnonymous=false and security - * is enabled. - */ -export async function getEnabledKibanaPluginIds(kibanaUrl: string): Promise { - try { - const { data } = await Axios.get('/api/status', { - baseURL: kibanaUrl, - }); - - return (data.status.statuses as Array<{ id: string }>) - .map(({ id }) => { - const match = id.match(PLUGIN_STATUS_ID); - if (match) { - return match[1]; - } - }) - .filter(isString); - } catch (error) { - throw new Error( - `Unable to fetch Kibana status API response from Kibana at ${kibanaUrl}: ${error}` - ); - } -} diff --git a/src/legacy/ui/ui_settings/ui_settings_service.js b/src/legacy/ui/ui_settings/ui_settings_service.js index 0451dc588c9234..9f79ed2dbe1687 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service.js +++ b/src/legacy/ui/ui_settings/ui_settings_service.js @@ -174,10 +174,11 @@ export class UiSettingsService { } = options; const { + isConflictError, isNotFoundError, isForbiddenError, isEsUnavailableError, - isNotAuthorizedError + isNotAuthorizedError, } = this._savedObjectsClient.errors; const isIgnorableError = error => ( @@ -196,7 +197,14 @@ export class UiSettingsService { version: this._id, buildNum: this._buildNum, logWithMetadata: this._logWithMetadata, - onWriteError(error, attributes) { + async onWriteError(error, attributes) { + if (isConflictError(error)) { + // trigger `!failedUpgradeAttributes` check below, since another + // request caused the uiSettings object to be created so we can + // just re-read + return false; + } + if (isNotAuthorizedError(error) || isForbiddenError(error)) { return attributes; } diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 1455eb8ed8aa49..f7b7885840455e 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -18,36 +18,24 @@ */ import Url from 'url'; +import { KbnClient } from '@kbn/dev-utils'; import { FtrProviderContext } from '../../ftr_provider_context'; -// @ts-ignore not ts yet -import { KibanaServerStatus } from './status'; -// @ts-ignore not ts yet -import { KibanaServerUiSettings } from './ui_settings'; -// @ts-ignore not ts yet -import { KibanaServerVersion } from './version'; -import { KibanaServerSavedObjects } from './saved_objects'; export function KibanaServerProvider({ getService }: FtrProviderContext) { const log = getService('log'); const config = getService('config'); const lifecycle = getService('lifecycle'); - const url = Url.format(config.get('servers.kibana')); + const defaults = config.get('uiSettings.defaults'); + + const kbn = new KbnClient(log, [url], defaults); - return new (class KibanaServer { - public readonly status = new KibanaServerStatus(url); - public readonly version = new KibanaServerVersion(this.status); - public readonly savedObjects = new KibanaServerSavedObjects(url, log); - public readonly uiSettings = new KibanaServerUiSettings( - url, - log, - config.get('uiSettings.defaults'), - lifecycle - ); + if (defaults) { + lifecycle.on('beforeTests', async () => { + await kbn.uiSettings.update(defaults); + }); + } - public resolveUrl(path = '/') { - return Url.resolve(url, path); - } - })(); + return kbn; } diff --git a/test/common/services/kibana_server/ui_settings.js b/test/common/services/kibana_server/ui_settings.js deleted file mode 100644 index aa75839c20152f..00000000000000 --- a/test/common/services/kibana_server/ui_settings.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Wreck from '@hapi/wreck'; -import { get } from 'lodash'; - -export class KibanaServerUiSettings { - constructor(url, log, defaults, lifecycle) { - this._log = log; - this._defaults = defaults; - this._wreck = Wreck.defaults({ - headers: { 'kbn-xsrf': 'ftr/services/uiSettings' }, - baseUrl: url, - json: true, - redirects: 3, - }); - - if (this._defaults) { - lifecycle.on('beforeTests', async () => { - await this.update(defaults); - }); - } - } - - /** - * Gets defaultIndex from the config doc. - */ - async getDefaultIndex() { - const { payload } = await this._wreck.get('/api/kibana/settings'); - const defaultIndex = get(payload, 'settings.defaultIndex.userValue'); - this._log.verbose('uiSettings.defaultIndex: %j', defaultIndex); - return defaultIndex; - } - - async replace(doc) { - const { payload } = await this._wreck.get('/api/kibana/settings'); - - for (const key of Object.keys(payload.settings)) { - if (!payload.settings[key].isOverridden) { - await this._wreck.delete(`/api/kibana/settings/${key}`); - } - } - - this._log.debug('replacing kibana config doc: %j', doc); - - await this._wreck.post('/api/kibana/settings', { - payload: { - changes: { - ...this._defaults, - ...doc, - }, - }, - }); - } - - /** - * Add fields to the config doc (like setting timezone and defaultIndex) - * @return {Promise} A promise that is resolved when elasticsearch has a response - */ - async update(updates) { - this._log.debug('applying update to kibana config: %j', updates); - await this._wreck.post('/api/kibana/settings', { - payload: { - changes: updates, - }, - }); - } -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index 294f119881b70f..4280e85376201f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -17,6 +17,7 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('pagerduty action', () => { let simulatedActionId = ''; @@ -24,11 +25,9 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { // need to wait for kibanaServer to settle ... before(() => { - const kibanaServer = getService('kibanaServer'); - const kibanaUrl = kibanaServer.status && kibanaServer.status.kibanaServerUrl; - pagerdutySimulatorURL = `${kibanaUrl}${getExternalServiceSimulatorPath( - ExternalServiceSimulator.PAGERDUTY - )}`; + pagerdutySimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) + ); }); after(() => esArchiver.unload('empty_kibana')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index aa678ca9105599..595058e72af0b2 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -17,6 +17,7 @@ import { export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('slack action', () => { let simulatedActionId = ''; @@ -24,11 +25,9 @@ export default function slackTest({ getService }: FtrProviderContext) { // need to wait for kibanaServer to settle ... before(() => { - const kibanaServer = getService('kibanaServer'); - const kibanaUrl = kibanaServer.status && kibanaServer.status.kibanaServerUrl; - slackSimulatorURL = `${kibanaUrl}${getExternalServiceSimulatorPath( - ExternalServiceSimulator.SLACK - )}`; + slackSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK) + ); }); after(() => esArchiver.unload('empty_kibana')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 89fc986fd02556..0f17019518824d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -28,6 +28,7 @@ function parsePort(url: Record): Record { - const kibanaServer = getService('kibanaServer'); - const kibanaUrl = kibanaServer.status && kibanaServer.status.kibanaServerUrl; - const webhookServiceUrl = getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK); - webhookSimulatorURL = `${kibanaUrl}${webhookServiceUrl}`; + webhookSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) + ); }); after(() => esArchiver.unload('empty_kibana')); From 0262f6184c86634920d19923e43b5f367dc682a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2019 00:07:13 -0700 Subject: [PATCH 07/53] Update mocha related packages (major) (#43915) * Update mocha related packages * update snapshots * upgrade gulp-mocha so that it uses new mocha too * fix async/cb overspecification --- package.json | 4 +- packages/eslint-config-kibana/package.json | 2 +- .../lib/mocha/filter_suites_by_tags.test.js | 138 +++--- .../__tests__/data_recognizer.js | 6 +- x-pack/package.json | 6 +- x-pack/tasks/test.js | 1 + x-pack/test/mocha.opts | 1 - yarn.lock | 443 ++++++++---------- 8 files changed, 284 insertions(+), 317 deletions(-) delete mode 100644 x-pack/test/mocha.opts diff --git a/package.json b/package.json index ac313331b3152a..6f54c8683410a7 100644 --- a/package.json +++ b/package.json @@ -379,7 +379,7 @@ "eslint-plugin-import": "2.18.2", "eslint-plugin-jest": "22.17.0", "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-mocha": "5.3.0", + "eslint-plugin-mocha": "6.1.1", "eslint-plugin-no-unsanitized": "3.0.2", "eslint-plugin-node": "9.2.0", "eslint-plugin-prefer-object-spread": "1.2.1", @@ -422,7 +422,7 @@ "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "^3.0.1", - "mocha": "3.5.3", + "mocha": "6.2.1", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 8c6359e66a7a5d..da2a37cc41ad3c 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -24,7 +24,7 @@ "eslint-plugin-jsx-a11y": "6.2.3", "eslint-plugin-import": "2.18.2", "eslint-plugin-jest": "^22.17.0", - "eslint-plugin-mocha": "^5.3.0", + "eslint-plugin-mocha": "^6.1.1", "eslint-plugin-no-unsanitized": "3.0.2", "eslint-plugin-prefer-object-spread": "1.2.1", "eslint-plugin-react": "7.13.0", diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js index fb1ca192c5fd37..9901f62ae71cf3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js @@ -95,16 +95,16 @@ it('only runs hooks of parents and tests in level1a', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1a test 1a", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1a test 1a", + ] + `); }); it('only runs hooks of parents and tests in level1b', async () => { @@ -114,16 +114,16 @@ it('only runs hooks of parents and tests in level1b', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1b' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1b test 1b", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1b' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1b", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1b test 1b", + ] + `); }); it('only runs hooks of parents and tests in level1a and level1b', async () => { @@ -133,20 +133,20 @@ it('only runs hooks of parents and tests in level1a and level1b', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a', 'level1b' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1a test 1a", - "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1b test 1b", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a', 'level1b' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1a test 1a", + "suite: level 1 level 1b", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1b test 1b", + ] + `); }); it('only runs level1a if including level1 and excluding level1b', async () => { @@ -156,17 +156,17 @@ it('only runs level1a if including level1 and excluding level1b', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", - "info: Filtering out any suites that include the tag(s): [ 'level1b' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1a test 1a", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", + "info: Filtering out any suites that include the tag(s): [ 'level1b' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1a test 1a", + ] + `); }); it('only runs level1b if including level1 and excluding level1a', async () => { @@ -176,17 +176,17 @@ it('only runs level1b if including level1 and excluding level1a', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", - "info: Filtering out any suites that include the tag(s): [ 'level1a' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1b test 1b", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", + "info: Filtering out any suites that include the tag(s): [ 'level1a' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1b", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1b test 1b", + ] + `); }); it('only runs level2 if excluding level1', async () => { @@ -196,15 +196,15 @@ it('only runs level2 if excluding level1', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Filtering out any suites that include the tag(s): [ 'level1' ]", - "suite: ", - "suite: level 2", - "suite: level 2 level 2a", - "hook: \\"before each\\" hook: rootBeforeEach", - "test: level 2 level 2a test 2a", -] -`); + Array [ + "info: Filtering out any suites that include the tag(s): [ 'level1' ]", + "suite: ", + "suite: level 2", + "suite: level 2 level 2a", + "hook: \\"before each\\" hook: rootBeforeEach", + "test: level 2 level 2a test 2a", + ] + `); }); it('does nothing if everything excluded', async () => { @@ -214,8 +214,8 @@ it('does nothing if everything excluded', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Filtering out any suites that include the tag(s): [ 'level1', 'level2a' ]", -] -`); + Array [ + "info: Filtering out any suites that include the tag(s): [ 'level1', 'level2a' ]", + ] + `); }); diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js b/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js index 59bfd564f7ca27..f17eb5c5e46de4 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/__tests__/data_recognizer.js @@ -27,18 +27,16 @@ describe('ML - data recognizer', () => { ]; // check all module IDs are the same as the list above - it('listModules - check all module IDs', async (done) => { + it('listModules - check all module IDs', async () => { const modules = await dr.listModules(); const ids = modules.map(m => m.id); expect(ids.join()).to.equal(moduleIds.join()); - done(); }); - it('getModule - load a single module', async (done) => { + it('getModule - load a single module', async () => { const module = await dr.getModule(moduleIds[0]); expect(module.id).to.equal(moduleIds[0]); - done(); }); }); diff --git a/x-pack/package.json b/x-pack/package.json index 90383d901a18fc..d15d66aa18ebea 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -136,7 +136,7 @@ "graphql-codegen-typescript-resolvers-template": "^0.13.0", "graphql-codegen-typescript-template": "^0.13.0", "gulp": "3.9.1", - "gulp-mocha": "2.2.0", + "gulp-mocha": "^7.0.1", "gulp-multi-process": "1.3.1", "hapi": "^17.5.3", "jest": "^24.9.0", @@ -144,10 +144,10 @@ "jest-styled-components": "^6.3.3", "jsdom": "^12.2.0", "madge": "3.4.4", - "mocha": "3.5.3", + "mocha": "6.2.1", "mocha-junit-reporter": "^1.23.1", "mocha-multi-reporters": "^1.1.7", - "mochawesome": "^4.0.1", + "mochawesome": "^4.1.0", "mochawesome-merge": "^2.0.1", "mochawesome-report-generator": "^4.0.1", "mustache": "^2.3.0", diff --git a/x-pack/tasks/test.js b/x-pack/tasks/test.js index 4bd72f0df64a3f..5aae780dda6d7e 100644 --- a/x-pack/tasks/test.js +++ b/x-pack/tasks/test.js @@ -12,6 +12,7 @@ import { createAutoJUnitReporter } from '../../src/dev'; const MOCHA_OPTIONS = { ui: 'bdd', + require: require.resolve('../../src/setup_node_env'), reporter: createAutoJUnitReporter({ reportName: 'X-Pack Mocha Tests', }), diff --git a/x-pack/test/mocha.opts b/x-pack/test/mocha.opts deleted file mode 100644 index 5f1aa61861e607..00000000000000 --- a/x-pack/test/mocha.opts +++ /dev/null @@ -1 +0,0 @@ ---require dev-tools/mocha/setup_mocha.js diff --git a/yarn.lock b/yarn.lock index 969fbe719a2b51..824618215a8be7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4865,6 +4865,11 @@ ansi-color@^0.2.1: resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" integrity sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o= +ansi-colors@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" + integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + ansi-colors@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.1.0.tgz#6374b4dd5d4718ff3ce27a671a3b1cad077132a9" @@ -6841,10 +6846,10 @@ browser-resolve@^1.8.1: dependencies: resolve "1.1.7" -browser-stdout@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" - integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8= +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.1.1" @@ -8233,11 +8238,6 @@ comma-separated-tokens@^1.0.0: dependencies: trim "0.0.1" -commander@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" - integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= - commander@2, commander@2.19.0, commander@^2.11.0, commander@^2.12.2, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -8253,18 +8253,6 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" - integrity sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM= - -commander@2.9.0, commander@~2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= - dependencies: - graceful-readlink ">= 1.0.0" - commander@3.0.0, commander@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" @@ -8287,6 +8275,13 @@ commander@~2.8.1: dependencies: graceful-readlink ">= 1.0.0" +commander@~2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= + dependencies: + graceful-readlink ">= 1.0.0" + common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -9492,6 +9487,11 @@ dargs@^5.1.0: resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" integrity sha1-7H6lDHhWTNNsnV7Bj2Yyn63ieCk= +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -9560,20 +9560,6 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@2.2.0, debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= - dependencies: - ms "0.7.1" - -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" - integrity sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw= - dependencies: - ms "2.0.0" - debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -9609,6 +9595,13 @@ debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + integrity sha1-+HBX6ZWxofauaklgZkE3vFbwOdo= + dependencies: + ms "0.7.1" + debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -10106,26 +10099,16 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== -diff@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" - integrity sha1-fyjS657nsVqX79ic5j3P2qPMur8= - -diff@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" - integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k= +diff@3.5.0, diff@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== diff@^2.1.2: version "2.2.3" resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" integrity sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k= -diff@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" @@ -10998,11 +10981,6 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz#4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1" - integrity sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE= - escape-string-regexp@1.0.5, escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -11224,10 +11202,10 @@ eslint-plugin-jsx-a11y@6.2.3: has "^1.0.3" jsx-ast-utils "^2.2.1" -eslint-plugin-mocha@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-5.3.0.tgz#cf3eb18ae0e44e433aef7159637095a7cb19b15b" - integrity sha512-3uwlJVLijjEmBeNyH60nzqgA1gacUWLUmcKV8PIGNvj1kwP/CTgAWQHn2ayyJVwziX+KETkr9opNwT1qD/RZ5A== +eslint-plugin-mocha@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-6.1.1.tgz#5a036f2f806e1a5fb7d19f7538ebeff3afb15377" + integrity sha512-p/otruG425jRYDa28HjbBYYXoFNzq3Qp++gn5dbE44Kz4NvmIsSUKSV1T+RLYUcZOcdJKKAftXbaqkHFqReKoA== dependencies: ramda "^0.26.1" @@ -11619,6 +11597,21 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/execa/-/execa-2.0.4.tgz#2f5cc589c81db316628627004ea4e37b93391d8e" + integrity sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ== + dependencies: + cross-spawn "^6.0.5" + get-stream "^5.0.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^3.0.0" + onetime "^5.1.0" + p-finally "^2.0.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + execall@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execall/-/execall-1.0.0.tgz#73d0904e395b3cab0658b08d09ec25307f29bb73" @@ -12496,6 +12489,13 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" + integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + dependencies: + is-buffer "~2.0.3" + flatted@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" @@ -13127,6 +13127,13 @@ get-stream@^4.0.0: dependencies: pump "^3.0.0" +get-stream@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -13305,26 +13312,6 @@ glob2base@^0.0.12: dependencies: find-index "^0.1.1" -glob@3.2.11: - version "3.2.11" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" - integrity sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0= - dependencies: - inherits "2" - minimatch "0.3" - -glob@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" - integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg= - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.2" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.1.3, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" @@ -13984,10 +13971,10 @@ grouped-queue@^0.3.0, grouped-queue@^0.3.3: dependencies: lodash "^4.17.2" -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" - integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== growly@^1.3.0: version "1.3.0" @@ -14138,17 +14125,17 @@ gulp-babel@^8.0.0: through2 "^2.0.0" vinyl-sourcemaps-apply "^0.2.0" -gulp-mocha@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-2.2.0.tgz#1ce5eba4b94b40c7436afec3c4982c8eea894192" - integrity sha1-HOXrpLlLQMdDav7DxJgsjuqJQZI= +gulp-mocha@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/gulp-mocha/-/gulp-mocha-7.0.1.tgz#cd29f2fc214a8c08c7d96bf13927d539385a856d" + integrity sha512-LYBEWdOw52kvP+si91iR00LYX9iKXLTBjcKh9b3ChHvVmKtpoITjeRFslPEzDubEk+z6VI1ONEwn9ABqW9/tig== dependencies: - gulp-util "^3.0.0" - mocha "^2.0.1" - plur "^2.1.0" - resolve-from "^1.0.0" - temp "^0.8.3" - through "^2.3.4" + dargs "^7.0.0" + execa "^2.0.4" + mocha "^6.2.0" + plugin-error "^1.0.1" + supports-color "^7.0.0" + through2 "^3.0.1" gulp-multi-process@1.3.1: version "1.3.1" @@ -14570,12 +14557,7 @@ he@0.5.0: resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2" integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI= -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= - -he@1.2.x, he@^1.1.1: +he@1.2.0, he@1.2.x, he@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -15649,7 +15631,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.0, is-buffer@^2.0.2: +is-buffer@^2.0.0, is-buffer@^2.0.2, is-buffer@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== @@ -16097,6 +16079,11 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + is-string@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64" @@ -16457,14 +16444,6 @@ iterare@^1.1.2: resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.1.2.tgz#32e65fe03c72f727b1ae5fd002ed6a215f523ae8" integrity sha512-25rVYmj/dDvTR6zOa9jY1Ihd6USLa0J508Ub2iy7Aga+xu9JMbjDds2Uh03ReDGbva/YN3s3Ybi+Do0nOX6wAg== -jade@0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" - integrity sha1-jxDXl32NefL2/4YqgbBRPMslaGw= - dependencies: - commander "0.6.1" - mkdirp "0.3.0" - jaeger-client@^3.5.3: version "3.13.0" resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.13.0.tgz#c5b228242d65389a13eb24eeb56a55409d72c94e" @@ -17122,7 +17101,7 @@ json2module@^0.0.3: dependencies: rw "^1.3.2" -json3@3.3.2, json3@^3.3.2: +json3@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= @@ -17958,24 +17937,11 @@ lodash-es@^4.17.11, lodash-es@^4.17.4, lodash-es@^4.17.5, lodash-es@^4.2.1: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - integrity sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4= - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= -lodash._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - integrity sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE= - lodash._basetostring@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" @@ -18056,15 +18022,6 @@ lodash.clonedeepwith@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeepwith/-/lodash.clonedeepwith-4.5.0.tgz#6ee30573a03a1a60d670a62ef33c10cf1afdbdd4" integrity sha1-buMFc6A6GmDWcKYu8zwQzxr9vdQ= -lodash.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - integrity sha1-1/KEnw29p+BGgruM1yqwIkYd6+c= - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -19209,14 +19166,6 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" - integrity sha1-J12O2qxPG7MyZHIInnlJyDlGmd0= - dependencies: - lru-cache "2" - sigmund "~1.0.0" - "minimatch@2 || 3", minimatch@3.0.4, minimatch@3.0.x, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -19362,11 +19311,6 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= - mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -19411,39 +19355,34 @@ mocha-multi-reporters@^1.1.7: debug "^3.1.0" lodash "^4.16.4" -mocha@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" - integrity sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg== +mocha@6.2.1, mocha@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.1.tgz#da941c99437da9bac412097859ff99543969f94c" + integrity sha512-VCcWkLHwk79NYQc8cxhkmI8IigTIhsCwZ6RTxQsqK6go4UvEhzJkYuHm8B2YtlSxcYq2fY+ucr4JBwoD6ci80A== dependencies: - browser-stdout "1.3.0" - commander "2.9.0" - debug "2.6.8" - diff "3.2.0" + ansi-colors "3.2.3" + browser-stdout "1.3.1" + debug "3.2.6" + diff "3.5.0" escape-string-regexp "1.0.5" - glob "7.1.1" - growl "1.9.2" - he "1.1.1" - json3 "3.3.2" - lodash.create "3.1.1" - mkdirp "0.5.1" - supports-color "3.1.2" - -mocha@^2.0.1: - version "2.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" - integrity sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg= - dependencies: - commander "2.3.0" - debug "2.2.0" - diff "1.4.0" - escape-string-regexp "1.0.2" - glob "3.2.11" - growl "1.9.2" - jade "0.26.3" + find-up "3.0.0" + glob "7.1.3" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "2.2.0" + minimatch "3.0.4" mkdirp "0.5.1" - supports-color "1.2.0" - to-iso-string "0.0.2" + ms "2.1.1" + node-environment-flags "1.0.5" + object.assign "4.1.0" + strip-json-comments "2.0.1" + supports-color "6.0.0" + which "1.3.1" + wide-align "1.1.3" + yargs "13.3.0" + yargs-parser "13.1.1" + yargs-unparser "1.6.0" mochawesome-merge@^2.0.1: version "2.0.1" @@ -19474,10 +19413,10 @@ mochawesome-report-generator@^4.0.0, mochawesome-report-generator@^4.0.1: validator "^10.11.0" yargs "^13.2.2" -mochawesome@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/mochawesome/-/mochawesome-4.0.1.tgz#351af69c8904468e75a71f8704ed0b5767795ccc" - integrity sha512-F/hVmiwWCvwBiW/UPhs4/lfgf8mBJBr89W/9fDu+hb+rQ9gFxWh9N/BU7RtEH+dMfBF4o8XIdYHrEcwxJhzqsw== +mochawesome@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mochawesome/-/mochawesome-4.1.0.tgz#57cdb9509a9fc54790884ec867e109644ba949ee" + integrity sha512-U23K19mLqmuBqFyIBl7FVkcIuG/2JYStCj+91WmxK1/psLgHlWBEZsNe25U0x4t1Eqgu55aHv+0utLwzfhnupw== dependencies: chalk "^2.4.1" diff "^4.0.1" @@ -19904,6 +19843,14 @@ node-ensure@^0.0.0: resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" integrity sha1-7K52QVDemYYexcgQ/V0Jaxg5Mqc= +node-environment-flags@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a" + integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ== + dependencies: + object.getownpropertydescriptors "^2.0.3" + semver "^5.7.0" + node-fetch@1.7.3, node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -20296,6 +20243,13 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" +npm-run-path@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" + integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== + dependencies: + path-key "^3.0.0" + "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -20476,7 +20430,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.0.4, object.assign@^4.1.0: +object.assign@4.1.0, object.assign@^4.0.4, object.assign@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== @@ -20895,6 +20849,11 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-finally@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" + integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== + p-is-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" @@ -21386,6 +21345,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.0.tgz#99a10d870a803bdd5ee6f0470e58dfcd2f9a54d3" + integrity sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg== + path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" @@ -21674,7 +21638,7 @@ plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -plur@^2.1.0, plur@^2.1.2: +plur@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a" integrity sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo= @@ -23615,7 +23579,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0 isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.1, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: +"readable-stream@2 || 3", readable-stream@^3.0.1, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== @@ -24599,11 +24563,6 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" -rimraf@~2.2.6: - version "2.2.8" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" - integrity sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI= - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" @@ -26138,7 +26097,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -26345,6 +26304,11 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -26357,7 +26321,7 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= -strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: +strip-json-comments@2.0.1, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= @@ -26505,18 +26469,6 @@ supertest@^3.1.0: methods "~1.1.2" superagent "3.8.2" -supports-color@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-1.2.0.tgz#ff1ed1e61169d06b3cf2d588e188b18d8847e17e" - integrity sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4= - -supports-color@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" - integrity sha1-cqJiiU2dQIuVbKBf83su2KbiotU= - dependencies: - has-flag "^1.0.0" - supports-color@5.5.0, supports-color@^5.0.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -26524,6 +26476,13 @@ supports-color@5.5.0, supports-color@^5.0.0, supports-color@^5.4.0, supports-col dependencies: has-flag "^3.0.0" +supports-color@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" + integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== + dependencies: + has-flag "^3.0.0" + supports-color@6.1.0, supports-color@^6.0.0, supports-color@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" @@ -26848,14 +26807,6 @@ temp-dir@^1.0.0: resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= -temp@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" - integrity sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k= - dependencies: - os-tmpdir "^1.0.0" - rimraf "~2.2.6" - temp@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/temp/-/temp-0.4.0.tgz#671ad63d57be0fe9d7294664b3fc400636678a60" @@ -27010,6 +26961,13 @@ through2@^0.6.1: readable-stream ">=1.0.33-1 <1.1.0-0" xtend ">=4.0.0 <4.1.0-0" +through2@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" + integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== + dependencies: + readable-stream "2 || 3" + through2@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" @@ -27209,11 +27167,6 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= -to-iso-string@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/to-iso-string/-/to-iso-string-0.0.2.tgz#4dc19e664dfccbe25bd8db508b00c6da158255d1" - integrity sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE= - to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -29716,7 +29669,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@1, which@^1.2.9, which@^1.3.1, which@~1.3.0: +which@1, which@1.3.1, which@^1.2.9, which@^1.3.1, which@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -29730,6 +29683,13 @@ which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.8, which@^1.3.0: dependencies: isexe "^2.0.0" +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + wide-align@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" @@ -30183,6 +30143,14 @@ yallist@^3.0.3: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== +yargs-parser@13.1.1, yargs-parser@^13.0.0, yargs-parser@^13.1.0, yargs-parser@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^10.0.0: version "10.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" @@ -30198,14 +30166,6 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.0.0, yargs-parser@^13.1.0, yargs-parser@^13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" @@ -30228,6 +30188,15 @@ yargs-parser@^9.0.2: dependencies: camelcase "^4.1.0" +yargs-unparser@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" + integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== + dependencies: + flat "^4.1.0" + lodash "^4.17.15" + yargs "^13.3.0" + yargs@12.0.5, yargs@^12.0.5: version "12.0.5" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" @@ -30263,6 +30232,22 @@ yargs@13.2.4: y18n "^4.0.0" yargs-parser "^13.1.0" +yargs@13.3.0, yargs@^13.2.2, yargs@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" + yargs@4.8.1: version "4.8.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" @@ -30301,22 +30286,6 @@ yargs@^11.0.0: y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@^13.2.2, yargs@^13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" - integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.1" - yargs@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.0.0.tgz#ba4cacc802b3c0b3e36a9e791723763d57a85066" From 85c8232c0b81e9e0dd2265e04b2cc26dffaf1b60 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Tue, 1 Oct 2019 09:11:33 +0200 Subject: [PATCH 08/53] Move KibanaMigrator into Server SavedObjectsService (#43433) * Rename SavedObjectsService -> SavedObjectsLegacyService * Expose legacy pluginSpecs from Core LegacyService * Expose legacy uiExports from Core LegacyService * Move kibana config to NP * Expose pluginExtendedConfig from LegacyService * Make KibanaMigrator NP compatible * KibanaMigrator -> NP SavedObjectsService * SavedObjectsService never stop retrying ES connection error * Move waiting for migrations to complete till after legacy service start * Fix ESArchiver's KibanaMigrator * Fix reload logging config tests * Run migrations on savedobjects start * Fix env tests * Fix and make legacy tests more robust/isolated * Cleanup code * Fix invalid config test * Fix SavedObject Migrations logging test * SavedObjectsService tests * Lifecycle logging and improve getting kibanaConfig instance * Fix awaitMigration bug and test * Fix typing error * Review comments * Remove unecessary KibanaConfig class * Move legacy plugin config extension, specs, uiExports entirely into Core uiExports, specs, disabledSpecs, config now get injected into KbnServer * Fix config deprecation test * Use existing logger mock * Create SavedObjectsConfig for migration config * Define KibanaMigratorContract type * KibanaMigratorContract -> IKibanaMigrator + docs improvements * Fix esArchiver's KibanaMigrator * Fix plugin generator integration test * ConfigServiceContract -> IConfigService * Address review comments * Review nits * Document migrations.skip config * Review comments continued... * awaitMigrations -> runMigrations * Type improvements --- .../kibana-plugin-server.internalcorestart.md | 12 -- .../core/server/kibana-plugin-server.md | 4 - ...server.savedobjectsschema._constructor_.md | 20 -- ...edobjectsschema.getconverttoaliasscript.md | 22 -- ...rver.savedobjectsschema.getindexfortype.md | 23 --- ...-server.savedobjectsschema.ishiddentype.md | 22 -- ....savedobjectsschema.isnamespaceagnostic.md | 22 -- ...kibana-plugin-server.savedobjectsschema.md | 27 --- ...er.savedobjectsserializer._constructor_.md | 20 -- ...er.savedobjectsserializer.generaterawid.md | 26 --- ...savedobjectsserializer.israwsavedobject.md | 24 --- ...na-plugin-server.savedobjectsserializer.md | 27 --- ...savedobjectsserializer.rawtosavedobject.md | 24 --- ...savedobjectsserializer.savedobjecttoraw.md | 24 --- ...dscopedsavedobjectsclientwrapperfactory.md | 11 - ...bjectsservice.getsavedobjectsrepository.md | 22 -- ...ectsservice.getscopedsavedobjectsclient.md | 11 - ...server.savedobjectsservice.importexport.md | 16 -- ...ibana-plugin-server.savedobjectsservice.md | 30 --- ....savedobjectsservice.savedobjectsclient.md | 11 - ...lugin-server.savedobjectsservice.schema.md | 11 - ...plugin-server.savedobjectsservice.types.md | 11 - kibana.d.ts | 2 +- .../integration_tests/generate_plugin.test.js | 7 +- .../integration_tests/invalid_config.test.js | 5 +- .../reload_logging_config.test.js | 18 +- src/cli/serve/serve.js | 3 +- src/core/server/bootstrap.ts | 32 +-- src/core/server/config/config_service.mock.ts | 14 +- src/core/server/config/config_service.ts | 3 + src/core/server/config/index.ts | 2 +- src/core/server/core_context.mock.ts | 41 ++++ src/core/server/core_context.ts | 4 +- .../elasticsearch/retry_call_cluster.test.ts | 58 ++++++ .../elasticsearch/retry_call_cluster.ts | 58 ++++++ src/core/server/index.test.mocks.ts | 12 +- src/core/server/index.ts | 9 +- src/core/server/kibana_config.ts | 34 ++++ .../__snapshots__/legacy_service.test.ts.snap | 50 ----- src/core/server/legacy/legacy_service.test.ts | 49 +++-- src/core/server/legacy/legacy_service.ts | 128 +++++++++--- .../plugins/find_legacy_plugin_specs.ts | 136 +++++++++++++ src/core/server/legacy/plugins/index.ts | 19 ++ src/core/server/logging/index.ts | 2 +- .../server/logging/logging_service.mock.ts | 7 +- src/core/server/logging/logging_service.ts | 1 + src/core/server/saved_objects/index.ts | 4 + .../server/saved_objects/mappings/index.ts | 8 +- .../server/saved_objects/mappings/types.ts | 5 + .../server/saved_objects/migrations/README.md | 16 +- .../migrations/core/document_migrator.test.ts | 21 +- .../migrations/core/document_migrator.ts | 34 ++-- .../migrations/core/index_migrator.test.ts | 3 +- .../migrations/core/migration_context.ts | 10 +- .../migrations/core/migration_logger.ts | 14 +- .../server/saved_objects/migrations/index.ts | 2 +- .../saved_objects/migrations/kibana/index.ts | 2 +- .../migrations/kibana/kibana_migrator.mock.ts | 46 +++++ .../migrations/kibana/kibana_migrator.test.ts | 169 ++++++---------- .../migrations/kibana/kibana_migrator.ts | 190 +++++++++--------- .../saved_objects/saved_objects_config.ts} | 28 +-- .../saved_objects_service.mock.ts | 49 +++++ .../saved_objects_service.test.ts | 113 +++++++++++ .../saved_objects/saved_objects_service.ts | 126 ++++++++++++ .../server/saved_objects/schema/schema.ts | 2 + .../saved_objects/serialization/index.ts | 1 + .../server/saved_objects/service/index.ts | 5 +- .../service/lib/repository.test.js | 30 +-- .../saved_objects/service/lib/repository.ts | 2 +- src/core/server/saved_objects/types.ts | 16 ++ src/core/server/server.api.md | 70 +++---- src/core/server/server.test.ts | 7 + src/core/server/server.ts | 21 +- src/es_archiver/lib/indices/kibana_index.js | 52 ++--- .../plugin_discovery/find_plugin_specs.js | 2 +- .../plugin_spec/plugin_spec.js | 9 +- .../config/__tests__/deprecation_warnings.js | 6 +- src/legacy/server/config/schema.js | 6 - src/legacy/server/kbn_server.d.ts | 6 +- src/legacy/server/kbn_server.js | 16 +- src/legacy/server/plugins/scan_mixin.js | 89 +------- .../saved_objects/saved_objects_mixin.js | 3 +- .../saved_objects/saved_objects_mixin.test.js | 87 ++++---- ...ct_ui_exports.js => collect_ui_exports.ts} | 13 +- src/legacy/ui/ui_exports/index.js | 1 - src/legacy/ui/ui_mixin.js | 2 - tasks/config/run.js | 1 + .../apis/saved_objects/migrations.js | 2 +- test/tsconfig.json | 1 + .../setup.js => test/typings/index.d.ts | 10 +- .../server/lib/ml_telemetry/ml_telemetry.ts | 8 +- .../plugins/ml/server/new_platform/plugin.ts | 8 +- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 6 +- .../lib/copy_to_spaces/copy_to_spaces.ts | 8 +- .../copy_to_spaces/lib/get_eligible_types.ts | 7 +- .../resolve_copy_conflicts.test.ts | 6 +- .../copy_to_spaces/resolve_copy_conflicts.ts | 8 +- .../server/lib/create_default_space.test.ts | 4 +- .../spaces/server/lib/create_default_space.ts | 4 +- .../on_post_auth_interceptor.test.ts | 4 +- .../spaces_tutorial_context_factory.test.ts | 4 +- .../spaces/server/new_platform/plugin.ts | 4 +- .../spaces_service/spaces_service.test.ts | 8 +- .../api/__fixtures__/create_test_handler.ts | 16 +- .../server/routes/api/external/index.ts | 4 +- x-pack/legacy/plugins/task_manager/index.ts | 2 +- 106 files changed, 1400 insertions(+), 1115 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-server.internalcorestart.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsschema.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md delete mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md create mode 100644 src/core/server/core_context.mock.ts create mode 100644 src/core/server/elasticsearch/retry_call_cluster.test.ts create mode 100644 src/core/server/elasticsearch/retry_call_cluster.ts create mode 100644 src/core/server/kibana_config.ts create mode 100644 src/core/server/legacy/plugins/find_legacy_plugin_specs.ts create mode 100644 src/core/server/legacy/plugins/index.ts create mode 100644 src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts rename src/{legacy/ui/ui_exports/ui_exports_mixin.js => core/server/saved_objects/saved_objects_config.ts} (63%) create mode 100644 src/core/server/saved_objects/saved_objects_service.mock.ts create mode 100644 src/core/server/saved_objects/saved_objects_service.test.ts create mode 100644 src/core/server/saved_objects/saved_objects_service.ts rename src/legacy/ui/ui_exports/{collect_ui_exports.js => collect_ui_exports.ts} (74%) rename src/legacy/server/config/setup.js => test/typings/index.d.ts (75%) diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.md deleted file mode 100644 index 4943249d284b04..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.internalcorestart.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) - -## InternalCoreStart interface - - -Signature: - -```typescript -export interface InternalCoreStart -``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index fec2fc4b64019f..d943228bbea06b 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -21,8 +21,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | -| [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) | | -| [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | ## Enumerations @@ -52,7 +50,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | -| [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | | [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | @@ -96,7 +93,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | -| [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) | | | [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md deleted file mode 100644 index f4fb88fa6d4f11..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [(constructor)](./kibana-plugin-server.savedobjectsschema._constructor_.md) - -## SavedObjectsSchema.(constructor) - -Constructs a new instance of the `SavedObjectsSchema` class - -Signature: - -```typescript -constructor(schemaDefinition?: SavedObjectsSchemaDefinition); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| schemaDefinition | SavedObjectsSchemaDefinition | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md deleted file mode 100644 index 5baf075463558a..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getConvertToAliasScript](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) - -## SavedObjectsSchema.getConvertToAliasScript() method - -Signature: - -```typescript -getConvertToAliasScript(type: string): string | undefined; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | - -Returns: - -`string | undefined` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md deleted file mode 100644 index ba1c439c8c6b4e..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getIndexForType](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) - -## SavedObjectsSchema.getIndexForType() method - -Signature: - -```typescript -getIndexForType(config: Config, type: string): string | undefined; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| config | Config | | -| type | string | | - -Returns: - -`string | undefined` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md deleted file mode 100644 index f67b12a4d14c3d..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [isHiddenType](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) - -## SavedObjectsSchema.isHiddenType() method - -Signature: - -```typescript -isHiddenType(type: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md deleted file mode 100644 index 2ca0abd7e4aa7e..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [isNamespaceAgnostic](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) - -## SavedObjectsSchema.isNamespaceAgnostic() method - -Signature: - -```typescript -isNamespaceAgnostic(type: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md deleted file mode 100644 index 0808811804eaf5..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) - -## SavedObjectsSchema class - -Signature: - -```typescript -export declare class SavedObjectsSchema -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(schemaDefinition)](./kibana-plugin-server.savedobjectsschema._constructor_.md) | | Constructs a new instance of the SavedObjectsSchema class | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [getConvertToAliasScript(type)](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) | | | -| [getIndexForType(config, type)](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) | | | -| [isHiddenType(type)](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) | | | -| [isNamespaceAgnostic(type)](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) | | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md deleted file mode 100644 index c05e97d3dbcdf8..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [(constructor)](./kibana-plugin-server.savedobjectsserializer._constructor_.md) - -## SavedObjectsSerializer.(constructor) - -Constructs a new instance of the `SavedObjectsSerializer` class - -Signature: - -```typescript -constructor(schema: SavedObjectsSchema); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| schema | SavedObjectsSchema | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md deleted file mode 100644 index 4705f48a201aee..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [generateRawId](./kibana-plugin-server.savedobjectsserializer.generaterawid.md) - -## SavedObjectsSerializer.generateRawId() method - -Given a saved object type and id, generates the compound id that is stored in the raw document. - -Signature: - -```typescript -generateRawId(namespace: string | undefined, type: string, id?: string): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| namespace | string | undefined | | -| type | string | | -| id | string | | - -Returns: - -`string` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md deleted file mode 100644 index e190e7bce8c011..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [isRawSavedObject](./kibana-plugin-server.savedobjectsserializer.israwsavedobject.md) - -## SavedObjectsSerializer.isRawSavedObject() method - -Determines whether or not the raw document can be converted to a saved object. - -Signature: - -```typescript -isRawSavedObject(rawDoc: RawDoc): any; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| rawDoc | RawDoc | | - -Returns: - -`any` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md deleted file mode 100644 index dd3f52554a81ea..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) - -## SavedObjectsSerializer class - -Signature: - -```typescript -export declare class SavedObjectsSerializer -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(schema)](./kibana-plugin-server.savedobjectsserializer._constructor_.md) | | Constructs a new instance of the SavedObjectsSerializer class | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [generateRawId(namespace, type, id)](./kibana-plugin-server.savedobjectsserializer.generaterawid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document. | -| [isRawSavedObject(rawDoc)](./kibana-plugin-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. | -| [rawToSavedObject(doc)](./kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. | -| [savedObjectToRaw(savedObj)](./kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md) | | Converts a document from the saved object client format to the format that is stored in elasticsearch. | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md deleted file mode 100644 index b36cdb3be64da9..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [rawToSavedObject](./kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md) - -## SavedObjectsSerializer.rawToSavedObject() method - -Converts a document from the format that is stored in elasticsearch to the saved object client format. - -Signature: - -```typescript -rawToSavedObject(doc: RawDoc): SanitizedSavedObjectDoc; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| doc | RawDoc | | - -Returns: - -`SanitizedSavedObjectDoc` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md deleted file mode 100644 index 4854a97a845b89..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [savedObjectToRaw](./kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md) - -## SavedObjectsSerializer.savedObjectToRaw() method - -Converts a document from the saved object client format to the format that is stored in elasticsearch. - -Signature: - -```typescript -savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): RawDoc; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| savedObj | SanitizedSavedObjectDoc | | - -Returns: - -`RawDoc` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md deleted file mode 100644 index 6e0d1a827750cf..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [addScopedSavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md) - -## SavedObjectsService.addScopedSavedObjectsClientWrapperFactory property - -Signature: - -```typescript -addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider['addClientWrapperFactory']; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md deleted file mode 100644 index 13ccad7ed01ae5..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [getSavedObjectsRepository](./kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md) - -## SavedObjectsService.getSavedObjectsRepository() method - -Signature: - -```typescript -getSavedObjectsRepository(...rest: any[]): any; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| rest | any[] | | - -Returns: - -`any` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md deleted file mode 100644 index c762de041edf5f..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [getScopedSavedObjectsClient](./kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md) - -## SavedObjectsService.getScopedSavedObjectsClient property - -Signature: - -```typescript -getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md deleted file mode 100644 index f9b4e46712f4a1..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [importExport](./kibana-plugin-server.savedobjectsservice.importexport.md) - -## SavedObjectsService.importExport property - -Signature: - -```typescript -importExport: { - objectLimit: number; - importSavedObjects(options: SavedObjectsImportOptions): Promise; - resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; - getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; - }; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md deleted file mode 100644 index d9e23e6f15928e..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md +++ /dev/null @@ -1,30 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) - -## SavedObjectsService interface - - -Signature: - -```typescript -export interface SavedObjectsService -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [addScopedSavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md) | ScopedSavedObjectsClientProvider<Request>['addClientWrapperFactory'] | | -| [getScopedSavedObjectsClient](./kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md) | ScopedSavedObjectsClientProvider<Request>['getClient'] | | -| [importExport](./kibana-plugin-server.savedobjectsservice.importexport.md) | {
objectLimit: number;
importSavedObjects(options: SavedObjectsImportOptions): Promise<SavedObjectsImportResponse>;
resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise<SavedObjectsImportResponse>;
getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise<Readable>;
} | | -| [SavedObjectsClient](./kibana-plugin-server.savedobjectsservice.savedobjectsclient.md) | typeof SavedObjectsClient | | -| [schema](./kibana-plugin-server.savedobjectsservice.schema.md) | SavedObjectsSchema | | -| [types](./kibana-plugin-server.savedobjectsservice.types.md) | string[] | | - -## Methods - -| Method | Description | -| --- | --- | -| [getSavedObjectsRepository(rest)](./kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md) | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md deleted file mode 100644 index 4a7722928e85e0..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsservice.savedobjectsclient.md) - -## SavedObjectsService.SavedObjectsClient property - -Signature: - -```typescript -SavedObjectsClient: typeof SavedObjectsClient; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md deleted file mode 100644 index be5682e6f034e4..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [schema](./kibana-plugin-server.savedobjectsservice.schema.md) - -## SavedObjectsService.schema property - -Signature: - -```typescript -schema: SavedObjectsSchema; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md deleted file mode 100644 index a783ef4270f186..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [types](./kibana-plugin-server.savedobjectsservice.types.md) - -## SavedObjectsService.types property - -Signature: - -```typescript -types: string[]; -``` diff --git a/kibana.d.ts b/kibana.d.ts index e0b20f6fa28af6..d242965e9bdd5a 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -42,7 +42,7 @@ export namespace Legacy { export type Request = LegacyKibanaServer.Request; export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; export type SavedObjectsClient = LegacyKibanaServer.SavedObjectsClient; - export type SavedObjectsService = LegacyKibanaServer.SavedObjectsService; + export type SavedObjectsService = LegacyKibanaServer.SavedObjectsLegacyService; export type Server = LegacyKibanaServer.Server; export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 18e161e0ce925e..523317ab63e85b 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -88,7 +88,12 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug await withProcRunner(log, async proc => { await proc.run('kibana', { cmd: 'yarn', - args: ['start', '--optimize.enabled=false', '--logging.json=false'], + args: [ + 'start', + '--optimize.enabled=false', + '--logging.json=false', + '--migrations.skip=true', + ], cwd: generatedPath, wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/, }); diff --git a/src/cli/serve/integration_tests/invalid_config.test.js b/src/cli/serve/integration_tests/invalid_config.test.js index a3f44697281090..e86fb03ad79546 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.js +++ b/src/cli/serve/integration_tests/invalid_config.test.js @@ -25,9 +25,12 @@ const INVALID_CONFIG_PATH = resolve(__dirname, '__fixtures__/invalid_config.yml' describe('cli invalid config support', function () { it('exits with statusCode 64 and logs a single line when config is invalid', function () { + // Unused keys only throw once LegacyService starts, so disable migrations so that Core + // will finish the start lifecycle without a running Elasticsearch instance. const { error, status, stdout } = spawnSync(process.execPath, [ 'src/cli', - '--config', INVALID_CONFIG_PATH + '--config', INVALID_CONFIG_PATH, + '--migrations.skip=true' ], { cwd: ROOT_DIR }); diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js index 2b6f229ca9dae4..206118d2d1be85 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ b/src/cli/serve/integration_tests/reload_logging_config.test.js @@ -83,7 +83,7 @@ describe('Server logging configuration', function () { it('should be reloadable via SIGHUP process signaling', async function () { expect.assertions(3); - child = spawn(process.execPath, [kibanaPath, '--config', testConfigFile, '--oss'], { + child = spawn(process.execPath, [kibanaPath, '--config', testConfigFile, '--oss', '--verbose'], { stdio: 'pipe' }); @@ -114,7 +114,9 @@ describe('Server logging configuration', function () { const data = JSON.parse(line); sawJson = true; - if (data.tags.includes('listening')) { + // We know the sighup handler will be registered before + // root.setup() is called + if (data.message.includes('setting up root')) { isJson = false; setLoggingJson(false); @@ -128,10 +130,9 @@ describe('Server logging configuration', function () { // the switch yet, so we ignore before switching over. } else { // Kibana has successfully stopped logging json, so kill the server. - sawNonjson = true; - child.kill(); + child && child.kill(); child = undefined; } }) @@ -178,10 +179,11 @@ describe('Server logging configuration', function () { '--config', testConfigFile, '--logging.dest', logPath, '--plugins.initialize', 'false', - '--logging.json', 'false' + '--logging.json', 'false', + '--verbose' ]); - watchFileUntil(logPath, /http server running/, 2 * minute) + watchFileUntil(logPath, /starting server/, 2 * minute) .then(() => { // once the server is running, archive the log file and issue SIGHUP fs.renameSync(logPath, logPathArchived); @@ -190,8 +192,8 @@ describe('Server logging configuration', function () { .then(() => watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 10 * second)) .then(contents => { const lines = contents.toString().split('\n'); - // should be the first and only new line of the log file - expect(lines).toHaveLength(2); + // should be the first line of the new log file + expect(lines[0]).toMatch(/Reloaded logging configuration due to SIGHUP/); child.kill(); }) .then(done, done); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 7f479a7e118e02..1f7593d788304b 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -194,7 +194,6 @@ export default function (program) { .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) .option('--optimize', 'Optimize and then stop the server'); - if (CAN_REPL) { command.option('--repl', 'Run the server with a REPL prompt and access to the server object'); } @@ -240,7 +239,7 @@ export default function (program) { repl: !!opts.repl, basePath: !!opts.basePath, optimize: !!opts.optimize, - oss: !!opts.oss, + oss: !!opts.oss }, features: { isClusterModeSupported: CAN_CLUSTER, diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 6a4bfc7c581df8..2dff4430b4dbef 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -70,6 +70,22 @@ export async function bootstrap({ const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown); + process.on('SIGHUP', () => { + const cliLogger = root.logger.get('cli'); + cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] }); + + try { + rawConfigService.reloadConfig(); + } catch (err) { + return shutdown(err); + } + + cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] }); + }); + + process.on('SIGINT', () => shutdown()); + process.on('SIGTERM', () => shutdown()); + function shutdown(reason?: Error) { rawConfigService.stop(); return root.shutdown(reason); @@ -87,22 +103,6 @@ export async function bootstrap({ cliLogger.info('Optimization done.'); await shutdown(); } - - process.on('SIGHUP', () => { - const cliLogger = root.logger.get('cli'); - cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] }); - - try { - rawConfigService.reloadConfig(); - } catch (err) { - return shutdown(err); - } - - cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] }); - }); - - process.on('SIGINT', () => shutdown()); - process.on('SIGTERM', () => shutdown()); } function onRootShutdown(reason?: any) { diff --git a/src/core/server/config/config_service.mock.ts b/src/core/server/config/config_service.mock.ts index b9c4fa91ae7028..e87869e92deebc 100644 --- a/src/core/server/config/config_service.mock.ts +++ b/src/core/server/config/config_service.mock.ts @@ -20,11 +20,13 @@ import { BehaviorSubject } from 'rxjs'; import { ObjectToConfigAdapter } from './object_to_config_adapter'; -import { ConfigService } from './config_service'; +import { IConfigService } from './config_service'; -type ConfigServiceContract = PublicMethodsOf; -const createConfigServiceMock = () => { - const mocked: jest.Mocked = { +const createConfigServiceMock = ({ + atPath = {}, + getConfig$ = {}, +}: { atPath?: Record; getConfig$?: Record } = {}) => { + const mocked: jest.Mocked = { atPath: jest.fn(), getConfig$: jest.fn(), optionalAtPath: jest.fn(), @@ -33,8 +35,8 @@ const createConfigServiceMock = () => { isEnabledAtPath: jest.fn(), setSchema: jest.fn(), }; - mocked.atPath.mockReturnValue(new BehaviorSubject({})); - mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter({}))); + mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); + mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); mocked.getUsedPaths.mockResolvedValue([]); mocked.getUnusedPaths.mockResolvedValue([]); mocked.isEnabledAtPath.mockResolvedValue(true); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index fff19aa3af0f04..8d3cc733cf250c 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -26,6 +26,9 @@ import { Config, ConfigPath, Env } from '.'; import { Logger, LoggerFactory } from '../logging'; import { hasConfigPathIntersection } from './config'; +/** @internal */ +export type IConfigService = PublicMethodsOf; + /** @internal */ export class ConfigService { private readonly log: Logger; diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 257263069cabd8..d27462a86a9c8e 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { ConfigService } from './config_service'; +export { ConfigService, IConfigService } from './config_service'; export { RawConfigService } from './raw_config_service'; export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts new file mode 100644 index 00000000000000..e8c0a0a4830bfb --- /dev/null +++ b/src/core/server/core_context.mock.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreContext } from './core_context'; +import { getEnvOptions } from './config/__mocks__/env'; +import { Env, IConfigService } from './config'; +import { loggingServiceMock } from './logging/logging_service.mock'; +import { configServiceMock } from './config/config_service.mock'; +import { ILoggingService } from './logging'; + +function create({ + env = Env.createDefault(getEnvOptions()), + logger = loggingServiceMock.create(), + configService = configServiceMock.create(), +}: { + env?: Env; + logger?: jest.Mocked; + configService?: jest.Mocked; +} = {}): CoreContext { + return { coreId: Symbol(), env, logger, configService }; +} + +export const mockCoreContext = { + create, +}; diff --git a/src/core/server/core_context.ts b/src/core/server/core_context.ts index 701f5a83a81c20..237fc2e6aafdce 100644 --- a/src/core/server/core_context.ts +++ b/src/core/server/core_context.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ConfigService, Env } from './config'; +import { IConfigService, Env } from './config'; import { LoggerFactory } from './logging'; /** @internal */ @@ -31,6 +31,6 @@ export type CoreId = symbol; export interface CoreContext { coreId: CoreId; env: Env; - configService: ConfigService; + configService: IConfigService; logger: LoggerFactory; } diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts new file mode 100644 index 00000000000000..e2c6415a08c56d --- /dev/null +++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import elasticsearch from 'elasticsearch'; +import { retryCallCluster } from './retry_call_cluster'; + +describe('retryCallCluster', () => { + it('retries ES API calls that rejects with NoConnection errors', () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + return i++ <= 2 + ? Promise.reject(new elasticsearch.errors.NoConnections()) + : Promise.resolve('success'); + }); + const retried = retryCallCluster(callEsApi); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + + it('rejects when ES API calls reject with other errors', async () => { + expect.assertions(3); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + i++; + + return i === 1 + ? Promise.reject(new Error('unknown error')) + : i === 2 + ? Promise.resolve('success') + : i === 3 || i === 4 + ? Promise.reject(new elasticsearch.errors.NoConnections()) + : i === 5 + ? Promise.reject(new Error('unknown error')) + : null; + }); + const retried = retryCallCluster(callEsApi); + await expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + await expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); +}); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts new file mode 100644 index 00000000000000..4b74dffebbef9d --- /dev/null +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { retryWhen, concatMap } from 'rxjs/operators'; +import { defer, throwError, iif, timer } from 'rxjs'; +import elasticsearch from 'elasticsearch'; +import { CallAPIOptions } from '.'; + +/** + * Retries the provided Elasticsearch API call when a `NoConnections` error is + * encountered. The API call will be retried once a second, indefinitely, until + * a successful response or a different error is received. + * + * @param apiCaller + */ + +// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged +export function retryCallCluster( + apiCaller: ( + endpoint: string, + clientParams: Record, + options?: CallAPIOptions + ) => Promise +) { + return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { + return defer(() => apiCaller(endpoint, clientParams, options)) + .pipe( + retryWhen(errors => + errors.pipe( + concatMap((error, i) => + iif( + () => error instanceof elasticsearch.errors.NoConnections, + timer(1000), + throwError(error) + ) + ) + ) + ) + ) + .toPromise(); + }; +} diff --git a/src/core/server/index.test.mocks.ts b/src/core/server/index.test.mocks.ts index 9526a7d79ee43d..12cba7b29fc780 100644 --- a/src/core/server/index.test.mocks.ts +++ b/src/core/server/index.test.mocks.ts @@ -35,7 +35,11 @@ jest.doMock('./elasticsearch/elasticsearch_service', () => ({ ElasticsearchService: jest.fn(() => mockElasticsearchService), })); -export const mockLegacyService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() }; +export const mockLegacyService = { + setup: jest.fn().mockReturnValue({ uiExports: {} }), + start: jest.fn(), + stop: jest.fn(), +}; jest.mock('./legacy/legacy_service', () => ({ LegacyService: jest.fn(() => mockLegacyService), })); @@ -45,3 +49,9 @@ export const mockConfigService = configServiceMock.create(); jest.doMock('./config/config_service', () => ({ ConfigService: jest.fn(() => mockConfigService), })); + +import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; +export const mockSavedObjectsService = savedObjectsServiceMock.create(); +jest.doMock('./saved_objects/saved_objects_service', () => ({ + SavedObjectsService: jest.fn(() => mockSavedObjectsService), +})); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 83328d560ee22f..d3fe64ddc1e0df 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -55,6 +55,7 @@ import { } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; +import { SavedObjectsServiceStart } from './saved_objects'; export { bootstrap } from './bootstrap'; export { ConfigPath, ConfigService } from './config'; @@ -152,7 +153,7 @@ export { SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, SavedObjectsSerializer, - SavedObjectsService, + SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from './saved_objects'; @@ -232,9 +233,11 @@ export interface InternalCoreSetup { } /** - * @public + * @internal */ -export interface InternalCoreStart {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface InternalCoreStart { + savedObjects: SavedObjectsServiceStart; +} export { ContextSetup, diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts new file mode 100644 index 00000000000000..d46960289a8d01 --- /dev/null +++ b/src/core/server/kibana_config.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export type KibanaConfigType = TypeOf; + +export const config = { + path: 'kibana', + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + defaultAppId: schema.string({ defaultValue: 'home' }), + index: schema.string({ defaultValue: '.kibana' }), + disableWelcomeScreen: schema.boolean({ defaultValue: false }), + autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), + autocompleteTimeout: schema.duration({ defaultValue: 1000 }), + }), +}; diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap index f0c477e627460b..9a23b3b3b23b37 100644 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap @@ -1,53 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: cli args. cluster manager with base path proxy 1`] = ` -Object { - "basePath": true, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": true, - "repl": false, - "silent": false, - "watch": false, -} -`; - -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: config. cluster manager with base path proxy 1`] = ` -Object { - "server": Object { - "autoListen": true, - }, -} -`; - -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager without base path proxy.: cluster manager without base path proxy 1`] = ` -Array [ - Array [ - Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "silent": true, - "watch": false, - }, - Object { - "server": Object { - "autoListen": true, - }, - }, - undefined, - ], -] -`; - -exports[`once LegacyService is set up with connection info creates legacy kbnServer and closes it if \`listen\` fails. 1`] = `"something failed"`; - exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` Array [ Array [ @@ -60,8 +12,6 @@ Array [ ] `; -exports[`once LegacyService is set up with connection info throws if fails to retrieve initial config. 1`] = `"something failed"`; - exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` Array [ Array [ diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 9d208445a0a1fc..cf72bb72079e9c 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -21,6 +21,14 @@ import { BehaviorSubject, throwError } from 'rxjs'; jest.mock('../../../legacy/server/kbn_server'); jest.mock('../../../cli/cluster/cluster_manager'); +jest.mock('./plugins/find_legacy_plugin_specs.ts', () => ({ + findLegacyPluginSpecs: (settings: Record) => ({ + pluginSpecs: [], + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: [], + }), +})); import { LegacyService } from '.'; // @ts-ignore: implicit any for JS file @@ -36,6 +44,8 @@ import { HttpServiceStart, BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_service'; +import { SavedObjectsServiceStart } from 'src/core/server/saved_objects/saved_objects_service'; +import { KibanaMigrator } from '../saved_objects/migrations'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -55,6 +65,7 @@ let setupDeps: { let startDeps: { core: { http: HttpServiceStart; + savedObjects: SavedObjectsServiceStart; plugins: PluginsServiceStart; }; plugins: Record; @@ -95,6 +106,9 @@ beforeEach(() => { http: { isListening: () => true, }, + savedObjects: { + migrator: {} as KibanaMigrator, + }, plugins: { contracts: new Map() }, }, plugins: {}, @@ -130,13 +144,15 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, { server: { autoListen: true } }, { setupDeps, startDeps, handledConfigPaths: ['foo.bar'], logger, - } + }, + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); const [mockKbnServer] = MockKbnServer.mock.instances; @@ -158,13 +174,15 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, { server: { autoListen: true } }, { setupDeps, startDeps, handledConfigPaths: ['foo.bar'], logger, - } + }, + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); const [mockKbnServer] = MockKbnServer.mock.instances; @@ -184,7 +202,9 @@ describe('once LegacyService is set up with connection info', () => { }); await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); + await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"something failed"` + ); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.listen).toHaveBeenCalled(); @@ -200,8 +220,12 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); + await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"something failed"` + ); + await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Legacy service is not setup yet."` + ); expect(MockKbnServer).not.toHaveBeenCalled(); expect(MockClusterManager).not.toHaveBeenCalled(); @@ -285,13 +309,15 @@ describe('once LegacyService is set up without connection info', () => { test('creates legacy kbnServer with `autoListen: false`.', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, { server: { autoListen: true } }, { setupDeps, startDeps, handledConfigPaths: ['foo.bar'], logger, - } + }, + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); }); @@ -332,9 +358,9 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); - expect(MockClusterManager.create.mock.calls).toMatchSnapshot( - 'cluster manager without base path proxy' - ); + const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; + expect(cliArgs.basePath).toBe(false); + expect(basePathProxy).not.toBeDefined(); }); test('creates ClusterManager with base path proxy.', async () => { @@ -355,9 +381,8 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { expect(MockClusterManager.create).toBeCalledTimes(1); - const [[cliArgs, config, basePathProxy]] = MockClusterManager.create.mock.calls; - expect(cliArgs).toMatchSnapshot('cli args. cluster manager with base path proxy'); - expect(config).toMatchSnapshot('config. cluster manager with base path proxy'); + const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; + expect(cliArgs.basePath).toEqual(true); expect(basePathProxy).toBeInstanceOf(BasePathProxyServer); }); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 612c46cd3bbc54..268e5f553723ff 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -18,15 +18,18 @@ */ import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; -import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators'; +import { first, map, publishReplay, tap } from 'rxjs/operators'; import { CoreService } from '../../types'; -import { InternalCoreSetup, InternalCoreStart } from '../../server'; +import { InternalCoreSetup, InternalCoreStart } from '../'; +import { SavedObjectsLegacyUiExports } from '../types'; import { Config } from '../config'; import { CoreContext } from '../core_context'; import { DevConfig, DevConfigType } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; import { Logger } from '../logging'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; +import { findLegacyPluginSpecs } from './plugins'; +import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs'; interface LegacyKbnServer { applyLoggingConfiguration: (settings: Readonly>) => void; @@ -70,13 +73,30 @@ export interface LegacyServiceStartDeps { } /** @internal */ -export class LegacyService implements CoreService { +export interface LegacyServiceSetup { + pluginSpecs: LegacyPluginSpec[]; + uiExports: SavedObjectsLegacyUiExports; + pluginExtendedConfig: Config; +} + +/** @internal */ +export class LegacyService implements CoreService { private readonly log: Logger; private readonly devConfig$: Observable; private readonly httpConfig$: Observable; private kbnServer?: LegacyKbnServer; private configSubscription?: Subscription; private setupDeps?: LegacyServiceSetupDeps; + private update$: ConnectableObservable | undefined; + private legacyRawConfig: Config | undefined; + private legacyPlugins: + | { + pluginSpecs: LegacyPluginSpec[]; + disabledPluginSpecs: LegacyPluginSpec[]; + uiExports: SavedObjectsLegacyUiExports; + } + | undefined; + private settings: Record | undefined; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('legacy-service'); @@ -87,17 +107,11 @@ export class LegacyService implements CoreService { .atPath('server') .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); } + public async setup(setupDeps: LegacyServiceSetupDeps) { this.setupDeps = setupDeps; - } - public async start(startDeps: LegacyServiceStartDeps) { - const { setupDeps } = this; - if (!setupDeps) { - throw new Error('Legacy service is not setup yet.'); - } - this.log.debug('starting legacy service'); - const update$ = this.coreContext.configService.getConfig$().pipe( + this.update$ = this.coreContext.configService.getConfig$().pipe( tap(config => { if (this.kbnServer !== undefined) { this.kbnServer.applyLoggingConfiguration(config.toRaw()); @@ -107,21 +121,66 @@ export class LegacyService implements CoreService { publishReplay(1) ) as ConnectableObservable; - this.configSubscription = update$.connect(); + this.configSubscription = this.update$.connect(); - // Receive initial config and create kbnServer/ClusterManager. - this.kbnServer = await update$ + this.settings = await this.update$ .pipe( first(), - mergeMap(async config => { - if (this.coreContext.env.isDevClusterMaster) { - await this.createClusterManager(config); - return; - } - return await this.createKbnServer(config, setupDeps, startDeps); - }) + map(config => getLegacyRawConfig(config)) ) .toPromise(); + + const { + pluginSpecs, + pluginExtendedConfig, + disabledPluginSpecs, + uiExports, + } = await findLegacyPluginSpecs(this.settings, this.coreContext.logger); + + this.legacyPlugins = { + pluginSpecs, + disabledPluginSpecs, + uiExports, + }; + + this.legacyRawConfig = pluginExtendedConfig; + + // check for unknown uiExport types + if (uiExports.unknown && uiExports.unknown.length > 0) { + throw new Error( + `Unknown uiExport types: ${uiExports.unknown + .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) + .join(', ')}` + ); + } + + return { + pluginSpecs, + uiExports, + pluginExtendedConfig, + }; + } + + public async start(startDeps: LegacyServiceStartDeps) { + const { setupDeps } = this; + if (!setupDeps || !this.legacyRawConfig || !this.legacyPlugins || !this.settings) { + throw new Error('Legacy service is not setup yet.'); + } + this.log.debug('starting legacy service'); + + // Receive initial config and create kbnServer/ClusterManager. + + if (this.coreContext.env.isDevClusterMaster) { + await this.createClusterManager(this.legacyRawConfig); + } else { + this.kbnServer = await this.createKbnServer( + this.settings, + this.legacyRawConfig, + setupDeps, + startDeps, + this.legacyPlugins + ); + } } public async stop() { @@ -151,24 +210,35 @@ export class LegacyService implements CoreService { require('../../../cli/cluster/cluster_manager').create( this.coreContext.env.cliArgs, - getLegacyRawConfig(config), + config, await basePathProxy$.toPromise() ); } private async createKbnServer( + settings: Record, config: Config, setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps + startDeps: LegacyServiceStartDeps, + legacyPlugins: { + pluginSpecs: LegacyPluginSpec[]; + disabledPluginSpecs: LegacyPluginSpec[]; + uiExports: SavedObjectsLegacyUiExports; + } ) { // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer(getLegacyRawConfig(config), { - handledConfigPaths: await this.coreContext.configService.getUsedPaths(), - setupDeps, - startDeps, - logger: this.coreContext.logger, - }); + const kbnServer: LegacyKbnServer = new KbnServer( + settings, + config, + { + handledConfigPaths: await this.coreContext.configService.getUsedPaths(), + setupDeps, + startDeps, + logger: this.coreContext.logger, + }, + legacyPlugins + ); // The kbnWorkerType check is necessary to prevent the repl // from being started multiple times in different processes. diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts new file mode 100644 index 00000000000000..f1f4da8d0b4d75 --- /dev/null +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, merge, forkJoin } from 'rxjs'; +import { toArray, tap, distinct, map } from 'rxjs/operators'; +import { + findPluginSpecs, + defaultConfig, + // @ts-ignore +} from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; +import { LoggerFactory } from '../../logging'; +import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; +import { Config } from '../../config'; + +export interface LegacyPluginPack { + getPath(): string; +} + +export interface LegacyPluginSpec { + getId: () => unknown; + getExpectedKibanaVersion: () => string; + getConfigPrefix: () => string; +} + +export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) { + const configToMutate: Config = defaultConfig(settings); + const { + pack$, + invalidDirectoryError$, + invalidPackError$, + otherError$, + deprecation$, + invalidVersionSpec$, + spec$, + disabledSpec$, + }: { + pack$: Observable; + invalidDirectoryError$: Observable<{ path: string }>; + invalidPackError$: Observable<{ path: string }>; + otherError$: Observable; + deprecation$: Observable; + invalidVersionSpec$: Observable; + spec$: Observable; + disabledSpec$: Observable; + } = findPluginSpecs(settings, configToMutate) as any; + + const logger = loggerFactory.get('legacy-plugins'); + + const log$ = merge( + pack$.pipe( + tap(definition => { + const path = definition.getPath(); + logger.debug(`Found plugin at ${path}`, { path }); + }) + ), + + invalidDirectoryError$.pipe( + tap(error => { + logger.warn(`Unable to scan directory for plugins "${error.path}"`, { + err: error, + dir: error.path, + }); + }) + ), + + invalidPackError$.pipe( + tap(error => { + logger.warn(`Skipping non-plugin directory at ${error.path}`, { + path: error.path, + }); + }) + ), + + otherError$.pipe( + tap(error => { + // rethrow unhandled errors, which will fail the server + throw error; + }) + ), + + invalidVersionSpec$.pipe( + map(spec => { + const name = spec.getId(); + const pluginVersion = spec.getExpectedKibanaVersion(); + // @ts-ignore + const kibanaVersion = settings.pkg.version; + return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; + }), + distinct(), + tap(message => { + logger.warn(message); + }) + ), + + deprecation$.pipe( + tap(({ spec, message }) => { + const deprecationLogger = loggerFactory.get( + 'plugins', + spec.getConfigPrefix(), + 'config', + 'deprecation' + ); + deprecationLogger.warn(message); + }) + ) + ); + + const [disabledPluginSpecs, pluginSpecs] = await forkJoin( + disabledSpec$.pipe(toArray()), + spec$.pipe(toArray()), + log$.pipe(toArray()) + ).toPromise(); + + return { + disabledPluginSpecs, + pluginSpecs, + pluginExtendedConfig: configToMutate, + uiExports: collectLegacyUiExports(pluginSpecs), + }; +} diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts new file mode 100644 index 00000000000000..7c69546f0c4de3 --- /dev/null +++ b/src/core/server/legacy/plugins/index.ts @@ -0,0 +1,19 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts index cde85c2600ffc3..fd35ed39092b31 100644 --- a/src/core/server/logging/index.ts +++ b/src/core/server/logging/index.ts @@ -24,4 +24,4 @@ export { LogLevel } from './log_level'; /** @internal */ export { config, LoggingConfigType } from './logging_config'; /** @internal */ -export { LoggingService } from './logging_service'; +export { LoggingService, ILoggingService } from './logging_service'; diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index d423e6b064e5fa..b5f522ca36a5f0 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -19,10 +19,9 @@ // Test helpers to simplify mocking logs and collecting all their outputs import { Logger } from './logger'; -import { LoggingService } from './logging_service'; +import { ILoggingService } from './logging_service'; import { LoggerFactory } from './logger_factory'; -type LoggingServiceContract = PublicMethodsOf; type MockedLogger = jest.Mocked; const createLoggingServiceMock = () => { @@ -36,7 +35,7 @@ const createLoggingServiceMock = () => { warn: jest.fn(), }; - const mocked: jest.Mocked = { + const mocked: jest.Mocked = { get: jest.fn(), asLoggerFactory: jest.fn(), upgrade: jest.fn(), @@ -65,7 +64,7 @@ const collectLoggingServiceMock = (loggerFactory: LoggerFactory) => { }; const clearLoggingServiceMock = (loggerFactory: LoggerFactory) => { - const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked; + const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked; mockedLoggerFactory.get.mockClear(); mockedLoggerFactory.asLoggerFactory.mockClear(); mockedLoggerFactory.upgrade.mockClear(); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index e340e769ac20e8..ada02c3b6901ad 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -24,6 +24,7 @@ import { LoggerAdapter } from './logger_adapter'; import { LoggerFactory } from './logger_factory'; import { LoggingConfigType, LoggerConfigType, LoggingConfig } from './logging_config'; +export type ILoggingService = PublicMethodsOf; /** * Service that is responsible for maintaining loggers and logger appenders. * @internal diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 1a667d6978f1a7..674f8df33ee37b 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -30,3 +30,7 @@ export { getSortedObjectsForExport, SavedObjectsExportOptions } from './export'; export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization'; export { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; + +export { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; + +export { config } from './saved_objects_config'; diff --git a/src/core/server/saved_objects/mappings/index.ts b/src/core/server/saved_objects/mappings/index.ts index 0d3bfd00c415ec..15b0736ca5f1fe 100644 --- a/src/core/server/saved_objects/mappings/index.ts +++ b/src/core/server/saved_objects/mappings/index.ts @@ -17,4 +17,10 @@ * under the License. */ export { getTypes, getProperty, getRootProperties, getRootPropertiesObjects } from './lib'; -export { FieldMapping, MappingMeta, MappingProperties, IndexMapping } from './types'; +export { + FieldMapping, + MappingMeta, + MappingProperties, + IndexMapping, + SavedObjectsMapping, +} from './types'; diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index f0ec1e4a9dda27..8bb1a69d2eb132 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -42,6 +42,11 @@ export interface MappingProperties { [field: string]: FieldMapping; } +export interface SavedObjectsMapping { + pluginId: string; + properties: MappingProperties; +} + export interface MappingMeta { // A dictionary of key -> md5 hash (e.g. 'dashboard': '24234qdfa3aefa3wa') // with each key being a root-level mapping property, and each value being diff --git a/src/core/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md index 0b62d86172a521..91249024358ac7 100644 --- a/src/core/server/saved_objects/migrations/README.md +++ b/src/core/server/saved_objects/migrations/README.md @@ -98,9 +98,19 @@ If a plugin is disbled, all of its documents are retained in the Kibana index. T Kibana index migrations expose a few config settings which might be tweaked: -* `migrations.scrollDuration` - The [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context) value used to read batches of documents from the source index. Defaults to `15m`. -* `migrations.batchSize` - The number of documents to read / transform / write at a time during index migrations -* `migrations.pollInterval` - How often, in milliseconds, secondary Kibana instances will poll to see if the primary Kibana instance has finished migrating the index. +* `migrations.scrollDuration` - The + [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context) + value used to read batches of documents from the source index. Defaults to + `15m`. +* `migrations.batchSize` - The number of documents to read / transform / write + at a time during index migrations +* `migrations.pollInterval` - How often, in milliseconds, secondary Kibana + instances will poll to see if the primary Kibana instance has finished + migrating the index. +* `migrations.skip` - Skip running migrations on startup (defaults to false). + This should only be used for running integration tests without a running + elasticsearch cluster. Note: even though migrations won't run on startup, + individual docs will still be migrated when read from ES. ## Example diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index eb6de3afc95a3e..38496a3503833c 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -20,6 +20,10 @@ import _ from 'lodash'; import { RawSavedObjectDoc } from '../../serialization'; import { DocumentMigrator } from './document_migrator'; +import { loggingServiceMock } from '../../../logging/logging_service.mock'; + +const mockLoggerFactory = loggingServiceMock.create(); +const mockLogger = mockLoggerFactory.get('mock logger'); describe('DocumentMigrator', () => { function testOpts() { @@ -27,7 +31,7 @@ describe('DocumentMigrator', () => { kibanaVersion: '25.2.3', migrations: {}, validateDoc: _.noop, - log: jest.fn(), + log: mockLogger, }; } @@ -474,7 +478,7 @@ describe('DocumentMigrator', () => { }); it('logs the document and transform that failed', () => { - const log = jest.fn(); + const log = mockLogger; const migrator = new DocumentMigrator({ ...testOpts(), migrations: { @@ -497,28 +501,26 @@ describe('DocumentMigrator', () => { expect('Did not throw').toEqual('But it should have!'); } catch (error) { expect(error.message).toMatch(/Dang diggity!/); - const warning = log.mock.calls.filter(([[level]]) => level === 'warning')[0][1]; + const warning = loggingServiceMock.collect(mockLoggerFactory).warn[0][0]; expect(warning).toContain(JSON.stringify(failedDoc)); expect(warning).toContain('dog:1.2.3'); } }); it('logs message in transform function', () => { - const logStash: string[] = []; const logTestMsg = '...said the joker to the thief'; const migrator = new DocumentMigrator({ ...testOpts(), migrations: { dog: { '1.2.3': (doc, log) => { - log!.info(logTestMsg); + log.info(logTestMsg); + log.warning(logTestMsg); return doc; }, }, }, - log: (path: string[], message: string) => { - logStash.push(message); - }, + log: mockLogger, }); const doc = { id: 'joker', @@ -527,7 +529,8 @@ describe('DocumentMigrator', () => { migrationVersion: {}, }; migrator.migrate(doc); - expect(logStash[0]).toEqual(logTestMsg); + expect(loggingServiceMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg); + expect(loggingServiceMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg); }); test('extracts the latest migration version info', () => { diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 0576f1d22199fa..563d978dcc1f1e 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -64,26 +64,26 @@ import Boom from 'boom'; import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; +import { Logger } from '../../../logging'; import { RawSavedObjectDoc } from '../../serialization'; import { SavedObjectsMigrationVersion } from '../../types'; -import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; +import { MigrationLogger, SavedObjectsMigrationLogger } from './migration_logger'; -export type TransformFn = ( - doc: RawSavedObjectDoc, - log?: SavedObjectsMigrationLogger -) => RawSavedObjectDoc; +export type TransformFn = (doc: RawSavedObjectDoc) => RawSavedObjectDoc; + +type MigrationFn = (doc: RawSavedObjectDoc, log: SavedObjectsMigrationLogger) => RawSavedObjectDoc; type ValidateDoc = (doc: RawSavedObjectDoc) => void; -interface MigrationDefinition { - [type: string]: { [version: string]: TransformFn }; +export interface MigrationDefinition { + [type: string]: { [version: string]: MigrationFn }; } interface Opts { kibanaVersion: string; migrations: MigrationDefinition; validateDoc: ValidateDoc; - log: LogFn; + log: Logger; } interface ActiveMigrations { @@ -125,7 +125,7 @@ export class DocumentMigrator implements VersionedTransformer { constructor(opts: Opts) { validateMigrationDefinition(opts.migrations); - this.migrations = buildActiveMigrations(opts.migrations, new MigrationLogger(opts.log)); + this.migrations = buildActiveMigrations(opts.migrations, opts.log); this.transformDoc = buildDocumentTransform({ kibanaVersion: opts.kibanaVersion, migrations: this.migrations, @@ -207,10 +207,7 @@ function validateMigrationDefinition(migrations: MigrationDefinition) { * From: { type: { version: fn } } * To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } } */ -function buildActiveMigrations( - migrations: MigrationDefinition, - log: SavedObjectsMigrationLogger -): ActiveMigrations { +function buildActiveMigrations(migrations: MigrationDefinition, log: Logger): ActiveMigrations { return _.mapValues(migrations, (versions, prop) => { const transforms = Object.entries(versions) .map(([version, transform]) => ({ @@ -299,15 +296,10 @@ function markAsUpToDate(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { * If a specific transform function fails, this tacks on a bit of information * about the document and transform that caused the failure. */ -function wrapWithTry( - version: string, - prop: string, - transform: TransformFn, - log: SavedObjectsMigrationLogger -) { +function wrapWithTry(version: string, prop: string, transform: MigrationFn, log: Logger) { return function tryTransformDoc(doc: RawSavedObjectDoc) { try { - const result = transform(doc, log); + const result = transform(doc, new MigrationLogger(log)); // A basic sanity check to help migration authors detect basic errors // (e.g. forgetting to return the transformed doc) @@ -319,7 +311,7 @@ function wrapWithTry( } catch (error) { const failedTransform = `${prop}:${version}`; const failedDoc = JSON.stringify(doc); - log.warning( + log.warn( `Failed to transform document ${doc}. Transform: ${failedTransform}\nDoc: ${failedDoc}` ); throw error; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 2f9c5d1a08672a..2fc65f6e475d87 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -21,6 +21,7 @@ import _ from 'lodash'; import { SavedObjectsSchema } from '../../schema'; import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; import { IndexMigrator } from './index_migrator'; +import { loggingServiceMock } from '../../../logging/logging_service.mock'; describe('IndexMigrator', () => { let testOpts: any; @@ -30,7 +31,7 @@ describe('IndexMigrator', () => { batchSize: 10, callCluster: jest.fn(), index: '.kibana', - log: jest.fn(), + log: loggingServiceMock.create().get(), mappingProperties: {}, pollInterval: 1, scrollDuration: '1m', diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index 633bccf8aceecb..d4e97ee6c57471 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -24,13 +24,14 @@ * serves as a central blueprint for what migrations will end up doing. */ +import { Logger } from 'src/core/server/logging'; import { SavedObjectsSerializer } from '../../serialization'; import { MappingProperties } from '../../mappings'; import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; import { fetchInfo, FullIndexInfo } from './elastic_index'; -import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; +import { SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; export interface MigrationOpts { batchSize: number; @@ -38,7 +39,7 @@ export interface MigrationOpts { scrollDuration: string; callCluster: CallCluster; index: string; - log: LogFn; + log: Logger; mappingProperties: MappingProperties; documentMigrator: VersionedTransformer; serializer: SavedObjectsSerializer; @@ -71,8 +72,7 @@ export interface Context { * and various info needed to migrate the source index. */ export async function migrationContext(opts: MigrationOpts): Promise { - const { callCluster } = opts; - const log = new MigrationLogger(opts.log); + const { log, callCluster } = opts; const alias = opts.index; const source = createSourceContext(await fetchInfo(callCluster, alias), alias); const dest = createDestContext(source, alias, opts.mappingProperties); @@ -82,7 +82,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { alias, source, dest, - log, + log: new MigrationLogger(log), batchSize: opts.batchSize, documentMigrator: opts.documentMigrator, pollInterval: opts.pollInterval, diff --git a/src/core/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts index 9c98b7d85a8d82..7c61d0c48d9bd8 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -17,6 +17,8 @@ * under the License. */ +import { Logger } from 'src/core/server/logging'; + /* * This file provides a helper class for ensuring that all logging * in the migration system is done in a fairly uniform way. @@ -32,13 +34,13 @@ export interface SavedObjectsMigrationLogger { } export class MigrationLogger implements SavedObjectsMigrationLogger { - private log: LogFn; + private logger: Logger; - constructor(log: LogFn) { - this.log = log; + constructor(log: Logger) { + this.logger = log; } - public info = (msg: string) => this.log(['info', 'migrations'], msg); - public debug = (msg: string) => this.log(['debug', 'migrations'], msg); - public warning = (msg: string) => this.log(['warning', 'migrations'], msg); + public info = (msg: string) => this.logger.info(msg); + public debug = (msg: string) => this.logger.debug(msg); + public warning = (msg: string) => this.logger.warn(msg); } diff --git a/src/core/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts index 7a5c16bbe4af04..6cb7ecac92ab9e 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { KibanaMigrator } from './kibana'; +export { KibanaMigrator, IKibanaMigrator } from './kibana'; diff --git a/src/core/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts index ed48f3f4893de6..25772c4c9b0b16 100644 --- a/src/core/server/saved_objects/migrations/kibana/index.ts +++ b/src/core/server/saved_objects/migrations/kibana/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { KibanaMigrator } from './kibana_migrator'; +export { KibanaMigrator, IKibanaMigrator } from './kibana_migrator'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts new file mode 100644 index 00000000000000..ca732f4f150282 --- /dev/null +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaMigrator, mergeProperties } from './kibana_migrator'; +import { buildActiveMappings } from '../core'; +import { SavedObjectsMapping } from '../../mappings'; + +const createMigrator = ( + { + savedObjectMappings, + }: { + savedObjectMappings: SavedObjectsMapping[]; + } = { savedObjectMappings: [] } +) => { + const mockMigrator: jest.Mocked> = { + runMigrations: jest.fn(), + getActiveMappings: jest.fn(), + migrateDocument: jest.fn(), + }; + + mockMigrator.getActiveMappings.mockReturnValue( + buildActiveMappings({ properties: mergeProperties(savedObjectMappings) }) + ); + mockMigrator.migrateDocument.mockImplementation(doc => doc); + return mockMigrator; +}; + +export const mockKibanaMigrator = { + create: createMigrator, +}; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 9fc8afd356043c..51551ae4887b51 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -18,13 +18,14 @@ */ import _ from 'lodash'; -import { KbnServer, KibanaMigrator } from './kibana_migrator'; +import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; +import { loggingServiceMock } from '../../../logging/logging_service.mock'; describe('KibanaMigrator', () => { describe('getActiveMappings', () => { it('returns full index mappings w/ core properties', () => { - const { kbnServer } = mockKbnServer(); - kbnServer.uiExports.savedObjectMappings = [ + const options = mockOptions(); + options.savedObjectMappings = [ { pluginId: 'aaa', properties: { amap: { type: 'text' } }, @@ -34,13 +35,13 @@ describe('KibanaMigrator', () => { properties: { bmap: { type: 'text' } }, }, ]; - const mappings = new KibanaMigrator({ kbnServer }).getActiveMappings(); + const mappings = new KibanaMigrator(options).getActiveMappings(); expect(mappings).toMatchSnapshot(); }); it('Fails if duplicate mappings are defined', () => { - const { kbnServer } = mockKbnServer(); - kbnServer.uiExports.savedObjectMappings = [ + const options = mockOptions(); + options.savedObjectMappings = [ { pluginId: 'aaa', properties: { amap: { type: 'text' } }, @@ -50,56 +51,27 @@ describe('KibanaMigrator', () => { properties: { amap: { type: 'long' } }, }, ]; - expect(() => new KibanaMigrator({ kbnServer }).getActiveMappings()).toThrow( + expect(() => new KibanaMigrator(options).getActiveMappings()).toThrow( /Plugin bbb is attempting to redefine mapping "amap"/ ); }); }); - describe('awaitMigration', () => { - it('changes isMigrated to true if migrations were skipped', async () => { - const { kbnServer } = mockKbnServer(); - kbnServer.server.plugins.elasticsearch = undefined; - const result = await new KibanaMigrator({ kbnServer }).awaitMigration(); + describe('runMigrations', () => { + it('resolves isMigrated if migrations were skipped', async () => { + const skipMigrations = true; + const result = await new KibanaMigrator(mockOptions()).runMigrations(skipMigrations); expect(result).toEqual([{ status: 'skipped' }, { status: 'skipped' }]); }); - it('waits for kbnServer.ready and elasticsearch.ready before attempting migrations', async () => { - const { kbnServer } = mockKbnServer(); + it('only runs migrations once if called multiple times', async () => { + const options = mockOptions(); const clusterStub = jest.fn(() => ({ status: 404 })); - const waitUntilReady = jest.fn(async () => undefined); - kbnServer.server.plugins.elasticsearch = { - waitUntilReady, - getCluster() { - expect(kbnServer.ready as any).toHaveBeenCalledTimes(1); - expect(waitUntilReady).toHaveBeenCalledTimes(1); - - return { - callWithInternalUser: clusterStub, - }; - }, - }; - - const migrationResults = await new KibanaMigrator({ kbnServer }).awaitMigration(); - expect(migrationResults.length).toEqual(2); - }); - - it('only handles and deletes index templates once', async () => { - const { kbnServer } = mockKbnServer(); - const clusterStub = jest.fn(() => ({ status: 404 })); - const waitUntilReady = jest.fn(async () => undefined); - - kbnServer.server.plugins.elasticsearch = { - waitUntilReady, - getCluster() { - return { - callWithInternalUser: clusterStub, - }; - }, - }; - - await new KibanaMigrator({ kbnServer }).awaitMigration(); + options.callCluster = clusterStub; + const migrator = new KibanaMigrator(options); + await migrator.runMigrations(); + await migrator.runMigrations(); // callCluster with "cat.templates" is called by "deleteIndexTemplates" function // and should only be done once @@ -111,75 +83,60 @@ describe('KibanaMigrator', () => { }); }); -function mockKbnServer({ configValues }: { configValues?: any } = {}) { +function mockOptions({ configValues }: { configValues?: any } = {}): KibanaMigratorOptions { const callCluster = jest.fn(); - const kbnServer: KbnServer = { - version: '8.2.3', - ready: jest.fn(async () => undefined), - uiExports: { - savedObjectsManagement: {}, - savedObjectValidations: {}, - savedObjectMigrations: {}, - savedObjectMappings: [ - { - pluginId: 'testtype', - properties: { - testtype: { - properties: { - name: { type: 'keyword' }, - }, + return { + logger: loggingServiceMock.create().get(), + kibanaVersion: '8.2.3', + savedObjectValidations: {}, + savedObjectMigrations: {}, + savedObjectMappings: [ + { + pluginId: 'testtype', + properties: { + testtype: { + properties: { + name: { type: 'keyword' }, }, }, }, - { - pluginId: 'testtype2', - properties: { - testtype2: { - properties: { - name: { type: 'keyword' }, - }, + }, + { + pluginId: 'testtype2', + properties: { + testtype2: { + properties: { + name: { type: 'keyword' }, }, }, }, - ], - savedObjectSchemas: { - testtype2: { - isNamespaceAgnostic: false, - indexPattern: 'other-index', - }, }, - }, - server: { - config: () => ({ - get: ((name: string) => { - if (configValues && configValues[name]) { - return configValues[name]; - } - switch (name) { - case 'kibana.index': - return '.my-index'; - case 'migrations.batchSize': - return 20; - case 'migrations.pollInterval': - return 20000; - case 'migrations.scrollDuration': - return '10m'; - default: - throw new Error(`Unexpected config ${name}`); - } - }) as any, - }), - log: _.noop as any, - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: callCluster, - }), - waitUntilReady: async () => undefined, - }, + ], + savedObjectSchemas: { + testtype2: { + isNamespaceAgnostic: false, + indexPattern: 'other-index', }, }, + kibanaConfig: { + enabled: true, + index: '.my-index', + } as KibanaMigratorOptions['kibanaConfig'], + savedObjectsConfig: { + batchSize: 20, + pollInterval: 20000, + scrollDuration: '10m', + skip: false, + }, + config: { + get: (name: string) => { + if (configValues && configValues[name]) { + return configValues[name]; + } else { + throw new Error(`Unexpected config ${name}`); + } + }, + } as KibanaMigratorOptions['config'], + callCluster, }; - - return { kbnServer, callCluster }; } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 78a8507e0c41d9..5bde5deec93820 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -22,81 +22,112 @@ * (the shape of the mappings and documents in the index). */ -import { once } from 'lodash'; -import { MappingProperties } from '../../mappings'; +import { Logger } from 'src/core/server/logging'; +import { KibanaConfigType } from 'src/core/server/kibana_config'; +import { MappingProperties, SavedObjectsMapping, IndexMapping } from '../../mappings'; import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from '../../schema'; -import { SavedObjectsManagementDefinition } from '../../management'; import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; -import { docValidator } from '../../validation'; -import { buildActiveMappings, CallCluster, IndexMigrator, LogFn } from '../core'; -import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; +import { docValidator, PropertyValidators } from '../../validation'; +import { buildActiveMappings, CallCluster, IndexMigrator } from '../core'; +import { + DocumentMigrator, + VersionedTransformer, + MigrationDefinition, +} from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; +import { SavedObjectsConfigType } from '../../saved_objects_config'; import { Config } from '../../../config'; -export interface KbnServer { - server: Server; - version: string; - ready: () => Promise; - uiExports: { - savedObjectMappings: any[]; - savedObjectMigrations: any; - savedObjectValidations: any; - savedObjectSchemas: SavedObjectsSchemaDefinition; - savedObjectsManagement: SavedObjectsManagementDefinition; - }; -} -interface Server { - log: LogFn; - config: () => { - get: { - (path: 'kibana.index' | 'migrations.scrollDuration'): string; - (path: 'migrations.batchSize' | 'migrations.pollInterval'): number; - }; - }; - plugins: { elasticsearch: ElasticsearchPlugin | undefined }; +export interface KibanaMigratorOptions { + callCluster: CallCluster; + config: Config; + savedObjectsConfig: SavedObjectsConfigType; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; + logger: Logger; + savedObjectMappings: SavedObjectsMapping[]; + savedObjectMigrations: MigrationDefinition; + savedObjectSchemas: SavedObjectsSchemaDefinition; + savedObjectValidations: PropertyValidators; } -interface ElasticsearchPlugin { - getCluster: (name: 'admin') => { callWithInternalUser: CallCluster }; - waitUntilReady: () => Promise; -} +export type IKibanaMigrator = Pick; /** * Manages the shape of mappings and documents in the Kibana index. - * - * @export - * @class KibanaMigrator */ export class KibanaMigrator { + private readonly callCluster: CallCluster; + private readonly config: Config; + private readonly savedObjectsConfig: SavedObjectsConfigType; + private readonly documentMigrator: VersionedTransformer; + private readonly kibanaConfig: KibanaConfigType; + private readonly log: Logger; + private readonly mappingProperties: MappingProperties; + private readonly schema: SavedObjectsSchema; + private readonly serializer: SavedObjectsSerializer; + private migrationResult?: Promise>; + + /** + * Creates an instance of KibanaMigrator. + */ + constructor({ + callCluster, + config, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, + logger, + savedObjectMappings, + savedObjectMigrations, + savedObjectSchemas, + savedObjectValidations, + }: KibanaMigratorOptions) { + this.config = config; + this.callCluster = callCluster; + this.kibanaConfig = kibanaConfig; + this.savedObjectsConfig = savedObjectsConfig; + this.schema = new SavedObjectsSchema(savedObjectSchemas); + this.serializer = new SavedObjectsSerializer(this.schema); + this.mappingProperties = mergeProperties(savedObjectMappings || []); + this.log = logger; + this.documentMigrator = new DocumentMigrator({ + kibanaVersion, + migrations: savedObjectMigrations || {}, + validateDoc: docValidator(savedObjectValidations || {}), + log: this.log, + }); + } + /** * Migrates the mappings and documents in the Kibana index. This will run only * once and subsequent calls will return the result of the original call. * - * @returns - * @memberof KibanaMigrator + * @returns - A promise which resolves once all migrations have been applied. + * The promise resolves with an array of migration statuses, one for each + * elasticsearch index which was migrated. */ - public awaitMigration = once(async () => { - const { server } = this.kbnServer; + public runMigrations(skipMigrations: boolean = false): Promise> { + if (this.migrationResult === undefined) { + this.migrationResult = this.runMigrationsInternal(skipMigrations); + } - // Wait until the plugins have been found an initialized... - await this.kbnServer.ready(); + return this.migrationResult; + } - // We can't do anything if the elasticsearch plugin has been disabled. - if (!server.plugins.elasticsearch) { - server.log( - ['warning', 'migration'], - 'The elasticsearch plugin is disabled. Skipping migrations.' + private runMigrationsInternal(skipMigrations: boolean) { + if (skipMigrations) { + this.log.warn( + 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' + ); + return Promise.resolve( + Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })) ); - return Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })); } - // Wait until elasticsearch is green... - await server.plugins.elasticsearch.waitUntilReady(); - - const config = server.config() as Config; - const kibanaIndexName = config.get('kibana.index'); + const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ - config, + config: this.config, kibanaIndexName, indexMap: this.mappingProperties, schema: this.schema, @@ -104,14 +135,14 @@ export class KibanaMigrator { const migrators = Object.keys(indexMap).map(index => { return new IndexMigrator({ - batchSize: config.get('migrations.batchSize'), - callCluster: server.plugins.elasticsearch!.getCluster('admin').callWithInternalUser, + batchSize: this.savedObjectsConfig.batchSize, + callCluster: this.callCluster, documentMigrator: this.documentMigrator, index, log: this.log, mappingProperties: indexMap[index].typeMappings, - pollInterval: config.get('migrations.pollInterval'), - scrollDuration: config.get('migrations.scrollDuration'), + pollInterval: this.savedObjectsConfig.pollInterval, + scrollDuration: this.savedObjectsConfig.scrollDuration, serializer: this.serializer, // Only necessary for the migrator of the kibana index. obsoleteIndexTemplatePattern: @@ -120,61 +151,22 @@ export class KibanaMigrator { }); }); - if (migrators.length === 0) { - throw new Error(`Migrations failed to run, no mappings found or Kibana is not "ready".`); - } - return Promise.all(migrators.map(migrator => migrator.migrate())); - }); - - private kbnServer: KbnServer; - private documentMigrator: VersionedTransformer; - private mappingProperties: MappingProperties; - private log: LogFn; - private serializer: SavedObjectsSerializer; - private readonly schema: SavedObjectsSchema; - - /** - * Creates an instance of KibanaMigrator. - * - * @param opts - * @prop {KbnServer} kbnServer - An instance of the Kibana server object. - * @memberof KibanaMigrator - */ - constructor({ kbnServer }: { kbnServer: KbnServer }) { - this.kbnServer = kbnServer; - - this.schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas); - this.serializer = new SavedObjectsSerializer(this.schema); - - this.mappingProperties = mergeProperties(kbnServer.uiExports.savedObjectMappings || []); - - this.log = (meta: string[], message: string) => kbnServer.server.log(meta, message); - - this.documentMigrator = new DocumentMigrator({ - kibanaVersion: kbnServer.version, - migrations: kbnServer.uiExports.savedObjectMigrations || {}, - validateDoc: docValidator(kbnServer.uiExports.savedObjectValidations || {}), - log: this.log, - }); } /** * Gets all the index mappings defined by Kibana's enabled plugins. * - * @returns - * @memberof KibanaMigrator */ - public getActiveMappings() { + public getActiveMappings(): IndexMapping { return buildActiveMappings({ properties: this.mappingProperties }); } /** * Migrates an individual doc to the latest version, as defined by the plugin migrations. * - * @param {RawSavedObjectDoc} doc - * @returns {RawSavedObjectDoc} - * @memberof KibanaMigrator + * @param doc - The saved object to migrate + * @returns `doc` with all registered migrations applied. */ public migrateDocument(doc: RawSavedObjectDoc): RawSavedObjectDoc { return this.documentMigrator.migrate(doc); @@ -185,7 +177,7 @@ export class KibanaMigrator { * Merges savedObjectMappings properties into a single object, verifying that * no mappings are redefined. */ -function mergeProperties(mappings: any[]): MappingProperties { +export function mergeProperties(mappings: SavedObjectsMapping[]): MappingProperties { return mappings.reduce((acc, { pluginId, properties }) => { const duplicate = Object.keys(properties).find(k => acc.hasOwnProperty(k)); if (duplicate) { diff --git a/src/legacy/ui/ui_exports/ui_exports_mixin.js b/src/core/server/saved_objects/saved_objects_config.ts similarity index 63% rename from src/legacy/ui/ui_exports/ui_exports_mixin.js rename to src/core/server/saved_objects/saved_objects_config.ts index ea2a07f3b265e0..7217cde55d0611 100644 --- a/src/legacy/ui/ui_exports/ui_exports_mixin.js +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -17,22 +17,16 @@ * under the License. */ -import { collectUiExports } from './collect_ui_exports'; +import { schema, TypeOf } from '@kbn/config-schema'; -export function uiExportsMixin(kbnServer) { - kbnServer.uiExports = collectUiExports( - kbnServer.pluginSpecs - ); +export type SavedObjectsConfigType = TypeOf; - // check for unknown uiExport types - const { unknown = [] } = kbnServer.uiExports; - if (!unknown.length) { - return; - } - - throw new Error(`Unknown uiExport types: ${ - unknown - .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) - .join(', ') - }`); -} +export const config = { + path: 'migrations', + schema: schema.object({ + batchSize: schema.number({ defaultValue: 100 }), + scrollDuration: schema.string({ defaultValue: '15m' }), + pollInterval: schema.number({ defaultValue: 1500 }), + skip: schema.boolean({ defaultValue: false }), + }), +}; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts new file mode 100644 index 00000000000000..5561031d820ec0 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; +import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; + +type SavedObjectsServiceContract = PublicMethodsOf; + +const createStartContractMock = () => { + const startContract: jest.Mocked = { + migrator: mockKibanaMigrator.create(), + }; + + return startContract; +}; + +const createsavedObjectsServiceMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockResolvedValue({}); + mocked.start.mockResolvedValue(createStartContractMock()); + mocked.stop.mockResolvedValue(); + return mocked; +}; + +export const savedObjectsServiceMock = { + create: createsavedObjectsServiceMock, + createStartContract: createStartContractMock, +}; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts new file mode 100644 index 00000000000000..c13be579c04bb7 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('./migrations/kibana/kibana_migrator'); + +import { SavedObjectsService, SavedObjectsSetupDeps } from './saved_objects_service'; +import { mockCoreContext } from '../core_context.mock'; +import { KibanaMigrator } from './migrations/kibana/kibana_migrator'; +import { of } from 'rxjs'; +import elasticsearch from 'elasticsearch'; +import { Env } from '../config'; +import { configServiceMock } from '../mocks'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('SavedObjectsService', () => { + describe('#setup()', () => { + it('creates a KibanaMigrator which retries NoConnections errors from callAsInternalUser', async () => { + const coreContext = mockCoreContext.create(); + let i = 0; + const clusterClient = { + callAsInternalUser: jest + .fn() + .mockImplementation(() => + i++ <= 2 + ? Promise.reject(new elasticsearch.errors.NoConnections()) + : Promise.resolve('success') + ), + }; + + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of(clusterClient) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + + return expect((KibanaMigrator as jest.Mock).mock.calls[0][0].callCluster()).resolves.toMatch( + 'success' + ); + }); + }); + + describe('#start()', () => { + it('skips KibanaMigrator migrations when --optimize=true', async () => { + const coreContext = mockCoreContext.create({ + env: ({ cliArgs: { optimize: true }, packageInfo: { version: 'x.x.x' } } as unknown) as Env, + }); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; + await soService.start({}); + expect(migrator.runMigrations).toHaveBeenCalledWith(true); + }); + + it('skips KibanaMigrator migrations when migrations.skip=true', async () => { + const configService = configServiceMock.create({ atPath: { skip: true } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; + await soService.start({}); + expect(migrator.runMigrations).toHaveBeenCalledWith(true); + }); + + it('resolves with KibanaMigrator after waiting for migrations to complete', async () => { + const configService = configServiceMock.create({ atPath: { skip: false } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; + expect(migrator.runMigrations).toHaveBeenCalledTimes(0); + const startContract = await soService.start({}); + expect(startContract.migrator).toBeInstanceOf(KibanaMigrator); + expect(migrator.runMigrations).toHaveBeenCalledWith(false); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts new file mode 100644 index 00000000000000..ebcea8dc3b2750 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -0,0 +1,126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreService } from 'src/core/types'; +import { first } from 'rxjs/operators'; +import { KibanaMigrator, IKibanaMigrator } from './migrations'; +import { CoreContext } from '../core_context'; +import { LegacyServiceSetup } from '../legacy/legacy_service'; +import { ElasticsearchServiceSetup } from '../elasticsearch'; +import { KibanaConfigType } from '../kibana_config'; +import { retryCallCluster } from '../elasticsearch/retry_call_cluster'; +import { SavedObjectsConfigType } from './saved_objects_config'; +import { Logger } from '..'; + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SavedObjectsServiceSetup {} + +/** + * @public + */ +export interface SavedObjectsServiceStart { + migrator: IKibanaMigrator; +} + +/** @internal */ +export interface SavedObjectsSetupDeps { + legacy: LegacyServiceSetup; + elasticsearch: ElasticsearchServiceSetup; +} + +/** @internal */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SavedObjectsStartDeps {} + +export class SavedObjectsService + implements CoreService { + private migrator: KibanaMigrator | undefined; + logger: Logger; + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('savedobjects-service'); + } + + public async setup(coreSetup: SavedObjectsSetupDeps) { + this.logger.debug('Setting up SavedObjects service'); + + const { + savedObjectSchemas, + savedObjectMappings, + savedObjectMigrations, + savedObjectValidations, + } = await coreSetup.legacy.uiExports; + + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(first()).toPromise(); + + const kibanaConfig = await this.coreContext.configService + .atPath('kibana') + .pipe(first()) + .toPromise(); + + const savedObjectsConfig = await this.coreContext.configService + .atPath('migrations') + .pipe(first()) + .toPromise(); + + this.migrator = new KibanaMigrator({ + savedObjectSchemas, + savedObjectMappings, + savedObjectMigrations, + savedObjectValidations, + logger: this.coreContext.logger.get('migrations'), + kibanaVersion: this.coreContext.env.packageInfo.version, + config: coreSetup.legacy.pluginExtendedConfig, + savedObjectsConfig, + kibanaConfig, + callCluster: retryCallCluster(adminClient.callAsInternalUser), + }); + + return ({} as any) as Promise; + } + + public async start(core: SavedObjectsStartDeps): Promise { + this.logger.debug('Starting SavedObjects service'); + + /** + * Note: We want to ensure that migrations have completed before + * continuing with further Core startup steps that might use SavedObjects + * such as running the legacy server, legacy plugins and allowing incoming + * HTTP requests. + * + * However, our build system optimize step and some tests depend on the + * HTTP server running without an Elasticsearch server being available. + * So, when the `migrations.skip` is true, we skip migrations altogether. + */ + const cliArgs = this.coreContext.env.cliArgs; + const savedObjectsConfig = await this.coreContext.configService + .atPath('migrations') + .pipe(first()) + .toPromise(); + const skipMigrations = cliArgs.optimize || savedObjectsConfig.skip; + await this.migrator!.runMigrations(skipMigrations); + + return { migrator: this.migrator! }; + } + + public async stop() {} +} diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 09676fb5040124..06d29bf7dcf326 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -26,10 +26,12 @@ interface SavedObjectsSchemaTypeDefinition { convertToAliasScript?: string; } +/** @internal */ export interface SavedObjectsSchemaDefinition { [key: string]: SavedObjectsSchemaTypeDefinition; } +/** @internal */ export class SavedObjectsSchema { private readonly definition?: SavedObjectsSchemaDefinition; constructor(schemaDefinition?: SavedObjectsSchemaDefinition) { diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index bd875db2001f4e..217ffe7129e94e 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -77,6 +77,7 @@ function assertNonEmptyString(value: string, name: string) { } } +/** @internal */ export class SavedObjectsSerializer { private readonly schema: SavedObjectsSchema; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 386539e755d9a7..685ce51bc7d29c 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -26,9 +26,10 @@ import { SavedObjectsSchema } from '../schema'; import { SavedObjectsResolveImportErrorsOptions } from '../import/types'; /** - * @public + * @internal + * @deprecated */ -export interface SavedObjectsService { +export interface SavedObjectsLegacyService { // ATTENTION: these types are incomplete addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider< Request diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 5a2e6a617fbb5d..c35502b719d58c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -263,7 +263,7 @@ describe('SavedObjectsRepository', () => { onBeforeWrite = jest.fn(); migrator = { migrateDocument: jest.fn(doc => doc), - awaitMigration: async () => ({ status: 'skipped' }), + runMigrations: async () => ({ status: 'skipped' }), }; const serializer = new SavedObjectsSerializer(schema); @@ -297,7 +297,7 @@ describe('SavedObjectsRepository', () => { }); it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -313,7 +313,7 @@ describe('SavedObjectsRepository', () => { } ) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('formats Elasticsearch response', async () => { @@ -552,7 +552,7 @@ describe('SavedObjectsRepository', () => { describe('#bulkCreate', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); callAdminCluster.mockReturnValue({ @@ -576,7 +576,7 @@ describe('SavedObjectsRepository', () => { ]) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('formats Elasticsearch request', async () => { @@ -998,7 +998,7 @@ describe('SavedObjectsRepository', () => { describe('#delete', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); callAdminCluster.mockReturnValue({ result: 'deleted' }); @@ -1008,7 +1008,7 @@ describe('SavedObjectsRepository', () => { }) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('throws notFound when ES is unable to find the document', async () => { @@ -1114,14 +1114,14 @@ describe('SavedObjectsRepository', () => { describe('#find', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); callAdminCluster.mockReturnValue(noNamespaceSearchResults); await expect(savedObjectsRepository.find({ type: 'foo' })).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('requires type to be defined', async () => { @@ -1315,7 +1315,7 @@ describe('SavedObjectsRepository', () => { }; it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -1324,7 +1324,7 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository.get('index-pattern', 'logstash-*') ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('formats Elasticsearch response when there is no namespace', async () => { @@ -1408,7 +1408,7 @@ describe('SavedObjectsRepository', () => { describe('#bulkGet', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -1421,7 +1421,7 @@ describe('SavedObjectsRepository', () => { ]) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('prepends type to id when getting objects when there is no namespace', async () => { @@ -1662,7 +1662,7 @@ describe('SavedObjectsRepository', () => { }); it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -1672,7 +1672,7 @@ describe('SavedObjectsRepository', () => { }) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveReturnedTimes(1); + expect(migrator.runMigrations).toHaveReturnedTimes(1); }); it('mockReturnValue current ES document _seq_no and _primary_term encoded as version', async () => { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e93d9e4047501e..3c2a644f003bda 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -125,7 +125,7 @@ export class SavedObjectsRepository { this._onBeforeWrite = onBeforeWrite; this._unwrappedCallCluster = async (...args: Parameters) => { - await migrator.awaitMigration(); + await migrator.runMigrations(); return callCluster(...args); }; this._schema = schema; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a7e8f5fd4ac7ca..1cc424199b8872 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -18,6 +18,10 @@ */ import { SavedObjectsClient } from './service/saved_objects_client'; +import { SavedObjectsMapping } from './mappings'; +import { MigrationDefinition } from './migrations/core/document_migrator'; +import { SavedObjectsSchemaDefinition } from './schema'; +import { PropertyValidators } from './validation'; /** * Information about the migrations that have been applied to this SavedObject. @@ -201,3 +205,15 @@ export interface SavedObjectsBaseOptions { * @public */ export type SavedObjectsClientContract = Pick; + +/** + * @internal + * @deprecated + */ +export interface SavedObjectsLegacyUiExports { + unknown: [{ pluginSpec: { getId: () => unknown }; type: unknown }] | undefined; + savedObjectMappings: SavedObjectsMapping[]; + savedObjectMigrations: MigrationDefinition; + savedObjectSchemas: SavedObjectsSchemaDefinition; + savedObjectValidations: PropertyValidators; +} diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0dc1ed50564837..4ae1c0c267ea94 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -10,6 +10,8 @@ import { ConfigOptions } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { IncomingHttpHeaders } from 'http'; +import { KibanaConfigType } from 'src/core/server/kibana_config'; +import { Logger as Logger_2 } from 'src/core/server/logging'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { PeerCertificate } from 'tls'; @@ -299,8 +301,12 @@ export interface InternalCoreSetup { http: HttpServiceSetup; } -// @public (undocumented) +// @internal (undocumented) export interface InternalCoreStart { + // Warning: (ae-forgotten-export) The symbol "SavedObjectsServiceStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + savedObjects: SavedObjectsServiceStart; } // @public @@ -396,6 +402,8 @@ export interface LegacyServiceSetupDeps { // @public @deprecated (undocumented) export interface LegacyServiceStartDeps { + // Warning: (ae-incompatible-release-tags) The symbol "core" is marked as @public, but its signature references "InternalCoreStart" which is marked as @internal + // // (undocumented) core: InternalCoreStart & { plugins: PluginsServiceStart; @@ -955,6 +963,31 @@ export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } +// @internal @deprecated (undocumented) +export interface SavedObjectsLegacyService { + // Warning: (ae-forgotten-export) The symbol "ScopedSavedObjectsClientProvider" needs to be exported by the entry point index.d.ts + // + // (undocumented) + addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider['addClientWrapperFactory']; + // (undocumented) + getSavedObjectsRepository(...rest: any[]): any; + // (undocumented) + getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; + // (undocumented) + importExport: { + objectLimit: number; + importSavedObjects(options: SavedObjectsImportOptions): Promise; + resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; + getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; + }; + // (undocumented) + SavedObjectsClient: typeof SavedObjectsClient; + // (undocumented) + schema: SavedObjectsSchema; + // (undocumented) + types: string[]; +} + // @public (undocumented) export interface SavedObjectsMigrationLogger { // (undocumented) @@ -1003,9 +1036,7 @@ export interface SavedObjectsResolveImportErrorsOptions { supportedTypes: string[]; } -// Warning: (ae-missing-release-tag) "SavedObjectsSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @internal (undocumented) export class SavedObjectsSchema { // Warning: (ae-forgotten-export) The symbol "SavedObjectsSchemaDefinition" needs to be exported by the entry point index.d.ts constructor(schemaDefinition?: SavedObjectsSchemaDefinition); @@ -1019,9 +1050,7 @@ export class SavedObjectsSchema { isNamespaceAgnostic(type: string): boolean; } -// Warning: (ae-missing-release-tag) "SavedObjectsSerializer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @internal (undocumented) export class SavedObjectsSerializer { constructor(schema: SavedObjectsSchema); generateRawId(namespace: string | undefined, type: string, id?: string): string; @@ -1031,33 +1060,6 @@ export class SavedObjectsSerializer { savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): SavedObjectsRawDoc; } -// @public (undocumented) -export interface SavedObjectsService { - // Warning: (ae-forgotten-export) The symbol "ScopedSavedObjectsClientProvider" needs to be exported by the entry point index.d.ts - // - // (undocumented) - addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider['addClientWrapperFactory']; - // (undocumented) - getSavedObjectsRepository(...rest: any[]): any; - // (undocumented) - getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; - // (undocumented) - importExport: { - objectLimit: number; - importSavedObjects(options: SavedObjectsImportOptions): Promise; - resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; - getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; - }; - // Warning: (ae-incompatible-release-tags) The symbol "SavedObjectsClient" is marked as @public, but its signature references "SavedObjectsClient" which is marked as @internal - // - // (undocumented) - SavedObjectsClient: typeof SavedObjectsClient; - // (undocumented) - schema: SavedObjectsSchema; - // (undocumented) - types: string[]; -} - // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { // (undocumented) diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 694888ab6243eb..cb1a88f6e8aed8 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -23,6 +23,7 @@ import { mockLegacyService, mockPluginsService, mockConfigService, + mockSavedObjectsService, } from './index.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -51,6 +52,7 @@ test('sets up services on "setup"', async () => { expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -58,6 +60,7 @@ test('sets up services on "setup"', async () => { expect(mockElasticsearchService.setup).toHaveBeenCalledTimes(1); expect(mockPluginsService.setup).toHaveBeenCalledTimes(1); expect(mockLegacyService.setup).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); }); test('runs services on "start"', async () => { @@ -70,10 +73,12 @@ test('runs services on "start"', async () => { expect(mockHttpService.start).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); await server.start(); expect(mockHttpService.start).toHaveBeenCalledTimes(1); expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -93,6 +98,7 @@ test('stops services on "stop"', async () => { expect(mockElasticsearchService.stop).not.toHaveBeenCalled(); expect(mockPluginsService.stop).not.toHaveBeenCalled(); expect(mockLegacyService.stop).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -100,6 +106,7 @@ test('stops services on "stop"', async () => { expect(mockElasticsearchService.stop).toHaveBeenCalledTimes(1); expect(mockPluginsService.stop).toHaveBeenCalledTimes(1); expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e0569ed80fca4a..2b63d6ac3be1c9 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -26,11 +26,14 @@ import { HttpService, HttpServiceSetup } from './http'; import { LegacyService } from './legacy'; import { Logger, LoggerFactory } from './logging'; import { PluginsService, config as pluginsConfig } from './plugins'; +import { SavedObjectsService } from '../server/saved_objects'; import { config as elasticsearchConfig } from './elasticsearch'; import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; +import { config as kibanaConfig } from './kibana_config'; +import { config as savedObjectsConfig } from './saved_objects'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; import { InternalCoreSetup } from './index'; @@ -42,9 +45,10 @@ export class Server { private readonly context: ContextService; private readonly elasticsearch: ElasticsearchService; private readonly http: HttpService; - private readonly plugins: PluginsService; private readonly legacy: LegacyService; private readonly log: Logger; + private readonly plugins: PluginsService; + private readonly savedObjects: SavedObjectsService; constructor( readonly config$: Observable, @@ -60,6 +64,7 @@ export class Server { this.plugins = new PluginsService(core); this.legacy = new LegacyService(core); this.elasticsearch = new ElasticsearchService(core); + this.savedObjects = new SavedObjectsService(core); } public async setup() { @@ -88,18 +93,26 @@ export class Server { this.registerCoreContext(coreSetup); const pluginsSetup = await this.plugins.setup(coreSetup); - await this.legacy.setup({ + const legacySetup = await this.legacy.setup({ core: { ...coreSetup, plugins: pluginsSetup }, plugins: mapToObject(pluginsSetup.contracts), }); + await this.savedObjects.setup({ + elasticsearch: elasticsearchServiceSetup, + legacy: legacySetup, + }); + return coreSetup; } public async start() { + this.log.debug('starting server'); const pluginsStart = await this.plugins.start({}); + const savedObjectsStart = await this.savedObjects.start({}); const coreStart = { + savedObjects: savedObjectsStart, plugins: pluginsStart, }; @@ -109,6 +122,7 @@ export class Server { }); await this.http.start(); + return coreStart; } @@ -117,6 +131,7 @@ export class Server { await this.legacy.stop(); await this.plugins.stop(); + await this.savedObjects.stop(); await this.elasticsearch.stop(); await this.http.stop(); } @@ -148,6 +163,8 @@ export class Server { [httpConfig.path, httpConfig.schema], [pluginsConfig.path, pluginsConfig.schema], [devConfig.path, devConfig.schema], + [kibanaConfig.path, kibanaConfig.schema], + [savedObjectsConfig.path, savedObjectsConfig.schema], ]; for (const [path, schema] of schemas) { diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index b751ca2f1864b0..dc916e11d698c0 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -77,39 +77,41 @@ export async function deleteKibanaIndices({ client, stats, log }) { */ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { const uiExports = await getUiExports(kibanaPluginIds); - const version = await loadElasticVersion(); + const kibanaVersion = await loadKibanaVersion(); + const config = { - 'kibana.index': '.kibana', - 'migrations.scrollDuration': '5m', - 'migrations.batchSize': 100, - 'migrations.pollInterval': 100, 'xpack.task_manager.index': '.kibana_task_manager', }; - const ready = async () => undefined; - const elasticsearch = { - getCluster: () => ({ - callWithInternalUser: (path, ...args) => _.get(client, path).call(client, ...args), - }), - waitUntilReady: ready, - }; - - const server = { - log: ([logType, messageType], ...args) => log[logType](`[${messageType}] ${args.join(' ')}`), - config: () => ({ get: path => config[path] }), - plugins: { elasticsearch }, - }; - const kbnServer = { - server, - version, - uiExports, - ready, + const migratorOptions = { + config: { get: path => config[path] }, + savedObjectsConfig: { + 'scrollDuration': '5m', + 'batchSize': 100, + 'pollInterval': 100, + }, + kibanaConfig: { + index: '.kibana', + }, + logger: { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + }, + version: kibanaVersion, + savedObjectSchemas: uiExports.savedObjectSchemas, + savedObjectMappings: uiExports.savedObjectMappings, + savedObjectMigrations: uiExports.savedObjectMigrations, + savedObjectValidations: uiExports.savedObjectValidations, + callCluster: (path, ...args) => _.get(client, path).call(client, ...args), }; - return await new KibanaMigrator({ kbnServer }).awaitMigration(); + return await new KibanaMigrator(migratorOptions).runMigrations(); } -async function loadElasticVersion() { +async function loadKibanaVersion() { const readFile = promisify(fs.readFile); const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); return JSON.parse(packageJson).version; diff --git a/src/legacy/plugin_discovery/find_plugin_specs.js b/src/legacy/plugin_discovery/find_plugin_specs.js index 29434357558b10..faccdf396df043 100644 --- a/src/legacy/plugin_discovery/find_plugin_specs.js +++ b/src/legacy/plugin_discovery/find_plugin_specs.js @@ -39,7 +39,7 @@ import { isInvalidPackError, } from './errors'; -function defaultConfig(settings) { +export function defaultConfig(settings) { return Config.withDefaultSchema( transformDeprecations(settings) ); diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js index 3627fafa9d39a7..17da5ffca12421 100644 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js @@ -103,7 +103,10 @@ export class PluginSpec { throw createInvalidPluginError(this, 'plugin.publicDir must be an absolute path'); } if (basename(this._publicDir) !== 'public') { - throw createInvalidPluginError(this, `publicDir for plugin ${this.getId()} must end with a "public" directory.`); + throw createInvalidPluginError( + this, + `publicDir for plugin ${this.getId()} must end with a "public" directory.` + ); } } } @@ -145,7 +148,9 @@ export class PluginSpec { // the version of kibana down to the patch level. If these two versions need // to diverge, they can specify a kibana.version in the package to indicate the // version of kibana the plugin is intended to work with. - return this._kibanaVersion || get(this.getPack().getPkg(), 'kibana.version') || this.getVersion(); + return ( + this._kibanaVersion || get(this.getPack().getPkg(), 'kibana.version') || this.getVersion() + ); } isVersionCompatible(actualKibanaVersion) { diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js index ecaa6fa22f54c9..db5c0e96654928 100644 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ b/src/legacy/server/config/__tests__/deprecation_warnings.js @@ -25,7 +25,7 @@ const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startu const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); const SECOND = 1000; -describe('config/deprecation warnings mixin', function () { +describe('config/deprecation warnings', function () { this.timeout(15 * SECOND); let stdio = ''; @@ -99,11 +99,11 @@ describe('config/deprecation warnings mixin', function () { } }) .filter(Boolean) - .filter(line => ( + .filter(line => line.type === 'log' && line.tags.includes('deprecation') && line.tags.includes('warning') - )); + ); expect(deprecationLines).to.have.length(1); expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 2265952e9245d1..919653bc941f46 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -154,12 +154,6 @@ export default () => Joi.object({ data: Joi.string().default(getData()) }).default(), - migrations: Joi.object({ - batchSize: Joi.number().default(100), - scrollDuration: Joi.string().default('15m'), - pollInterval: Joi.number().default(1500), - }).default(), - stats: Joi.object({ maximumWaitTimeForAllCollectorsInS: Joi.number().default(60) }).default(), diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 502c1f285e50da..406697ab65d8f6 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -26,7 +26,7 @@ import { ElasticsearchServiceSetup, LoggerFactory, SavedObjectsClientContract, - SavedObjectsService, + SavedObjectsLegacyService, } from '../../core/server'; import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/server/'; @@ -62,7 +62,7 @@ declare module 'hapi' { interface Server { config: () => KibanaConfig; indexPatternsServiceFactory: IndexPatternsServiceFactory; - savedObjects: SavedObjectsService; + savedObjects: SavedObjectsLegacyService; usage: { collectorSet: any }; injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void; getHiddenUiAppById(appId: string): UiApp; @@ -127,4 +127,4 @@ export { Server, Request, ResponseToolkit } from 'hapi'; // Re-export commonly accessed api types. export { IndexPatternsService } from './index_patterns'; -export { SavedObjectsService, SavedObjectsClient } from 'src/core/server'; +export { SavedObjectsLegacyService, SavedObjectsClient } from 'src/core/server'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 779fd183ab593e..754b5da5258397 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -24,7 +24,6 @@ import { isWorker } from 'cluster'; import { fromRoot, pkg } from '../utils'; import { Config } from './config'; import loggingConfiguration from './logging/configuration'; -import configSetupMixin from './config/setup'; import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; @@ -50,14 +49,16 @@ import { i18nMixin } from './i18n'; const rootDir = fromRoot('.'); export default class KbnServer { - constructor(settings, core) { + constructor(settings, config, core, legacyPlugins) { this.name = pkg.name; this.version = pkg.version; this.build = pkg.build || false; this.rootDir = rootDir; this.settings = settings || {}; + this.config = config; const { setupDeps, startDeps, handledConfigPaths, logger } = core; + this.newPlatform = { coreContext: { logger, @@ -70,12 +71,13 @@ export default class KbnServer { }, }; + this.uiExports = legacyPlugins.uiExports; + this.pluginSpecs = legacyPlugins.pluginSpecs; + this.disabledPluginSpecs = legacyPlugins.disabledPluginSpecs; + this.ready = constant(this.mixin( Plugins.waitForInitSetupMixin, - // sets this.config, reads this.settings - configSetupMixin, - // sets this.server httpMixin, @@ -101,7 +103,7 @@ export default class KbnServer { // tell the config we are done loading plugins configCompleteMixin, - // setup this.uiExports and this.uiBundles + // setup this.uiBundles uiMixin, indexPatternsMixin, @@ -161,8 +163,6 @@ export default class KbnServer { const { server, config } = this; - await server.kibanaMigrator.awaitMigration(); - if (isWorker) { // help parent process know when we are ready process.send(['WORKER_LISTENING']); diff --git a/src/legacy/server/plugins/scan_mixin.js b/src/legacy/server/plugins/scan_mixin.js index 5e21207482e9da..8deeeb191f7023 100644 --- a/src/legacy/server/plugins/scan_mixin.js +++ b/src/legacy/server/plugins/scan_mixin.js @@ -16,96 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - -import * as Rx from 'rxjs'; -import { map, distinct, toArray, tap } from 'rxjs/operators'; -import { findPluginSpecs } from '../../plugin_discovery'; - import { Plugin } from './lib'; -export async function scanMixin(kbnServer, server, config) { - const { - pack$, - invalidDirectoryError$, - invalidPackError$, - otherError$, - deprecation$, - invalidVersionSpec$, - spec$, - disabledSpec$, - } = findPluginSpecs(kbnServer.settings, config); - - const logging$ = Rx.merge( - pack$.pipe( - tap(definition => { - const path = definition.getPath(); - server.logWithMetadata(['plugin', 'debug'], `Found plugin at ${path}`, { - path - }); - }) - ), - - invalidDirectoryError$.pipe( - tap(error => { - server.logWithMetadata(['plugin', 'warning'], `${error.code}: Unable to scan directory for plugins "${error.path}"`, { - err: error, - dir: error.path - }); - }) - ), - - invalidPackError$.pipe( - tap(error => { - server.logWithMetadata(['plugin', 'warning'], `Skipping non-plugin directory at ${error.path}`, { - path: error.path - }); - }) - ), - - otherError$.pipe( - tap(error => { - // rethrow unhandled errors, which will fail the server - throw error; - }) - ), - - invalidVersionSpec$.pipe( - map(spec => { - const name = spec.getId(); - const pluginVersion = spec.getExpectedKibanaVersion(); - const kibanaVersion = config.get('pkg.version'); - return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; - }), - distinct(), - tap(message => { - server.log(['plugin', 'warning'], message); - }) - ), - - deprecation$.pipe( - tap(({ spec, message }) => { - server.log(['warning', spec.getConfigPrefix(), 'config', 'deprecation'], message); - }) - ) - ); - - const enabledSpecs$ = spec$.pipe( - toArray(), - tap(specs => { - kbnServer.pluginSpecs = specs; - }) - ); - - const disabledSpecs$ = disabledSpec$.pipe( - toArray(), - tap(specs => { - kbnServer.disabledPluginSpecs = specs; - }) - ); - - // await completion of enabledSpecs$, disabledSpecs$, and logging$ - await Rx.merge(logging$, enabledSpecs$, disabledSpecs$).toPromise(); - +export async function scanMixin(kbnServer) { kbnServer.plugins = kbnServer.pluginSpecs.map(spec => ( new Plugin(kbnServer, spec) )); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 924bea15590731..edaa2850064228 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -20,7 +20,6 @@ // Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete /* eslint-disable @kbn/eslint/no-restricted-paths */ -import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { SavedObjectsSchema } from '../../../core/server/saved_objects/schema'; import { SavedObjectsSerializer } from '../../../core/server/saved_objects/serialization'; import { @@ -58,7 +57,7 @@ function getImportableAndExportableTypes({ kbnServer, visibleTypes }) { } export function savedObjectsMixin(kbnServer, server) { - const migrator = new KibanaMigrator({ kbnServer }); + const migrator = kbnServer.newPlatform.start.core.savedObjects.migrator; const mappings = migrator.getActiveMappings(); const allTypes = Object.keys(getRootPropertiesObjects(mappings)); const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas); diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 3bf1aa8139cc2a..cdbc642485706b 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -18,6 +18,48 @@ */ import { savedObjectsMixin } from './saved_objects_mixin'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { mockKibanaMigrator } from '../../../core/server/saved_objects/migrations/kibana/kibana_migrator.mock'; + +const savedObjectMappings = [ + { + pluginId: 'testtype', + properties: { + testtype: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, + { + pluginId: 'testtype2', + properties: { + doc1: { + properties: { + name: { type: 'keyword' }, + }, + }, + doc2: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, + { + pluginId: 'secretPlugin', + properties: { + hiddentype: { + properties: { + secret: { type: 'keyword' }, + }, + }, + }, + }, +]; + +const migrator = mockKibanaMigrator.create({ savedObjectMappings }); describe('Saved Objects Mixin', () => { let mockKbnServer; @@ -55,6 +97,9 @@ describe('Saved Objects Mixin', () => { }, }; mockKbnServer = { + newPlatform: { + start: { core: { savedObjects: { migrator } } }, + }, server: mockServer, ready: () => {}, pluginSpecs: { @@ -63,6 +108,7 @@ describe('Saved Objects Mixin', () => { }, }, uiExports: { + savedObjectMappings, savedObjectSchemas: { hiddentype: { hidden: true, @@ -71,43 +117,6 @@ describe('Saved Objects Mixin', () => { indexPattern: 'other-index', }, }, - savedObjectMappings: [ - { - pluginId: 'testtype', - properties: { - testtype: { - properties: { - name: { type: 'keyword' }, - }, - }, - }, - }, - { - pluginId: 'testtype2', - properties: { - doc1: { - properties: { - name: { type: 'keyword' }, - }, - }, - doc2: { - properties: { - name: { type: 'keyword' }, - }, - }, - }, - }, - { - pluginId: 'secretPlugin', - properties: { - hiddentype: { - properties: { - secret: { type: 'keyword' }, - }, - }, - }, - }, - ], }, }; }); @@ -290,7 +299,7 @@ describe('Saved Objects Mixin', () => { }); it('should call underlining callCluster', async () => { - stubCallCluster.mockImplementation(method => { + mockCallCluster.mockImplementation(method => { if (method === 'indices.get') { return { status: 404 }; } else if (method === 'indices.getAlias') { @@ -301,7 +310,7 @@ describe('Saved Objects Mixin', () => { }); const client = await service.getScopedSavedObjectsClient(); await client.create('testtype'); - expect(stubCallCluster).toHaveBeenCalled(); + expect(mockCallCluster).toHaveBeenCalled(); }); }); diff --git a/src/legacy/ui/ui_exports/collect_ui_exports.js b/src/legacy/ui/ui_exports/collect_ui_exports.ts similarity index 74% rename from src/legacy/ui/ui_exports/collect_ui_exports.js rename to src/legacy/ui/ui_exports/collect_ui_exports.ts index 3fda38b3e31a37..bfb290ffb36151 100644 --- a/src/legacy/ui/ui_exports/collect_ui_exports.js +++ b/src/legacy/ui/ui_exports/collect_ui_exports.ts @@ -17,14 +17,15 @@ * under the License. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SavedObjectsLegacyUiExports } from 'src/core/server/types'; +// @ts-ignore import { UI_EXPORT_DEFAULTS } from './ui_export_defaults'; +// @ts-ignore import * as uiExportTypeReducers from './ui_export_types'; +// @ts-ignore import { reduceExportSpecs } from '../../plugin_discovery'; -export function collectUiExports(pluginSpecs) { - return reduceExportSpecs( - pluginSpecs, - uiExportTypeReducers, - UI_EXPORT_DEFAULTS - ); +export function collectUiExports(pluginSpecs: unknown[]): SavedObjectsLegacyUiExports { + return reduceExportSpecs(pluginSpecs, uiExportTypeReducers, UI_EXPORT_DEFAULTS); } diff --git a/src/legacy/ui/ui_exports/index.js b/src/legacy/ui/ui_exports/index.js index f74553c70e0bf1..56db698dc7b037 100644 --- a/src/legacy/ui/ui_exports/index.js +++ b/src/legacy/ui/ui_exports/index.js @@ -18,4 +18,3 @@ */ export { collectUiExports } from './collect_ui_exports'; -export { uiExportsMixin } from './ui_exports_mixin'; diff --git a/src/legacy/ui/ui_mixin.js b/src/legacy/ui/ui_mixin.js index d49a729cf27017..f5a25937c3a755 100644 --- a/src/legacy/ui/ui_mixin.js +++ b/src/legacy/ui/ui_mixin.js @@ -17,7 +17,6 @@ * under the License. */ -import { uiExportsMixin } from './ui_exports'; import { fieldFormatsMixin } from './field_formats'; import { tutorialsMixin } from './tutorials_mixin'; import { uiAppsMixin } from './ui_apps'; @@ -27,7 +26,6 @@ import { uiRenderMixin } from './ui_render'; import { uiSettingsMixin } from './ui_settings'; export async function uiMixin(kbnServer) { - await kbnServer.mixin(uiExportsMixin); await kbnServer.mixin(uiAppsMixin); await kbnServer.mixin(uiBundlesMixin); await kbnServer.mixin(uiSettingsMixin); diff --git a/tasks/config/run.js b/tasks/config/run.js index a28eab17e280e3..ea5a4b01dc8a52 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -60,6 +60,7 @@ module.exports = function (grunt) { '--plugins.initialize=false', '--optimize.bundleFilter=tests', '--server.port=5610', + '--migrations.skip=true' ]; const NODE = 'node'; diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index 03b0baf1da92bd..5c492951ec9384 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -285,7 +285,7 @@ async function migrateIndex({ callCluster, index, migrations, mappingProperties, obsoleteIndexTemplatePattern, mappingProperties, batchSize: 10, - log: _.noop, + log: { info: _.noop, debug: _.noop, warn: _.noop }, pollInterval: 50, scrollDuration: '5m', serializer: new SavedObjectsSerializer(new SavedObjectsSchema()), diff --git a/test/tsconfig.json b/test/tsconfig.json index 26d69347df5a93..276238adf59013 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -14,6 +14,7 @@ "**/*.ts", "**/*.tsx", "../typings/lodash.topath/*.ts", + "typings/**/*", ], "exclude": [ "plugin_functional/plugins/**/*" diff --git a/src/legacy/server/config/setup.js b/test/typings/index.d.ts similarity index 75% rename from src/legacy/server/config/setup.js rename to test/typings/index.d.ts index 2164438a2fa55c..ba43e7e7184e5a 100644 --- a/src/legacy/server/config/setup.js +++ b/test/typings/index.d.ts @@ -17,10 +17,8 @@ * under the License. */ -import { Config } from './config'; -import { transformDeprecations } from './transform_deprecations'; +type MethodKeysOf = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; +}[keyof T]; -export default function (kbnServer) { - const settings = transformDeprecations(kbnServer.settings); - kbnServer.config = Config.withDefaultSchema(settings); -} +type PublicMethodsOf = Pick>; diff --git a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts index 442cef6e3bd944..1bac3f17806449 100644 --- a/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ b/x-pack/legacy/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts @@ -5,7 +5,7 @@ */ import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { SavedObjectsService } from 'src/legacy/server/kbn_server'; +import { SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; import { callWithInternalUserFactory } from '../../client/call_with_internal_user_factory'; export interface MlTelemetry { @@ -30,7 +30,7 @@ export function createMlTelemetry(count: number = 0): MlTelemetry { // savedObjects export function storeMlTelemetry( elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsService, + savedObjects: SavedObjectsLegacyService, mlTelemetry: MlTelemetry ): void { const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); @@ -42,7 +42,7 @@ export function storeMlTelemetry( // needs savedObjects and elasticsearchPlugin export function getSavedObjectsClient( elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsService + savedObjects: SavedObjectsLegacyService ): any { const { SavedObjectsClient, getSavedObjectsRepository } = savedObjects; const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); @@ -52,7 +52,7 @@ export function getSavedObjectsClient( export async function incrementFileDataVisualizerIndexCreationCount( elasticsearchPlugin: ElasticsearchPlugin, - savedObjects: SavedObjectsService + savedObjects: SavedObjectsLegacyService ): Promise { const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index 47d9f5328ef67d..5cca26a290acae 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -7,7 +7,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { ServerRoute } from 'hapi'; -import { KibanaConfig, SavedObjectsService } from 'src/legacy/server/kbn_server'; +import { KibanaConfig, SavedObjectsLegacyService } from 'src/legacy/server/kbn_server'; import { HttpServiceSetup, Logger, PluginInitializerContext } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; @@ -73,7 +73,7 @@ export interface MlCoreSetup { addAppLinksToSampleDataset: () => any; injectUiAppVars: (id: string, callback: () => {}) => any; http: MlHttpServiceSetup; - savedObjects: SavedObjectsService; + savedObjects: SavedObjectsLegacyService; usage: { collectorSet: { makeUsageCollector: any; @@ -99,7 +99,7 @@ export interface RouteInitialization { elasticsearchPlugin: ElasticsearchPlugin; route(route: ServerRoute | ServerRoute[]): void; xpackMainPlugin?: MlXpackMainPlugin; - savedObjects?: SavedObjectsService; + savedObjects?: SavedObjectsLegacyService; spacesPlugin: any; } export interface UsageInitialization { @@ -110,7 +110,7 @@ export interface UsageInitialization { register: (collector: any) => void; }; }; - savedObjects: SavedObjectsService; + savedObjects: SavedObjectsLegacyService; } export class Plugin { diff --git a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 3128165d78c7e1..10e427a29e4429 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsSchema, - SavedObjectsService, + SavedObjectsLegacyService, SavedObjectsClientContract, SavedObjectsImportResponse, SavedObjectsImportOptions, @@ -42,7 +42,7 @@ describe('copySavedObjectsToSpaces', () => { const setup = (setupOpts: SetupOpts) => { const savedObjectsClient = (null as unknown) as SavedObjectsClientContract; - const savedObjectsService: SavedObjectsService = ({ + const savedObjectsService: SavedObjectsLegacyService = ({ importExport: { objectLimit: 1000, getSortedObjectsForExport: @@ -73,7 +73,7 @@ describe('copySavedObjectsToSpaces', () => { schema: new SavedObjectsSchema({ globalType: { isNamespaceAgnostic: true }, }), - } as unknown) as SavedObjectsService; + } as unknown) as SavedObjectsLegacyService; return { savedObjectsClient, diff --git a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 40acf8fc32cba1..608d57d8736870 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectsService, SavedObject } from 'src/core/server'; +import { + SavedObjectsClientContract, + SavedObjectsLegacyService, + SavedObject, +} from 'src/core/server'; import { Readable } from 'stream'; import { SavedObjectsClientProviderOptions } from 'src/core/server'; import { spaceIdToNamespace } from '../utils/namespace'; @@ -20,7 +24,7 @@ export const COPY_TO_SPACES_SAVED_OBJECTS_CLIENT_OPTS: SavedObjectsClientProvide export function copySavedObjectsToSpacesFactory( savedObjectsClient: SavedObjectsClientContract, - savedObjectsService: SavedObjectsService + savedObjectsService: SavedObjectsLegacyService ) { const { importExport, types, schema } = savedObjectsService; const eligibleTypes = getEligibleTypes({ types, schema }); diff --git a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts index 203f41860ba3ea..76bb374f9eb6d4 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/lib/get_eligible_types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsService } from 'src/core/server'; +import { SavedObjectsLegacyService } from 'src/core/server'; -export function getEligibleTypes({ types, schema }: Pick) { +export function getEligibleTypes({ + types, + schema, +}: Pick) { return types.filter(type => !schema.isNamespaceAgnostic(type)); } diff --git a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 8fa0d03283f9c9..fbafb18699081e 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsSchema, - SavedObjectsService, + SavedObjectsLegacyService, SavedObjectsClientContract, SavedObjectsImportResponse, SavedObjectsResolveImportErrorsOptions, @@ -42,7 +42,7 @@ const expectStreamToContainObjects = async ( describe('resolveCopySavedObjectsToSpacesConflicts', () => { const setup = (setupOpts: SetupOpts) => { - const savedObjectsService: SavedObjectsService = ({ + const savedObjectsService: SavedObjectsLegacyService = ({ importExport: { objectLimit: 1000, getSortedObjectsForExport: @@ -76,7 +76,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { schema: new SavedObjectsSchema({ globalType: { isNamespaceAgnostic: true }, }), - } as unknown) as SavedObjectsService; + } as unknown) as SavedObjectsLegacyService; const savedObjectsClient = (null as unknown) as SavedObjectsClientContract; diff --git a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index 38c7b068d57298..d7c602c28b253c 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectsService, SavedObject } from 'src/core/server'; +import { + SavedObjectsClientContract, + SavedObjectsLegacyService, + SavedObject, +} from 'src/core/server'; import { Readable } from 'stream'; import { spaceIdToNamespace } from '../utils/namespace'; import { CopyOptions, ResolveConflictsOptions, CopyResponse } from './types'; @@ -15,7 +19,7 @@ import { createReadableStreamFromArray } from './lib/readable_stream_from_array' export function resolveCopySavedObjectsToSpacesConflictsFactory( savedObjectsClient: SavedObjectsClientContract, - savedObjectsService: SavedObjectsService + savedObjectsService: SavedObjectsLegacyService ) { const { importExport, types, schema } = savedObjectsService; const eligibleTypes = getEligibleTypes({ types, schema }); diff --git a/x-pack/legacy/plugins/spaces/server/lib/create_default_space.test.ts b/x-pack/legacy/plugins/spaces/server/lib/create_default_space.test.ts index 0476bf9ba929b6..cd0ecdea97fb2c 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/create_default_space.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/create_default_space.test.ts @@ -10,7 +10,7 @@ import * as Rx from 'rxjs'; import Boom from 'boom'; import { getClient } from '../../../../server/lib/get_client_shield'; import { createDefaultSpace } from './create_default_space'; -import { SavedObjectsService } from 'src/core/server'; +import { SavedObjectsLegacyService } from 'src/core/server'; import { ElasticsearchServiceSetup } from 'src/core/server'; let mockCallWithRequest; @@ -83,7 +83,7 @@ const createMockDeps = (settings: MockServerSettings = {}) => { return { config: mockServer.config(), - savedObjects: (mockServer.savedObjects as unknown) as SavedObjectsService, + savedObjects: (mockServer.savedObjects as unknown) as SavedObjectsLegacyService, elasticsearch: ({ dataClient$: Rx.of({ callAsInternalUser: jest.fn(), diff --git a/x-pack/legacy/plugins/spaces/server/lib/create_default_space.ts b/x-pack/legacy/plugins/spaces/server/lib/create_default_space.ts index bde9a5132182b8..9e574c19c987f1 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/create_default_space.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/create_default_space.ts @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import { SavedObjectsService, CoreSetup } from 'src/core/server'; +import { SavedObjectsLegacyService, CoreSetup } from 'src/core/server'; import { DEFAULT_SPACE_ID } from '../../common/constants'; interface Deps { elasticsearch: CoreSetup['elasticsearch']; - savedObjects: SavedObjectsService; + savedObjects: SavedObjectsLegacyService; } export async function createDefaultSpace({ elasticsearch, savedObjects }: Deps) { diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 06aed92b4edaf9..dfd4d586554bba 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -12,7 +12,7 @@ import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; import { HttpServiceSetup, CoreSetup, - SavedObjectsService, + SavedObjectsLegacyService, SavedObjectsErrorHelpers, } from '../../../../../../../src/core/server'; import { @@ -168,7 +168,7 @@ describe('onPostAuthInterceptor', () => { serverDefaultRoute: defaultRoute, serverBasePath: '', }, - savedObjects: (savedObjectsService as unknown) as SavedObjectsService, + savedObjects: (savedObjectsService as unknown) as SavedObjectsLegacyService, } as LegacyAPI; const service = new SpacesService(loggingMock, () => legacyAPI); diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts b/x-pack/legacy/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts index eed72ec744e0f7..725b4cdc6bbad9 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/spaces_tutorial_context_factory.test.ts @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { createSpacesTutorialContextFactory } from './spaces_tutorial_context_factory'; import { SpacesService } from '../new_platform/spaces_service'; -import { SavedObjectsService } from 'src/core/server'; +import { SavedObjectsLegacyService } from 'src/core/server'; import { SpacesAuditLogger } from './audit_logger'; import { elasticsearchServiceMock, coreMock } from '../../../../../../src/core/server/mocks'; import { spacesServiceMock } from '../new_platform/spaces_service/spaces_service.mock'; @@ -29,7 +29,7 @@ const legacyAPI: LegacyAPI = { legacyConfig: { serverBasePath: '/foo', }, - savedObjects: {} as SavedObjectsService, + savedObjects: {} as SavedObjectsLegacyService, } as LegacyAPI; const service = new SpacesService(log, () => legacyAPI); diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts index 382227a4f1cec7..2bd9edadc52ef0 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts @@ -5,7 +5,7 @@ */ import { Observable } from 'rxjs'; -import { SavedObjectsService, CoreSetup } from 'src/core/server'; +import { SavedObjectsLegacyService, CoreSetup } from 'src/core/server'; import { Logger, PluginInitializerContext } from 'src/core/server'; import { CapabilitiesModifier } from 'src/legacy/server/capabilities'; import { Legacy } from 'kibana'; @@ -36,7 +36,7 @@ import { initSpacesRequestInterceptors } from '../lib/request_interceptors'; * to function properly. */ export interface LegacyAPI { - savedObjects: SavedObjectsService; + savedObjects: SavedObjectsLegacyService; usage: { collectorSet: { register: (collector: any) => void; diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts index 809c118b8368d8..3200c90bca2be0 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts @@ -7,7 +7,11 @@ import * as Rx from 'rxjs'; import { SpacesService } from './spaces_service'; import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; import { SpacesAuditLogger } from '../../lib/audit_logger'; -import { KibanaRequest, SavedObjectsService, SavedObjectsErrorHelpers } from 'src/core/server'; +import { + KibanaRequest, + SavedObjectsLegacyService, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { getSpaceIdFromPath } from '../../lib/spaces_url_parser'; import { createOptionalPlugin } from '../../../../../server/lib/optional_plugin'; @@ -53,7 +57,7 @@ const createService = async (serverBasePath: string = '') => { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); }), }), - } as unknown) as SavedObjectsService, + } as unknown) as SavedObjectsLegacyService, } as LegacyAPI; const spacesService = new SpacesService(mockLogger, () => legacyAPI); diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts index d84c79c3d78b7d..13667555a9468f 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -9,7 +9,7 @@ import { Server } from 'hapi'; import { Legacy } from 'kibana'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchServiceMock, coreMock } from 'src/core/server/mocks'; -import { SavedObjectsSchema, SavedObjectsService } from 'src/core/server'; +import { SavedObjectsSchema, SavedObjectsLegacyService } from 'src/core/server'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from 'src/legacy/utils/streams'; import { createOptionalPlugin } from '../../../../../../server/lib/optional_plugin'; @@ -44,13 +44,19 @@ export interface RequestRunnerResult { server: any; mockSavedObjectsRepository: any; mockSavedObjectsService: { - getScopedSavedObjectsClient: jest.Mock; + getScopedSavedObjectsClient: jest.Mock< + SavedObjectsLegacyService['getScopedSavedObjectsClient'] + >; importExport: { getSortedObjectsForExport: jest.Mock< - SavedObjectsService['importExport']['getSortedObjectsForExport'] + SavedObjectsLegacyService['importExport']['getSortedObjectsForExport'] + >; + importSavedObjects: jest.Mock< + SavedObjectsLegacyService['importExport']['importSavedObjects'] + >; + resolveImportErrors: jest.Mock< + SavedObjectsLegacyService['importExport']['resolveImportErrors'] >; - importSavedObjects: jest.Mock; - resolveImportErrors: jest.Mock; }; }; headers: Record; diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/external/index.ts b/x-pack/legacy/plugins/spaces/server/routes/api/external/index.ts index 72ddc193e5c9f3..7828a6b59d5667 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/api/external/index.ts @@ -5,7 +5,7 @@ */ import { Legacy } from 'kibana'; -import { Logger, SavedObjectsService } from 'src/core/server'; +import { Logger, SavedObjectsLegacyService } from 'src/core/server'; import { XPackMainPlugin } from '../../../../../xpack_main/xpack_main'; import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; import { initDeleteSpacesApi } from './delete'; @@ -20,7 +20,7 @@ type Omit = Pick>; interface RouteDeps { xpackMain: XPackMainPlugin; legacyRouter: Legacy.Server['route']; - savedObjects: SavedObjectsService; + savedObjects: SavedObjectsLegacyService; spacesService: SpacesServiceSetup; log: Logger; } diff --git a/x-pack/legacy/plugins/task_manager/index.ts b/x-pack/legacy/plugins/task_manager/index.ts index 832c859e6d67cf..192c8a309b0ea6 100644 --- a/x-pack/legacy/plugins/task_manager/index.ts +++ b/x-pack/legacy/plugins/task_manager/index.ts @@ -73,7 +73,7 @@ export function taskManager(kibana: any) { // executing. Saved objects repository waits for migrations to finish before // finishing the request. To avoid this, we'll await within a separate // function block. - await this.kbnServer.server.kibanaMigrator.awaitMigration(); + await this.kbnServer.server.kibanaMigrator.runMigrations(); plugin.start(); })(); }); From 69bbd1199d1f20071a764c000dda8029ca035b68 Mon Sep 17 00:00:00 2001 From: Artyom Gospodarsky Date: Tue, 1 Oct 2019 11:35:26 +0300 Subject: [PATCH 09/53] URL field formatter fix (#46332) * Fix url render relative hyperlinks * Fix test cases * Make prefix less errorable * Add some tests for url formatter --- .../field_formats/types/__tests__/url.js | 24 +++++++++++++++++++ .../kibana/common/field_formats/types/url.js | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js index 678a533fa8efcb..ff847bea4bee63 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js @@ -189,5 +189,29 @@ describe('UrlFormat', function () { expect(converter('../foo/bar', null, null, parsedUrl)) .to.be('../foo/bar'); }); + + it('should support multiple types of urls w/o basePath', function () { + const url = new UrlFormat(); + const parsedUrl = { + origin: 'http://kibana.host.com', + pathname: '/app/kibana', + }; + const converter = url.getConverterFor('html'); + + expect(converter('10.22.55.66', null, null, parsedUrl)) + .to.be('10.22.55.66'); + + expect(converter('http://www.domain.name/app/kibana#/dashboard/', null, null, parsedUrl)) + .to.be('http://www.domain.name/app/kibana#/dashboard/'); + + expect(converter('/app/kibana', null, null, parsedUrl)) + .to.be('/app/kibana'); + + expect(converter('kibana#/dashboard/', null, null, parsedUrl)) + .to.be('kibana#/dashboard/'); + + expect(converter('#/dashboard/', null, null, parsedUrl)) + .to.be('#/dashboard/'); + }); }); }); diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/url.js b/src/legacy/core_plugins/kibana/common/field_formats/types/url.js index 4171e511f27f5d..63924727ab8bbf 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/url.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/url.js @@ -157,7 +157,9 @@ export function createUrlFormat(FieldFormat) { } // Handle urls like: `../app/kibana` else { - prefix = `${parsedUrl.origin}${parsedUrl.basePath}/app/`; + const prefixEnd = url[0] === '/' ? '' : '/'; + + prefix = `${parsedUrl.origin}${parsedUrl.basePath || ''}/app${prefixEnd}`; } } From 4ac0201260534a25140e29987e3cc683c6f5f648 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Tue, 1 Oct 2019 11:37:41 +0300 Subject: [PATCH 10/53] [Vis: Default Editor] 'Metrics & Axis' tab - unit tests (#45881) * Add unit tests for ValueAxisOptions * Add unit tests for ValueAxesPanel * Add unit tests for index.tsx * Update index.test.tsx * Update y_extents.test.tsx.snap * Rename folder * Update snapshots * Fix code review comments * Move data to mocks.ts * Replace mount to shallow * Update snapshots * Move tests to the same directory as source code files --- .../category_axis_panel.test.tsx.snap | 109 ++++ .../__snapshots__/chart_options.test.tsx.snap | 78 +++ .../custom_extents_options.test.tsx.snap | 43 ++ .../__snapshots__/index.test.tsx.snap | 354 ++++++++++++ .../__snapshots__/label_options.test.tsx.snap | 75 +++ .../__snapshots__/line_options.test.tsx.snap | 67 +++ .../value_axes_panel.test.tsx.snap | 502 ++++++++++++++++++ .../value_axis_options.test.tsx.snap | 415 +++++++++++++++ .../__snapshots__/y_extents.test.tsx.snap | 40 ++ .../metrics_axes/category_axis_panel.test.tsx | 78 +++ .../metrics_axes/category_axis_panel.tsx | 2 +- .../metrics_axes/chart_options.test.tsx | 103 ++++ .../options/metrics_axes/chart_options.tsx | 2 +- .../custom_extents_options.test.tsx | 150 ++++++ .../metrics_axes/custom_extents_options.tsx | 2 +- .../options/metrics_axes/index.test.tsx | 314 +++++++++++ .../components/options/metrics_axes/index.tsx | 14 +- .../metrics_axes/label_options.test.tsx | 98 ++++ .../options/metrics_axes/label_options.tsx | 2 +- .../metrics_axes/line_options.test.tsx | 77 +++ .../options/metrics_axes/line_options.tsx | 2 +- .../components/options/metrics_axes/mocks.ts | 87 +++ .../options/metrics_axes/series_panel.tsx | 2 +- .../metrics_axes/value_axes_panel.test.tsx | 155 ++++++ .../options/metrics_axes/value_axes_panel.tsx | 3 +- .../metrics_axes/value_axis_options.test.tsx | 165 ++++++ .../options/metrics_axes/y_extents.test.tsx | 112 ++++ .../options/metrics_axes/y_extents.tsx | 2 +- .../public/utils/collections.ts | 4 + 29 files changed, 3044 insertions(+), 13 deletions(-) create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx create mode 100644 src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap new file mode 100644 index 00000000000000..6eef5047634f4f --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CategoryAxisPanel component should init with the default set of props 1`] = ` + + +

+ +

+
+ + + + +
+`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap new file mode 100644 index 00000000000000..56f35ae0211732 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChartOptions component should init with the default set of props 1`] = ` + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap new file mode 100644 index 00000000000000..c7d3f4036fa049 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CustomExtentsOptions component should init with the default set of props 1`] = ` + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000000..256df603a7f33a --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -0,0 +1,354 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MetricsAxisOptions component should init with the default set of props 1`] = ` + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap new file mode 100644 index 00000000000000..0dc2f6519a1084 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LabelOptions component should init with the default set of props 1`] = ` + + + +

+ +

+
+ + + + + + + + + + + + +
+`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap new file mode 100644 index 00000000000000..7b45423f5f861e --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LineOptions component should init with the default set of props 1`] = ` + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap new file mode 100644 index 00000000000000..fd80e75b7783f6 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -0,0 +1,502 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ValueAxesPanel component should init with the default set of props 1`] = ` + + + + +

+ +

+
+
+ + + + + +
+ + + ValueAxis-1 + + + + Count + + + + } + buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + className="visEditorSidebar__section visEditorSidebar__collapsible" + data-test-subj="toggleYAxisOptions-ValueAxis-1" + extraAction={ + + + + } + id="yAxisAccordionValueAxis-1" + initialIsOpen={false} + key="ValueAxis-1" + paddingSize="none" + > + + + + + ValueAxis-1 + + + + Average + + + + } + buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + className="visEditorSidebar__section visEditorSidebar__collapsible" + data-test-subj="toggleYAxisOptions-ValueAxis-2" + extraAction={ + + + + } + id="yAxisAccordionValueAxis-2" + initialIsOpen={false} + key="ValueAxis-2" + paddingSize="none" + > + + + +
+`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap new file mode 100644 index 00000000000000..f2ee088450fbdd --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap @@ -0,0 +1,415 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ValueAxisOptions component should init with the default set of props 1`] = ` + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap new file mode 100644 index 00000000000000..3372e781a028ae --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`YExtents component should init with the default set of props 1`] = ` + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx new file mode 100644 index 00000000000000..a32e48baf4588c --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { CategoryAxisPanel, CategoryAxisPanelProps } from './category_axis_panel'; +import { Axis } from '../../../types'; +import { Positions, getPositions } from '../../../utils/collections'; +import { LabelOptions } from './label_options'; +import { categoryAxis } from './mocks'; + +const positions = getPositions(); + +describe('CategoryAxisPanel component', () => { + let setCategoryAxis: jest.Mock; + let onPositionChanged: jest.Mock; + let defaultProps: CategoryAxisPanelProps; + let axis: Axis; + + beforeEach(() => { + setCategoryAxis = jest.fn(); + onPositionChanged = jest.fn(); + axis = categoryAxis; + + defaultProps = { + axis, + vis: { + type: { + editorConfig: { + collections: { positions }, + }, + }, + }, + onPositionChanged, + setCategoryAxis, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should respond axis.show', () => { + const comp = shallow(); + + expect(comp.find(LabelOptions).exists()).toBeTruthy(); + + comp.setProps({ axis: { ...axis, show: false } }); + expect(comp.find(LabelOptions).exists()).toBeFalsy(); + }); + + it('should call onPositionChanged when position is changed', () => { + const value = Positions.RIGHT; + const comp = shallow(); + comp.find({ paramName: 'position' }).prop('setValue')('position', value); + + expect(setCategoryAxis).toHaveBeenLastCalledWith({ ...axis, position: value }); + expect(onPositionChanged).toBeCalledWith(value); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx index a2edc533e6e7e3..0ac400dba7bad3 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx @@ -28,7 +28,7 @@ import { SelectOption, SwitchOption } from '../../common'; import { LabelOptions } from './label_options'; import { Positions } from '../../../utils/collections'; -interface CategoryAxisPanelProps extends VisOptionsProps { +export interface CategoryAxisPanelProps extends VisOptionsProps { axis: Axis; onPositionChanged: (position: Positions) => void; setCategoryAxis: (value: Axis) => void; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx new file mode 100644 index 00000000000000..ba1a46ba7d89e7 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ChartOptions, ChartOptionsParams } from './chart_options'; +import { SeriesParam } from '../../../types'; +import { LineOptions } from './line_options'; +import { + ChartTypes, + ChartModes, + getInterpolationModes, + getChartTypes, + getChartModes, +} from '../../../utils/collections'; +import { valueAxis, seriesParam } from './mocks'; + +const interpolationModes = getInterpolationModes(); +const chartTypes = getChartTypes(); +const chartModes = getChartModes(); + +describe('ChartOptions component', () => { + let setParamByIndex: jest.Mock; + let changeValueAxis: jest.Mock; + let defaultProps: ChartOptionsParams; + let chart: SeriesParam; + + beforeEach(() => { + setParamByIndex = jest.fn(); + changeValueAxis = jest.fn(); + chart = { ...seriesParam }; + + defaultProps = { + index: 0, + chart, + vis: { + type: { + editorConfig: { + collections: { interpolationModes, chartTypes, chartModes }, + }, + }, + }, + stateParams: { + valueAxes: [valueAxis], + }, + setParamByIndex, + changeValueAxis, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should show LineOptions when type is line', () => { + chart.type = ChartTypes.LINE; + const comp = shallow(); + + expect(comp.find(LineOptions).exists()).toBeTruthy(); + }); + + it('should show line mode when type is area', () => { + chart.type = ChartTypes.AREA; + const comp = shallow(); + + expect(comp.find({ paramName: 'interpolate' }).exists()).toBeTruthy(); + }); + + it('should call changeValueAxis when valueAxis is changed', () => { + const comp = shallow(); + const paramName = 'valueAxis'; + const value = 'new'; + comp.find({ paramName }).prop('setValue')(paramName, value); + + expect(changeValueAxis).toBeCalledWith(0, paramName, value); + }); + + it('should call setParamByIndex when mode is changed', () => { + const comp = shallow(); + const paramName = 'mode'; + comp.find({ paramName }).prop('setValue')(paramName, ChartModes.NORMAL); + + expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartModes.NORMAL); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx index ba41ad802ade4d..1c9357c67c2f08 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx @@ -30,7 +30,7 @@ import { SetParamByIndex, ChangeValueAxis } from './'; export type SetChart = (paramName: T, value: SeriesParam[T]) => void; -interface ChartOptionsParams extends VisOptionsProps { +export interface ChartOptionsParams extends VisOptionsProps { chart: SeriesParam; index: number; changeValueAxis: ChangeValueAxis; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx new file mode 100644 index 00000000000000..b55d363fa56c8e --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { CustomExtentsOptions, CustomExtentsOptionsProps } from './custom_extents_options'; +import { YExtents } from './y_extents'; +import { valueAxis } from './mocks'; + +const BOUNDS_MARGIN = 'boundsMargin'; +const DEFAULT_Y_EXTENTS = 'defaultYExtents'; +const SCALE = 'scale'; +const SET_Y_EXTENTS = 'setYExtents'; + +describe('CustomExtentsOptions component', () => { + let setValueAxis: jest.Mock; + let setValueAxisScale: jest.Mock; + let setMultipleValidity: jest.Mock; + let defaultProps: CustomExtentsOptionsProps; + + beforeEach(() => { + setValueAxis = jest.fn(); + setValueAxisScale = jest.fn(); + setMultipleValidity = jest.fn(); + + defaultProps = { + axis: { ...valueAxis }, + setValueAxis, + setValueAxisScale, + setMultipleValidity, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + describe('boundsMargin', () => { + it('should set validity as true when value is positive', () => { + const comp = shallow(); + comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, 5); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); + }); + + it('should set validity as true when value is empty', () => { + const comp = shallow(); + comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, ''); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); + }); + + it('should set validity as false when value is negative', () => { + defaultProps.axis.scale.defaultYExtents = true; + const comp = shallow(); + comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, -1); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, false); + }); + }); + + describe('defaultYExtents', () => { + it('should show bounds margin input when defaultYExtents is true', () => { + const comp = shallow(); + + expect(comp.find({ paramName: BOUNDS_MARGIN }).exists()).toBeTruthy(); + }); + + it('should hide bounds margin input when defaultYExtents is false', () => { + defaultProps.axis.scale = { ...defaultProps.axis.scale, defaultYExtents: false }; + const comp = shallow(); + + expect(comp.find({ paramName: BOUNDS_MARGIN }).exists()).toBeFalsy(); + }); + + it('should call setValueAxis when value is true', () => { + const comp = shallow(); + comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, true); + + expect(setMultipleValidity).not.toBeCalled(); + expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axis.scale); + }); + + it('should reset boundsMargin when value is false', () => { + const comp = shallow(); + comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, false); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); + const newScale = { + ...defaultProps.axis.scale, + boundsMargin: undefined, + defaultYExtents: false, + }; + expect(setValueAxis).toBeCalledWith(SCALE, newScale); + }); + }); + + describe('setYExtents', () => { + it('should show YExtents when value is true', () => { + const comp = shallow(); + + expect(comp.find(YExtents).exists()).toBeTruthy(); + }); + + it('should hide YExtents when value is false', () => { + defaultProps.axis.scale = { ...defaultProps.axis.scale, setYExtents: false }; + const comp = shallow(); + + expect(comp.find(YExtents).exists()).toBeFalsy(); + }); + + it('should call setValueAxis when value is true', () => { + const comp = shallow(); + comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, true); + + expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axis.scale); + }); + + it('should reset min and max when value is false', () => { + const comp = shallow(); + comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, false); + + const newScale = { + ...defaultProps.axis.scale, + min: undefined, + max: undefined, + setYExtents: false, + }; + expect(setValueAxis).toBeCalledWith(SCALE, newScale); + }); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx index dca948457b61c8..99783a6887716b 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx @@ -25,7 +25,7 @@ import { NumberInputOption, SwitchOption } from '../../common'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; -interface CustomExtentsOptionsProps { +export interface CustomExtentsOptionsProps { axis: ValueAxis; setMultipleValidity(paramName: string, isValid: boolean): void; setValueAxis(paramName: T, value: ValueAxis[T]): void; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx new file mode 100644 index 00000000000000..dc5cf422776034 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx @@ -0,0 +1,314 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { MetricsAxisOptions } from './index'; +import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; +import { ValidationVisOptionsProps } from '../../common'; +import { Positions } from '../../../utils/collections'; +import { ValueAxesPanel } from './value_axes_panel'; +import { CategoryAxisPanel } from './category_axis_panel'; +import { ChartTypes } from '../../../utils/collections'; +import { AggConfig } from 'ui/vis'; +import { AggType } from 'ui/agg_types'; +import { defaultValueAxisId, valueAxis, seriesParam, categoryAxis } from './mocks'; + +jest.mock('./series_panel', () => ({ + SeriesPanel: () => 'SeriesPanel', +})); +jest.mock('./category_axis_panel', () => ({ + CategoryAxisPanel: () => 'CategoryAxisPanel', +})); +jest.mock('./value_axes_panel', () => ({ + ValueAxesPanel: () => 'ValueAxesPanel', +})); + +const SERIES_PARAMS = 'seriesParams'; +const VALUE_AXES = 'valueAxes'; + +const aggCount: AggConfig = { + id: '1', + type: { name: 'count' }, + makeLabel: () => 'Count', +} as AggConfig; + +const aggAverage: AggConfig = { + id: '2', + type: { name: 'average' } as AggType, + makeLabel: () => 'Average', +} as AggConfig; + +const createAggs = (aggs: any[]) => ({ + aggs, + bySchemaName: () => aggs, +}); + +describe('MetricsAxisOptions component', () => { + let setValue: jest.Mock; + let setVisType: jest.Mock; + let defaultProps: ValidationVisOptionsProps; + let axis: ValueAxis; + let axisRight: ValueAxis; + let chart: SeriesParam; + + beforeEach(() => { + setValue = jest.fn(); + setVisType = jest.fn(); + + axis = { + ...valueAxis, + name: 'LeftAxis-1', + position: Positions.LEFT, + }; + axisRight = { + ...valueAxis, + id: 'ValueAxis-2', + name: 'RightAxis-1', + position: Positions.RIGHT, + }; + chart = { + ...seriesParam, + type: ChartTypes.AREA, + }; + + defaultProps = { + aggs: createAggs([aggCount]), + aggsLabels: '', + vis: { + type: { + type: ChartTypes.AREA, + schemas: { metrics: [{ name: 'metric' }] }, + }, + }, + stateParams: { + valueAxes: [axis], + seriesParams: [chart], + categoryAxes: [categoryAxis], + grid: { valueAxis: defaultValueAxisId }, + }, + setValue, + setVisType, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + describe('useEffect', () => { + it('should update series when new agg is added', () => { + const comp = mount(); + comp.setProps({ + aggs: createAggs([aggCount, aggAverage]), + aggsLabels: `${aggCount.makeLabel()}, ${aggAverage.makeLabel()}`, + }); + + const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; + expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update series when new agg label is changed', () => { + const comp = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'New label' }; + comp.setProps({ + aggs: createAggs([agg]), + }); + + const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; + expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update visType when one seriesParam', () => { + const comp = mount(); + expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); + + comp.setProps({ + stateParams: { + ...defaultProps.stateParams, + seriesParams: [{ ...chart, type: ChartTypes.LINE }], + }, + }); + + expect(setVisType).toHaveBeenLastCalledWith(ChartTypes.LINE); + }); + + it('should set histogram visType when multiple seriesParam', () => { + const comp = mount(); + expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); + + comp.setProps({ + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, { ...chart, type: ChartTypes.LINE }], + }, + }); + + expect(setVisType).toHaveBeenLastCalledWith(ChartTypes.HISTOGRAM); + }); + }); + + describe('updateAxisTitle', () => { + it('should not update the value axis title if custom title was set', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const comp = mount(); + const newAgg = { + ...aggCount, + makeLabel: () => 'Custom label', + }; + comp.setProps({ + aggs: createAggs([newAgg]), + aggsLabels: `${newAgg.makeLabel()}`, + }); + const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should set the custom title to match the value axis label when only one agg exists for that axis', () => { + const comp = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { + const comp = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; + comp.setProps({ + aggs: createAggs([agg, aggAverage]), + aggsLabels: `${agg.makeLabel()}, ${aggAverage.makeLabel()}`, + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, chart], + }, + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + + it('should not overwrite the custom title with the value axis label if the custom title has been changed', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const comp = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + + it('should overwrite the custom title when the agg type changes', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const comp = mount(); + const agg = { + id: aggCount.id, + type: { name: 'max' }, + makeLabel: () => 'Max', + }; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should overwrite the custom title when the agg field changes', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const agg = { + id: aggCount.id, + type: { name: 'max' }, + makeLabel: () => 'Max', + } as AggConfig; + defaultProps.aggs = createAggs([agg]) as any; + const comp = mount(); + agg.params = { field: { name: 'Field' } }; + agg.makeLabel = () => 'Max, Field'; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + }); + + it('should add value axis', () => { + const comp = shallow(); + comp.find(ValueAxesPanel).prop('addValueAxis')(); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axis, axisRight]); + }); + + describe('removeValueAxis', () => { + beforeEach(() => { + defaultProps.stateParams.valueAxes = [axis, axisRight]; + }); + + it('should remove value axis', () => { + const comp = shallow(); + comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axisRight]); + }); + + it('should update seriesParams "valueAxis" prop', () => { + const updatedSeriesParam = { ...chart, valueAxis: 'ValueAxis-2' }; + const comp = shallow(); + comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, [updatedSeriesParam]); + }); + + it('should reset grid "valueAxis" prop', () => { + const updatedGrid = { valueAxis: undefined }; + defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + const comp = shallow(); + comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith('grid', updatedGrid); + }); + }); + + it('should update axis value when when category position chnaged', () => { + const comp = shallow(); + comp.find(CategoryAxisPanel).prop('onPositionChanged')(Positions.LEFT); + + const updatedValues = [{ ...axis, name: 'BottomAxis-1', position: Positions.BOTTOM }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx index 05797b8dde5b21..c7ada18f9e1f25 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx @@ -125,13 +125,17 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) lastLabels[axis.id] = newCustomLabel; if ( - aggTypeIsChanged || - aggFieldIsChanged || - axis.title.text === '' || - lastCustomLabels[axis.id] === axis.title.text + Object.keys(lastCustomLabels).length !== 0 && + (aggTypeIsChanged || + aggFieldIsChanged || + axis.title.text === '' || + lastCustomLabels[axis.id] === axis.title.text) ) { // Override axis title with new custom label - axes[axisNumber] = { ...axes[axisNumber], title: { ...axis, text: newCustomLabel } }; + axes[axisNumber] = { + ...axis, + title: { ...axis.title, text: newCustomLabel }, + }; isAxesChanged = true; } } diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx new file mode 100644 index 00000000000000..abb3a2455f9f9e --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { LabelOptions, LabelOptionsProps } from './label_options'; +import { TruncateLabelsOption } from '../../common'; +import { valueAxis, categoryAxis } from './mocks'; + +const FILTER = 'filter'; +const ROTATE = 'rotate'; +const DISABLED = 'disabled'; +const CATEGORY_AXES = 'categoryAxes'; + +describe('LabelOptions component', () => { + let setValue: jest.Mock; + let defaultProps: LabelOptionsProps; + + beforeEach(() => { + setValue = jest.fn(); + + defaultProps = { + axis: { ...valueAxis }, + axesName: CATEGORY_AXES, + index: 0, + stateParams: { + categoryAxes: [{ ...categoryAxis }], + valueAxes: [{ ...valueAxis }], + } as any, + setValue, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should show other fields when axis.labels.show is true', () => { + const comp = shallow(); + + expect(comp.find({ paramName: FILTER }).prop(DISABLED)).toBeFalsy(); + expect(comp.find({ paramName: ROTATE }).prop(DISABLED)).toBeFalsy(); + expect(comp.find(TruncateLabelsOption).prop(DISABLED)).toBeFalsy(); + }); + + it('should disable other fields when axis.labels.show is false', () => { + defaultProps.axis.labels.show = false; + const comp = shallow(); + + expect(comp.find({ paramName: FILTER }).prop(DISABLED)).toBeTruthy(); + expect(comp.find({ paramName: ROTATE }).prop(DISABLED)).toBeTruthy(); + expect(comp.find(TruncateLabelsOption).prop(DISABLED)).toBeTruthy(); + }); + + it('should set rotate as number', () => { + const comp = shallow(); + comp.find({ paramName: ROTATE }).prop('setValue')(ROTATE, '5'); + + const newAxes = [{ ...categoryAxis, labels: { ...categoryAxis.labels, rotate: 5 } }]; + expect(setValue).toBeCalledWith(CATEGORY_AXES, newAxes); + }); + + it('should set filter value', () => { + const comp = shallow(); + expect(defaultProps.stateParams.categoryAxes[0].labels.filter).toBeTruthy(); + comp.find({ paramName: FILTER }).prop('setValue')(FILTER, false); + + const newAxes = [{ ...categoryAxis, labels: { ...categoryAxis.labels, filter: false } }]; + expect(setValue).toBeCalledWith(CATEGORY_AXES, newAxes); + }); + + it('should set value for valueAxes', () => { + defaultProps.axesName = 'valueAxes'; + const comp = shallow(); + comp.find(TruncateLabelsOption).prop('setValue')('truncate', 10); + + const newAxes = [{ ...valueAxis, labels: { ...valueAxis.labels, truncate: 10 } }]; + expect(setValue).toBeCalledWith('valueAxes', newAxes); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx index a0d91a0abe38af..9918cd8cd807cb 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx @@ -27,7 +27,7 @@ import { BasicVislibParams, Axis } from '../../../types'; import { SelectOption, SwitchOption, TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../utils/collections'; -interface LabelOptionsProps extends VisOptionsProps { +export interface LabelOptionsProps extends VisOptionsProps { axis: Axis; axesName: 'categoryAxes' | 'valueAxes'; index: number; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx new file mode 100644 index 00000000000000..0e603814493fa5 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { LineOptions, LineOptionsParams } from './line_options'; +import { NumberInputOption } from '../../common'; +import { getInterpolationModes } from '../../../utils/collections'; +import { seriesParam } from './mocks'; + +const LINE_WIDTH = 'lineWidth'; +const DRAW_LINES = 'drawLinesBetweenPoints'; +const interpolationModes = getInterpolationModes(); + +describe('LineOptions component', () => { + let setChart: jest.Mock; + let defaultProps: LineOptionsParams; + + beforeEach(() => { + setChart = jest.fn(); + + defaultProps = { + chart: { ...seriesParam }, + vis: { + type: { + editorConfig: { + collections: { interpolationModes }, + }, + }, + }, + setChart, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should set lineWidth as undefined when empty value', () => { + const comp = shallow(); + comp.find(NumberInputOption).prop('setValue')(LINE_WIDTH, ''); + + expect(setChart).toBeCalledWith(LINE_WIDTH, undefined); + }); + + it('should set lineWidth value', () => { + const comp = shallow(); + comp.find(NumberInputOption).prop('setValue')(LINE_WIDTH, 5); + + expect(setChart).toBeCalledWith(LINE_WIDTH, 5); + }); + + it('should set drawLinesBetweenPoints', () => { + const comp = shallow(); + comp.find({ paramName: DRAW_LINES }).prop('setValue')(DRAW_LINES, false); + + expect(setChart).toBeCalledWith(DRAW_LINES, false); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx index 8a4c8cc05efab6..9514b69a20b04e 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx @@ -26,7 +26,7 @@ import { SeriesParam } from '../../../types'; import { NumberInputOption, SelectOption, SwitchOption } from '../../common'; import { SetChart } from './chart_options'; -interface LineOptionsParams { +export interface LineOptionsParams { chart: SeriesParam; vis: Vis; setChart: SetChart; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts new file mode 100644 index 00000000000000..422ad3c88fe8a4 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Axis, ValueAxis, SeriesParam } from '../../../types'; +import { + ChartTypes, + ChartModes, + InterpolationModes, + ScaleTypes, + Positions, + AxisTypes, +} from '../../../utils/collections'; + +const defaultValueAxisId = 'ValueAxis-1'; + +const axis = { + show: true, + style: {}, + title: { + text: '', + }, + labels: { + show: true, + filter: true, + truncate: 0, + color: 'black', + }, +}; + +const categoryAxis: Axis = { + ...axis, + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.BOTTOM, + scale: { + type: ScaleTypes.LINEAR, + }, +}; + +const valueAxis: ValueAxis = { + ...axis, + id: defaultValueAxisId, + name: 'ValueAxis-1', + type: AxisTypes.VALUE, + position: Positions.LEFT, + scale: { + type: ScaleTypes.LINEAR, + boundsMargin: 1, + defaultYExtents: true, + min: 1, + max: 2, + setYExtents: true, + }, +}; + +const seriesParam: SeriesParam = { + show: true, + type: ChartTypes.HISTOGRAM, + mode: ChartModes.STACKED, + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: InterpolationModes.LINEAR, + valueAxis: defaultValueAxisId, +}; + +export { defaultValueAxisId, categoryAxis, valueAxis, seriesParam }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx index 816b0bfeda598f..434202d64d6c30 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx @@ -27,7 +27,7 @@ import { BasicVislibParams } from '../../../types'; import { ChartOptions } from './chart_options'; import { SetParamByIndex, ChangeValueAxis } from './'; -interface SeriesPanelProps extends VisOptionsProps { +export interface SeriesPanelProps extends VisOptionsProps { changeValueAxis: ChangeValueAxis; setParamByIndex: SetParamByIndex; } diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx new file mode 100644 index 00000000000000..080c64db7ff851 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; +import { ValueAxis, SeriesParam } from '../../../types'; +import { Positions, getScaleTypes, getAxisModes, getPositions } from '../../../utils/collections'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { valueAxis, seriesParam } from './mocks'; + +const positions = getPositions(); +const axisModes = getAxisModes(); +const scaleTypes = getScaleTypes(); + +describe('ValueAxesPanel component', () => { + let setParamByIndex: jest.Mock; + let onValueAxisPositionChanged: jest.Mock; + let setMultipleValidity: jest.Mock; + let addValueAxis: jest.Mock; + let removeValueAxis: jest.Mock; + let defaultProps: ValueAxesPanelProps; + let axisLeft: ValueAxis; + let axisRight: ValueAxis; + let seriesParamCount: SeriesParam; + let seriesParamAverage: SeriesParam; + + beforeEach(() => { + setParamByIndex = jest.fn(); + onValueAxisPositionChanged = jest.fn(); + addValueAxis = jest.fn(); + removeValueAxis = jest.fn(); + setMultipleValidity = jest.fn(); + axisLeft = { ...valueAxis }; + axisRight = { + ...valueAxis, + id: 'ValueAxis-2', + position: Positions.RIGHT, + }; + seriesParamCount = { ...seriesParam }; + seriesParamAverage = { + ...seriesParam, + valueAxis: 'ValueAxis-2', + data: { + label: 'Average', + id: '1', + }, + }; + + defaultProps = { + stateParams: { + seriesParams: [seriesParamCount, seriesParamAverage], + valueAxes: [axisLeft, axisRight], + }, + vis: { + type: { + editorConfig: { + collections: { scaleTypes, axisModes, positions }, + }, + }, + }, + isCategoryAxisHorizontal: false, + setParamByIndex, + onValueAxisPositionChanged, + addValueAxis, + removeValueAxis, + setMultipleValidity, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should not allow to remove the last value axis', () => { + defaultProps.stateParams.valueAxes = [axisLeft]; + const comp = mountWithIntl(); + expect(comp.find('[data-test-subj="removeValueAxisBtn"] button').exists()).toBeFalsy(); + }); + + it('should display remove button when multiple axes', () => { + const comp = mountWithIntl(); + + expect(comp.find('[data-test-subj="removeValueAxisBtn"] button').exists()).toBeTruthy(); + }); + + it('should call removeAgg', () => { + const comp = mountWithIntl(); + comp + .find('[data-test-subj="removeValueAxisBtn"] button') + .first() + .simulate('click'); + + expect(removeValueAxis).toBeCalledWith(axisLeft); + }); + + it('should call addValueAxis', () => { + const comp = mountWithIntl(); + comp.find('[data-test-subj="visualizeAddYAxisButton"] button').simulate('click'); + + expect(addValueAxis).toBeCalled(); + }); + + describe('description', () => { + it('should show when one serie matches value axis', () => { + const comp = mountWithIntl(); + expect( + comp + .find('.visEditorSidebar__aggGroupAccordionButtonContent span') + .first() + .text() + ).toBe(seriesParamCount.data.label); + }); + + it('should show when multiple series match value axis', () => { + defaultProps.stateParams.seriesParams[1].valueAxis = 'ValueAxis-1'; + const comp = mountWithIntl(); + expect( + comp + .find('.visEditorSidebar__aggGroupAccordionButtonContent span') + .first() + .text() + ).toBe(`${seriesParamCount.data.label}, ${seriesParamAverage.data.label}`); + }); + + it('should not show when no series match value axis', () => { + defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + const comp = mountWithIntl(); + expect( + comp + .find('.visEditorSidebar__aggGroupAccordionButtonContent span') + .first() + .text() + ).toBe(''); + }); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx index 2ae54f9e093373..34a0d2cd981c5f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx @@ -36,7 +36,7 @@ import { ValueAxisOptions } from './value_axis_options'; import { SetParamByIndex } from './'; import { ValidationVisOptionsProps } from '../../common'; -interface ValueAxesPanelProps extends ValidationVisOptionsProps { +export interface ValueAxesPanelProps extends ValidationVisOptionsProps { isCategoryAxisHorizontal: boolean; addValueAxis: () => ValueAxis; removeValueAxis: (axis: ValueAxis) => void; @@ -74,6 +74,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { iconType="cross" onClick={() => removeValueAxis(axis)} aria-label={removeButtonTooltip} + data-test-subj="removeValueAxisBtn" /> ), diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx new file mode 100644 index 00000000000000..8cb476508c78b1 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx @@ -0,0 +1,165 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; +import { Axis } from '../../../types'; +import { TextInputOption } from '../../common'; +import { LabelOptions } from './label_options'; +import { + ScaleTypes, + Positions, + getScaleTypes, + getAxisModes, + getPositions, +} from '../../../utils/collections'; +import { valueAxis, categoryAxis } from './mocks'; + +const POSITION = 'position'; +const positions = getPositions(); +const axisModes = getAxisModes(); +const scaleTypes = getScaleTypes(); + +interface PositionOption { + text: string; + value: Positions; + disabled: boolean; +} + +describe('ValueAxisOptions component', () => { + let setParamByIndex: jest.Mock; + let onValueAxisPositionChanged: jest.Mock; + let setMultipleValidity: jest.Mock; + let defaultProps: ValueAxisOptionsParams; + let axis: Axis; + + beforeEach(() => { + setParamByIndex = jest.fn(); + setMultipleValidity = jest.fn(); + onValueAxisPositionChanged = jest.fn(); + axis = { ...valueAxis }; + + defaultProps = { + axis, + index: 0, + stateParams: { + categoryAxes: [{ ...categoryAxis }], + valueAxes: [axis], + }, + vis: { + type: { + editorConfig: { + collections: { scaleTypes, axisModes, positions }, + }, + }, + }, + isCategoryAxisHorizontal: false, + setParamByIndex, + onValueAxisPositionChanged, + setMultipleValidity, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should hide options when axis.show is false', () => { + defaultProps.axis.show = false; + const comp = shallow(); + + expect(comp.find(TextInputOption).exists()).toBeFalsy(); + expect(comp.find(LabelOptions).exists()).toBeFalsy(); + }); + + it('should only allow left and right value axis position when category axis is horizontal', () => { + defaultProps.isCategoryAxisHorizontal = true; + const comp = shallow(); + + const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); + + expect(options.length).toBe(4); + options.forEach(({ value, disabled }) => { + switch (value) { + case Positions.LEFT: + case Positions.RIGHT: + expect(disabled).toBeFalsy(); + break; + case Positions.TOP: + case Positions.BOTTOM: + expect(disabled).toBeTruthy(); + break; + } + }); + }); + + it('should only allow top and bottom value axis position when category axis is vertical', () => { + defaultProps.isCategoryAxisHorizontal = false; + const comp = shallow(); + + const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); + + expect(options.length).toBe(4); + options.forEach(({ value, disabled }) => { + switch (value) { + case Positions.LEFT: + case Positions.RIGHT: + expect(disabled).toBeTruthy(); + break; + case Positions.TOP: + case Positions.BOTTOM: + expect(disabled).toBeFalsy(); + break; + } + }); + }); + + it('should call onValueAxisPositionChanged when position is changed', () => { + const value = Positions.RIGHT; + const comp = shallow(); + comp.find({ paramName: POSITION }).prop('setValue')(POSITION, value); + + expect(onValueAxisPositionChanged).toBeCalledWith(defaultProps.index, value); + }); + + it('should call setValueAxis when title is changed', () => { + defaultProps.axis.show = true; + const textValue = 'New title'; + const comp = shallow(); + comp.find(TextInputOption).prop('setValue')('text', textValue); + + expect(setParamByIndex).toBeCalledWith('valueAxes', defaultProps.index, 'title', { + text: textValue, + }); + }); + + it('should call setValueAxis when scale value is changed', () => { + const scaleValue = ScaleTypes.SQUARE_ROOT; + const comp = shallow(); + comp.find({ paramName: 'type' }).prop('setValue')('type', scaleValue); + + expect(setParamByIndex).toBeCalledWith('valueAxes', defaultProps.index, 'scale', { + ...defaultProps.axis.scale, + type: scaleValue, + }); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx new file mode 100644 index 00000000000000..2df17b6e349852 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { YExtents, YExtentsProps } from './y_extents'; +import { ScaleTypes } from '../../../utils/collections'; +import { NumberInputOption } from '../../common'; + +describe('YExtents component', () => { + let setMultipleValidity: jest.Mock; + let setScale: jest.Mock; + let defaultProps: YExtentsProps; + const Y_EXTENTS = 'yExtents'; + + beforeEach(() => { + setMultipleValidity = jest.fn(); + setScale = jest.fn(); + + defaultProps = { + scale: { + type: ScaleTypes.LINEAR, + }, + setMultipleValidity, + setScale, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should call setMultipleValidity with true when min and max are not defined', () => { + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, true); + }); + + it('should call setMultipleValidity with true when min less than max', () => { + defaultProps.scale.min = 1; + defaultProps.scale.max = 2; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, true); + }); + + it('should call setMultipleValidity with false when min greater than max', () => { + defaultProps.scale.min = 1; + defaultProps.scale.max = 0; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); + }); + + it('should call setMultipleValidity with false when min equals max', () => { + defaultProps.scale.min = 1; + defaultProps.scale.max = 1; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); + }); + + it('should call setMultipleValidity with false when min equals 0 and scale is log', () => { + defaultProps.scale.min = 0; + defaultProps.scale.max = 1; + defaultProps.scale.type = ScaleTypes.LOG; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); + }); + + it('should call setScale with input number', () => { + const inputNumber = 5; + const comp = shallow(); + const inputProps = comp + .find(NumberInputOption) + .first() + .props(); + inputProps.setValue(Y_EXTENTS, inputNumber); + + expect(setScale).toBeCalledWith(Y_EXTENTS, inputNumber); + }); + + it('should call setScale with null when input is empty', () => { + const comp = shallow(); + const inputProps = comp + .find(NumberInputOption) + .first() + .props(); + inputProps.setValue(Y_EXTENTS, ''); + + expect(setScale).toBeCalledWith(Y_EXTENTS, null); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx index 29b986367d72ab..a5318f0ec3f00f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx @@ -49,7 +49,7 @@ function isNullOrUndefined(value?: number | null): value is null | undefined { return value === null || value === undefined; } -interface YExtentsProps { +export interface YExtentsProps { scale: Scale; setScale: SetScale; setMultipleValidity: (paramName: string, isValid: boolean) => void; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts index f47c62e8b0fd24..84b5cb5285948f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts @@ -330,4 +330,8 @@ export { getPositions, getRotateOptions, getScaleTypes, + getInterpolationModes, + getChartTypes, + getChartModes, + getAxisModes, }; From f4ea04c9cc8b9615b20d3a56e5eea98dcf53c86c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 1 Oct 2019 12:25:45 +0200 Subject: [PATCH 11/53] Advanced ui actions 2 np (#46948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 move advanced_ui_actions plugin to NP * fix: 🐛 fix NP plugin configs * fix: 🐛 remove import from legacy platform --- .../lib/panel/panel_header/panel_header.tsx | 9 ++++++-- x-pack/.i18nrc.json | 2 +- x-pack/index.js | 2 -- .../plugins/advanced_ui_actions/index.ts | 16 -------------- .../public/np_ready/public/legacy.ts | 21 ------------------- .../advanced_ui_actions}/kibana.json | 4 ++-- .../public/can_inherit_time_range.test.ts | 2 +- .../public/can_inherit_time_range.ts | 4 ++-- .../public/custom_time_range_action.test.ts | 6 +++--- .../public/custom_time_range_action.tsx | 15 ++++++------- .../public/custom_time_range_badge.test.ts | 5 ++--- .../public/custom_time_range_badge.tsx | 9 ++------ .../public/customize_time_range_modal.tsx | 4 ++-- .../public/does_inherit_time_range.ts | 2 +- .../advanced_ui_actions}/public/index.ts | 2 +- .../advanced_ui_actions}/public/plugin.ts | 11 +++++++--- .../public/test_helpers/index.ts | 0 .../test_helpers/time_range_container.ts | 4 ++-- .../test_helpers/time_range_embeddable.ts | 4 ++-- .../time_range_embeddable_factory.ts | 4 ++-- .../advanced_ui_actions}/public/types.ts | 2 +- 21 files changed, 45 insertions(+), 83 deletions(-) delete mode 100644 x-pack/legacy/plugins/advanced_ui_actions/index.ts delete mode 100644 x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/kibana.json (68%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/can_inherit_time_range.test.ts (95%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/can_inherit_time_range.ts (80%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/custom_time_range_action.test.ts (97%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/custom_time_range_action.tsx (84%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/custom_time_range_badge.test.ts (97%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/custom_time_range_badge.tsx (93%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/customize_time_range_modal.tsx (97%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/does_inherit_time_range.ts (90%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/index.ts (87%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/plugin.ts (88%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/test_helpers/index.ts (100%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/test_helpers/time_range_container.ts (90%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/test_helpers/time_range_embeddable.ts (84%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/test_helpers/time_range_embeddable_factory.ts (86%) rename x-pack/{legacy/plugins/advanced_ui_actions/public/np_ready => plugins/advanced_ui_actions}/public/types.ts (89%) diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 930b68dc884eff..1ad55aab5a449c 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -48,8 +48,13 @@ function renderBadges(badges: IAction[], embeddable: IEmbeddable) { )); } -function isVisualizeEmbeddable(embeddable: IEmbeddable | any): embeddable is any { - return embeddable.type === 'VISUALIZE_EMBEDDABLE_TYPE'; +const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; +type VisualizeEmbeddable = any; + +function isVisualizeEmbeddable( + embeddable: IEmbeddable | VisualizeEmbeddable +): embeddable is VisualizeEmbeddable { + return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE; } export function PanelHeader({ diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b92edfd06ffb1f..896c7ac7a259cb 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -2,7 +2,7 @@ "prefix": "xpack", "paths": { "xpack.actions": "legacy/plugins/actions", - "xpack.advancedUiActions": "legacy/plugins/advanced_ui_actions", + "xpack.advancedUiActions": "plugins/advanced_ui_actions", "xpack.alerting": "legacy/plugins/alerting", "xpack.apm": "legacy/plugins/apm", "xpack.beatsManagement": "legacy/plugins/beats_management", diff --git a/x-pack/index.js b/x-pack/index.js index 1aaae50d5a6130..10f969a21b68be 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -43,7 +43,6 @@ import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects' import { snapshotRestore } from './legacy/plugins/snapshot_restore'; import { actions } from './legacy/plugins/actions'; import { alerting } from './legacy/plugins/alerting'; -import { advancedUiActions } from './legacy/plugins/advanced_ui_actions'; import { lens } from './legacy/plugins/lens'; module.exports = function (kibana) { @@ -88,6 +87,5 @@ module.exports = function (kibana) { snapshotRestore(kibana), actions(kibana), alerting(kibana), - advancedUiActions(kibana), ]; }; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/index.ts b/x-pack/legacy/plugins/advanced_ui_actions/index.ts deleted file mode 100644 index e7b54d8863456e..00000000000000 --- a/x-pack/legacy/plugins/advanced_ui_actions/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { resolve } from 'path'; - -export const advancedUiActions = (kibana: any) => - new kibana.Plugin({ - id: 'advanced_ui_actions', - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: 'plugins/advanced_ui_actions/np_ready/public/legacy', - }, - }); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts b/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts deleted file mode 100644 index 535e55d71d3494..00000000000000 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/legacy.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import { npSetup, npStart } from 'ui/new_platform'; -/* eslint-enable @kbn/eslint/no-restricted-paths */ - -import { plugin } from '.'; - -const pluginInstance = plugin({} as any); -export const setup = pluginInstance.setup(npSetup.core, { - embeddable: npSetup.plugins.embeddable, - uiActions: npSetup.plugins.uiActions, -}); -export const start = pluginInstance.start(npStart.core, { - embeddable: npStart.plugins.embeddable, - uiActions: npStart.plugins.uiActions, -}); diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/kibana.json b/x-pack/plugins/advanced_ui_actions/kibana.json similarity index 68% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/kibana.json rename to x-pack/plugins/advanced_ui_actions/kibana.json index fd2c7ad1130c1f..515c4749de2128 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/kibana.json +++ b/x-pack/plugins/advanced_ui_actions/kibana.json @@ -1,9 +1,9 @@ { - "id": "advanced_ui_actions", + "id": "advancedUiActions", "version": "kibana", "requiredPlugins": [ "embeddable", - "ui_actions" + "uiActions" ], "server": false, "ui": true diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.test.ts b/x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.test.ts similarity index 95% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.test.ts rename to x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.test.ts index a0a550da1d24c9..03c096c8c178f4 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.test.ts +++ b/x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.test.ts @@ -8,7 +8,7 @@ import { canInheritTimeRange } from './can_inherit_time_range'; import { HelloWorldEmbeddable, HelloWorldContainer, -} from '../../../../../../../src/plugins/embeddable/public/lib/test_samples'; +} from '../../../../src/plugins/embeddable/public/lib/test_samples'; /** eslint-enable */ import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.ts b/x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.ts similarity index 80% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.ts rename to x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.ts index 09d8d26998e95e..2294c9be40e17d 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/can_inherit_time_range.ts +++ b/x-pack/plugins/advanced_ui_actions/public/can_inherit_time_range.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Embeddable, IContainer, ContainerInput } from 'src/plugins/embeddable/public'; -import { TimeRange } from '../../../../../../../src/plugins/data/public'; +import { Embeddable, IContainer, ContainerInput } from '../../../../src/plugins/embeddable/public'; +import { TimeRange } from '../../../../src/plugins/data/public'; import { TimeRangeInput } from './custom_time_range_action'; interface ContainerTimeRangeInput extends ContainerInput { diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts similarity index 97% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.test.ts rename to x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts index 1a5b26134ff5a3..bbdcf99495288b 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.test.ts @@ -10,18 +10,18 @@ import { skip } from 'rxjs/operators'; import * as Rx from 'rxjs'; import { mount } from 'enzyme'; -import { EmbeddableFactory } from '../../../../../../../src/plugins/embeddable/public'; +import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public'; import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory'; import { CustomTimeRangeAction } from './custom_time_range_action'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../src/core/public/mocks'; /* eslint-disable */ import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE_TYPE, HelloWorldEmbeddable, HelloWorldContainer, -} from '../../../../../../../src/plugins/embeddable/public/lib/test_samples'; +} from '../../../../src/plugins/embeddable/public/lib/test_samples'; /* eslint-enable */ import { nextTick } from 'test_utils/enzyme_helpers'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx similarity index 84% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.tsx rename to x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx index a9ab5edea4a25c..ca11fe91abdbf7 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_action.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx @@ -7,19 +7,13 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { - IAction, - IncompatibleActionError, -} from '../../../../../../../src/plugins/ui_actions/public'; -import { TimeRange } from '../../../../../../../src/plugins/data/public'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable'; -import { VisualizeEmbeddable } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../../../src/legacy/core_plugins/kibana/public/visualize/embeddable/constants'; - +import { IAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { OpenModal, CommonlyUsedRange } from './types'; const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE'; +const SEARCH_EMBEDDABLE_TYPE = 'search'; export interface TimeRangeInput extends EmbeddableInput { timeRange: TimeRange; @@ -31,6 +25,9 @@ function hasTimeRange( return (embeddable as Embeddable).getInput().timeRange !== undefined; } +const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; +type VisualizeEmbeddable = any; + function isVisualizeEmbeddable( embeddable: IEmbeddable | VisualizeEmbeddable ): embeddable is VisualizeEmbeddable { diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.test.ts b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts similarity index 97% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.test.ts rename to x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts index 9b13e5b03cf10d..c6046c02f0833d 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.test.ts +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.test.ts @@ -9,12 +9,11 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { skip } from 'rxjs/operators'; import * as Rx from 'rxjs'; import { mount } from 'enzyme'; - -import { EmbeddableFactory } from '../../../../../../../src/plugins/embeddable/public'; +import { EmbeddableFactory } from '../../../../src/plugins/embeddable/public'; import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers'; import { TimeRangeEmbeddableFactory } from './test_helpers/time_range_embeddable_factory'; import { CustomTimeRangeBadge } from './custom_time_range_badge'; -import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../src/core/public/mocks'; import { ReactElement } from 'react'; import { nextTick } from 'test_utils/enzyme_helpers'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx similarity index 93% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.tsx rename to x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx index 13fdbb17e1070f..78fe8e01e599e0 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/custom_time_range_badge.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_badge.tsx @@ -6,14 +6,9 @@ import React from 'react'; import { prettyDuration, commonDurationRanges } from '@elastic/eui'; - import { IEmbeddable, Embeddable, EmbeddableInput } from 'src/plugins/embeddable/public'; -import { - IAction, - IncompatibleActionError, -} from '../../../../../../../src/plugins/ui_actions/public'; -import { TimeRange } from '../../../../../../../src/plugins/data/public'; - +import { IAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { TimeRange } from '../../../../src/plugins/data/public'; import { CustomizeTimeRangeModal } from './customize_time_range_modal'; import { doesInheritTimeRange } from './does_inherit_time_range'; import { OpenModal, CommonlyUsedRange } from './types'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/customize_time_range_modal.tsx b/x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx similarity index 97% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/customize_time_range_modal.tsx rename to x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx index 4dabf28538668e..90393f9f4ff6f1 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/customize_time_range_modal.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/customize_time_range_modal.tsx @@ -19,8 +19,8 @@ import { EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Embeddable, IContainer, ContainerInput } from 'src/plugins/embeddable/public'; -import { TimeRange } from '../../../../../../../src/plugins/data/public'; +import { Embeddable, IContainer, ContainerInput } from '../../../../src/plugins/embeddable/public'; +import { TimeRange } from '../../../../src/plugins/data/public'; import { TimeRangeInput } from './custom_time_range_action'; import { doesInheritTimeRange } from './does_inherit_time_range'; import { CommonlyUsedRange } from './types'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/does_inherit_time_range.ts b/x-pack/plugins/advanced_ui_actions/public/does_inherit_time_range.ts similarity index 90% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/does_inherit_time_range.ts rename to x-pack/plugins/advanced_ui_actions/public/does_inherit_time_range.ts index 6b4033db345806..4cfe581b7eac55 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/does_inherit_time_range.ts +++ b/x-pack/plugins/advanced_ui_actions/public/does_inherit_time_range.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Embeddable, IContainer, ContainerInput } from 'src/plugins/embeddable/public'; +import { Embeddable, IContainer, ContainerInput } from '../../../../src/plugins/embeddable/public'; import { TimeRangeInput } from './custom_time_range_action'; export function doesInheritTimeRange(embeddable: Embeddable) { diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/index.ts b/x-pack/plugins/advanced_ui_actions/public/index.ts similarity index 87% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/index.ts rename to x-pack/plugins/advanced_ui_actions/public/index.ts index 5dd807ba2442d7..c11c1119a9b130 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/index.ts +++ b/x-pack/plugins/advanced_ui_actions/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext } from '../../../../src/core/public'; import { AdvancedUiActionsPublicPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts similarity index 88% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/plugin.ts rename to x-pack/plugins/advanced_ui_actions/public/plugin.ts index f1b87f6c694a19..fc106cc8ec26b9 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, +} from '../../../../src/core/public'; +import { IUiActionsStart, IUiActionsSetup } from '../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, Setup as EmbeddableSetup, Start as EmbeddableStart, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction } from './custom_time_range_action'; import { CustomTimeRangeBadge } from './custom_time_range_badge'; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/index.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/index.ts similarity index 100% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/index.ts rename to x-pack/plugins/advanced_ui_actions/public/test_helpers/index.ts diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_container.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts similarity index 90% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_container.ts rename to x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts index a916f40160c591..657ecf1fcee99b 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_container.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts @@ -9,8 +9,8 @@ import { Container, ContainerOutput, GetEmbeddableFactory, -} from '../../../../../../../../src/plugins/embeddable/public'; -import { TimeRange } from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/embeddable/public'; +import { TimeRange } from '../../../../../src/plugins/data/public'; /** * interfaces are not allowed to specify a sub-set of the required types until diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable.ts similarity index 84% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable.ts rename to x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable.ts index 0ca8a0ad9391f2..b768113f8b6c42 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable.ts @@ -9,8 +9,8 @@ import { Embeddable, EmbeddableInput, IContainer, -} from '../../../../../../../../src/plugins/embeddable/public'; -import { TimeRange } from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/embeddable/public'; +import { TimeRange } from '../../../../../src/plugins/data/public'; interface EmbeddableTimeRangeInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts similarity index 86% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable_factory.ts rename to x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts index 225bf3420faa19..efbf7a3bd2dc65 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/test_helpers/time_range_embeddable_factory.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts @@ -8,8 +8,8 @@ import { EmbeddableInput, IContainer, EmbeddableFactory, -} from '../../../../../../../../src/plugins/embeddable/public'; -import { TimeRange } from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../src/plugins/embeddable/public'; +import { TimeRange } from '../../../../../src/plugins/data/public'; import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable'; interface EmbeddableTimeRangeInput extends EmbeddableInput { diff --git a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/types.ts b/x-pack/plugins/advanced_ui_actions/public/types.ts similarity index 89% rename from x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/types.ts rename to x-pack/plugins/advanced_ui_actions/public/types.ts index 626782ba372ce4..bbd7c5528276f1 100644 --- a/x-pack/legacy/plugins/advanced_ui_actions/public/np_ready/public/types.ts +++ b/x-pack/plugins/advanced_ui_actions/public/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OverlayRef } from 'src/core/public'; +import { OverlayRef } from '../../../../src/core/public'; export interface CommonlyUsedRange { from: string; From 1f7715e22347ac28ed396bca7a6b673468276318 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 1 Oct 2019 03:55:33 -0700 Subject: [PATCH 12/53] Revert "[APM] Fix agent config flyout state (#46950)" (#46961) This reverts commit 09ef1a0f0a8a38e27c3e2f9eb10e46c6161e47cf. --- .../app/Settings/AddSettings/AddSettingFlyout.tsx | 15 +-------------- .../Settings/AddSettings/AddSettingFlyoutBody.tsx | 6 +----- .../apm/public/components/app/Settings/index.tsx | 1 + 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyout.tsx index f7cbfcbb85ebb2..613ce572e17b5f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyout.tsx @@ -18,7 +18,7 @@ import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -57,7 +57,6 @@ export function AddSettingsFlyout({ ? selectedConfig.settings.transaction_sample_rate.toString() : '' ); - const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( () => callApmApi({ @@ -89,18 +88,6 @@ export function AddSettingsFlyout({ env.name === environment && (Boolean(selectedConfig) || env.available) ); - useEffect(() => { - if (selectedConfig) { - setEnvironment(selectedConfig.service.environment); - setServiceName(selectedConfig.service.name); - setSampleRate(selectedConfig.settings.transaction_sample_rate.toString()); - } else { - setEnvironment(ENVIRONMENT_NOT_DEFINED); - setServiceName(undefined); - setSampleRate(''); - } - }, [selectedConfig]); - if (!isOpen) { return null; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx index 92a1452fae13b8..090f3fe0d5f912 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx @@ -157,11 +157,7 @@ export function AddSettingFlyoutBody({ placeholder={selectPlaceholderLabel} isLoading={environmentStatus === 'loading'} options={environmentOptions} - value={ - selectedConfig - ? environment || ENVIRONMENT_NOT_DEFINED - : environment - } + value={environment} disabled={!serviceName || Boolean(selectedConfig)} onChange={e => { e.preventDefault(); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx index c8ba5759f7a5e2..b75d3cf6ff458c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx @@ -52,6 +52,7 @@ export function Settings() { ); const hasConfigurations = !isEmpty(data); + return ( <> Date: Tue, 1 Oct 2019 13:22:04 +0100 Subject: [PATCH 13/53] Removing New Visualization title on save (#46719) * Removing New Visualization title on save * Removing title from saved visualization --- .../public/visualize/saved_visualizations/_saved_vis.js | 5 +---- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index fd13e458caeaae..f8adaed0bf584a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -26,7 +26,6 @@ */ import { VisProvider } from 'ui/vis'; -import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { updateOldState } from 'ui/vis/vis_update_state'; import { VisualizeConstants } from '../visualize_constants'; @@ -58,9 +57,7 @@ uiModules id: opts.id, indexPattern: opts.indexPattern, defaults: { - title: i18n.translate('kbn.visualize.defaultVisualizationTitle', { - defaultMessage: 'New Visualization', - }), + title: '', visState: (function () { if (!opts.type) return null; const def = {}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ba1cd13a1b9ffe..9b57ab465b707b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2426,7 +2426,6 @@ "kbn.visualize.badge.readOnly.text": "読み込み専用", "kbn.visualize.badge.readOnly.tooltip": "ビジュアライゼーションを保存できません", "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "indexPattern または savedSearchId が必要です", - "kbn.visualize.defaultVisualizationTitle": "新規ビジュアライゼーション", "kbn.visualize.disabledLabVisualizationMessage": "ラボビジュアライゼーションを表示するには、高度な設定でラボモードをオンにしてください。", "kbn.visualize.disabledLabVisualizationTitle": "{title} はラボビジュアライゼーションです。", "kbn.visualize.editor.createBreadcrumb": "作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 781043f6c7279d..3cd3ad3fdcd35e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2427,7 +2427,6 @@ "kbn.visualize.badge.readOnly.text": "只读", "kbn.visualize.badge.readOnly.tooltip": "无法保存可视化", "kbn.visualize.createVisualization.noIndexPatternOrSavedSearchIdErrorMessage": "必须提供 indexPattern 或 savedSearchId", - "kbn.visualize.defaultVisualizationTitle": "新建可视化", "kbn.visualize.disabledLabVisualizationMessage": "请在高级设置中打开实验室模式,以查看实验室可视化。", "kbn.visualize.disabledLabVisualizationTitle": "{title} 为实验室可视化。", "kbn.visualize.editor.createBreadcrumb": "创建", From dca6b3b93e3bb3df4f8ba473b201eea2415e5b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 1 Oct 2019 14:49:54 +0200 Subject: [PATCH 14/53] Increase breadcrumb `max` setting (#46595) --- src/core/public/chrome/ui/header/header_breadcrumbs.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index f4b1c1d49cd27f..68eb6a54f48a3c 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -64,7 +64,11 @@ export class HeaderBreadcrumbs extends Component { public render() { return ( - + ); } From cb6aa078e421928e74afecc46a0f3813d1d91f4a Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Tue, 1 Oct 2019 09:42:27 -0400 Subject: [PATCH 15/53] [Lens] Making field filters more obvious (#46615) * Using an EuiFacetButton for filter popover trigger * Added search icon and padding to field search * Add padding to sides of filter button --- .../editor_frame/_frame_layout.scss | 3 +- .../indexpattern_plugin/_datapanel.scss | 18 +- .../public/indexpattern_plugin/datapanel.tsx | 214 +++++++++--------- 3 files changed, 123 insertions(+), 112 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss index 7e7c402bbc7235..e3b91ee0674c83 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss @@ -20,8 +20,9 @@ min-width: $lnsPanelMinWidth + $euiSizeXL; overflow: hidden; // Leave out bottom padding so the suggestions scrollbar stays flush to window edge + // Leave out left padding so the left sidebar's focus states are visible outside of content bounds // This also means needing to add same amount of margin to page content and suggestion items - padding: $euiSize $euiSize 0; + padding: $euiSize $euiSize 0 0; &:first-child { padding-left: $euiSize; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss index cd2c9bff6b165e..4671d779833af7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss @@ -1,7 +1,9 @@ +@import '@elastic/eui/src/components/form/form_control_layout/mixins'; + .lnsInnerIndexPatternDataPanel { width: 100%; height: 100%; - padding: $euiSize 0 0 $euiSize; + padding: $euiSize $euiSize 0; } .lnsInnerIndexPatternDataPanel__header { @@ -24,7 +26,6 @@ .lnsInnerIndexPatternDataPanel__listWrapper { @include euiOverflowShadow; @include euiScrollBar; - margin-top: 2px; // form control shadow position: relative; flex-grow: 1; overflow: auto; @@ -37,3 +38,16 @@ left: 0; right: 0; } + +.lnsInnerIndexPatternDataPanel__filterButton { + width: 100%; + color: $euiColorPrimary; + padding-left: $euiSizeS; + padding-right: $euiSizeS; +} + +.lnsInnerIndexPatternDataPanel__textField { + @include euiFormControlLayoutPadding(1, 'right'); + @include euiFormControlLayoutPadding(1, 'left'); +} + diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index 12e6150a789b3a..85996659620e7d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -21,7 +21,8 @@ import { EuiText, EuiFormControlLayout, EuiSwitch, - EuiButtonIcon, + EuiFacetButton, + EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -352,120 +353,115 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
- - - - setLocalState(s => ({ ...localState, isTypeFilterOpen: false })) - } - button={ - { - setLocalState(s => ({ - ...s, - isTypeFilterOpen: !localState.isTypeFilterOpen, - })); - }} - data-test-subj="lnsIndexPatternFiltersToggle" - title={i18n.translate('xpack.lens.indexPatterns.toggleFiltersPopover', { - defaultMessage: 'Filters for index pattern', - })} - aria-label={i18n.translate( - 'xpack.lens.indexPatterns.toggleFiltersPopover', - { - defaultMessage: 'Filters for index pattern', - } - )} - /> - } - > - - {i18n.translate('xpack.lens.indexPatterns.filterByTypeLabel', { - defaultMessage: 'Filter by type', - })} - - ( - - setLocalState(s => ({ - ...s, - typeFilter: localState.typeFilter.includes(type) - ? localState.typeFilter.filter(t => t !== type) - : [...localState.typeFilter, type], - })) - } - > - {fieldTypeNames[type]} - - ))} - /> - - { - onToggleEmptyFields(); - }} - label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', { - defaultMessage: 'Only show fields with data', - })} - data-test-subj="lnsEmptyFilter" - /> - - - } - clear={{ - title: i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', { - defaultMessage: 'Clear name and type filters', - }), - 'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', { - defaultMessage: 'Clear name and type filters', - }), - onClick: () => { +
+ { + setLocalState(s => ({ + ...s, + nameFilter: '', + typeFilter: [], + })); + }, + }} + > + { + setLocalState({ ...localState, nameFilter: e.target.value }); + }} + aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { + defaultMessage: 'Search fields', + })} + /> + +
+
+ setLocalState(s => ({ ...localState, isTypeFilterOpen: false }))} + button={ + } + isSelected={localState.typeFilter.length ? true : false} + onClick={() => { setLocalState(s => ({ ...s, - nameFilter: '', - typeFilter: [], + isTypeFilterOpen: !localState.isTypeFilterOpen, })); - }, - }} - > - { - setLocalState({ ...localState, nameFilter: e.target.value }); }} - aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', { - defaultMessage: 'Search fields', + > + + + } + > + + {i18n.translate('xpack.lens.indexPatterns.filterByTypeLabel', { + defaultMessage: 'Filter by type', + })} + + ( + + setLocalState(s => ({ + ...s, + typeFilter: localState.typeFilter.includes(type) + ? localState.typeFilter.filter(t => t !== type) + : [...localState.typeFilter, type], + })) + } + > + {fieldTypeNames[type]} + + ))} + /> + + { + onToggleEmptyFields(); + }} + label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', { + defaultMessage: 'Only show fields with data', })} + data-test-subj="lnsEmptyFilter" /> - - - + + +
{ From 16dc1f33b0712b6270e9915047db4f60a2c42739 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 1 Oct 2019 14:56:05 +0100 Subject: [PATCH 16/53] [SIEM] Update wording (#46923) * update title for events histogram * update case size for TLS table * remove redundant file --- .../hosts/events_over_time/translation.ts | 2 +- .../page/network/tls_table/translations.ts | 2 +- .../public/pages/hosts/hosts_navigations.tsx | 387 ------------------ 3 files changed, 2 insertions(+), 389 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/translation.ts b/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/translation.ts index a2d7036f050368..5f68a1a1cae7d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/translation.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/events_over_time/translation.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const EVENT_COUNT_FREQUENCY_BY_ACTION = i18n.translate( 'xpack.siem.eventsOverTime.eventCountFrequencyByActionTitle', { - defaultMessage: 'Event count frequency by action', + defaultMessage: 'Event count by action', } ); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts index d81ba115747f51..89d0f58684cbe2 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const TRANSPORT_LAYER_SECURITY = i18n.translate( 'xpack.siem.network.ipDetails.tlsTable.transportLayerSecurityTitle', { - defaultMessage: 'Transport layer security', + defaultMessage: 'Transport Layer Security', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx deleted file mode 100644 index 37283b7d4aa8ed..00000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx +++ /dev/null @@ -1,387 +0,0 @@ -/* - * 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 { StaticIndexPattern } from 'ui/index_patterns'; -import { getOr, omit } from 'lodash/fp'; -import React from 'react'; -import { EuiSpacer } from '@elastic/eui'; -import * as i18n from './translations'; - -import { HostsTable, UncommonProcessTable } from '../../components/page/hosts'; - -import { HostsQuery } from '../../containers/hosts'; -import { AuthenticationTable } from '../../components/page/hosts/authentications_table'; -import { AnomaliesHostTable } from '../../components/ml/tables/anomalies_host_table'; -import { UncommonProcessesQuery } from '../../containers/uncommon_processes'; -import { InspectQuery, Refetch } from '../../store/inputs/model'; -import { NarrowDateRange } from '../../components/ml/types'; -import { hostsModel } from '../../store'; -import { manageQuery } from '../../components/page/manage_query'; -import { AuthenticationsQuery } from '../../containers/authentications'; -import { ESTermQuery } from '../../../common/typed_json'; -import { HostsTableType } from '../../store/hosts/model'; -import { StatefulEventsViewer } from '../../components/events_viewer'; -import { NavTab } from '../../components/navigation/types'; -import { EventsOverTimeQuery } from '../../containers/events/events_over_time'; -import { EventsOverTimeHistogram } from '../../components/page/hosts/events_over_time'; -import { UpdateDateRange } from '../../components/charts/common'; - -const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/hosts/${tabName}`; -const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => { - return `#/hosts/${hostName}/${tabName}`; -}; - -type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & - HostsTableType.authentications & - HostsTableType.uncommonProcesses & - HostsTableType.events; - -type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; - -export type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; - -type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications & - HostsTableType.uncommonProcesses & - HostsTableType.events; - -type KeyHostDetailsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & - HostsTableType.anomalies; - -export type KeyHostDetailsNavTab = - | KeyHostDetailsNavTabWithoutMlPermission - | KeyHostDetailsNavTabWithMlPermission; - -export type HostsNavTab = Record; - -export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { - const hostsNavTabs = { - [HostsTableType.hosts]: { - id: HostsTableType.hosts, - name: i18n.NAVIGATION_ALL_HOSTS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.hosts), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.authentications]: { - id: HostsTableType.authentications, - name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.authentications), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.uncommonProcesses]: { - id: HostsTableType.uncommonProcesses, - name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.anomalies]: { - id: HostsTableType.anomalies, - name: i18n.NAVIGATION_ANOMALIES_TITLE, - href: getTabsOnHostsUrl(HostsTableType.anomalies), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.events]: { - id: HostsTableType.events, - name: i18n.NAVIGATION_EVENTS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.events), - disabled: false, - urlKey: 'host', - }, - }; - - return hasMlUserPermissions ? hostsNavTabs : omit([HostsTableType.anomalies], hostsNavTabs); -}; - -export const navTabsHostDetails = ( - hostName: string, - hasMlUserPermissions: boolean -): Record => { - const hostDetailsNavTabs = { - [HostsTableType.authentications]: { - id: HostsTableType.authentications, - name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.authentications), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.uncommonProcesses]: { - id: HostsTableType.uncommonProcesses, - name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.uncommonProcesses), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.anomalies]: { - id: HostsTableType.anomalies, - name: i18n.NAVIGATION_ANOMALIES_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.anomalies), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.events]: { - id: HostsTableType.events, - name: i18n.NAVIGATION_EVENTS_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - }; - - return hasMlUserPermissions - ? hostDetailsNavTabs - : omit(HostsTableType.anomalies, hostDetailsNavTabs); -}; - -interface OwnProps { - type: hostsModel.HostsType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; - kqlQueryExpression: string; -} -export type HostsComponentsQueryProps = OwnProps & { - deleteQuery?: ({ id }: { id: string }) => void; - indexPattern: StaticIndexPattern; - skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; - updateDateRange?: UpdateDateRange; - filterQueryExpression?: string; - hostName?: string; -}; - -export type AnomaliesQueryTabBodyProps = OwnProps & { - skip: boolean; - narrowDateRange: NarrowDateRange; - hostName?: string; -}; - -const AuthenticationTableManage = manageQuery(AuthenticationTable); -const HostsTableManage = manageQuery(HostsTable); -const UncommonProcessTableManage = manageQuery(UncommonProcessTable); - -export const HostsQueryTabBody = ({ - deleteQuery, - endDate, - filterQuery, - indexPattern, - skip, - setQuery, - startDate, - type, -}: HostsComponentsQueryProps) => { - return ( - - {({ hosts, totalCount, loading, pageInfo, loadPage, id, inspect, isInspected, refetch }) => ( - - )} - - ); -}; - -export const AuthenticationsQueryTabBody = ({ - deleteQuery, - endDate, - filterQuery, - skip, - setQuery, - startDate, - type, -}: HostsComponentsQueryProps) => { - return ( - - {({ - authentications, - totalCount, - loading, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => { - return ( - - ); - }} - - ); -}; - -export const UncommonProcessTabBody = ({ - deleteQuery, - endDate, - filterQuery, - skip, - setQuery, - startDate, - type, -}: HostsComponentsQueryProps) => { - return ( - - {({ - uncommonProcesses, - totalCount, - loading, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - - ); -}; - -export const AnomaliesTabBody = ({ - endDate, - skip, - startDate, - type, - narrowDateRange, - hostName, -}: AnomaliesQueryTabBodyProps) => { - return ( - - ); -}; -const EventsOverTimeManage = manageQuery(EventsOverTimeHistogram); - -export const EventsTabBody = ({ - endDate, - kqlQueryExpression, - startDate, - setQuery, - filterQuery, - updateDateRange = () => {}, -}: HostsComponentsQueryProps) => { - const HOSTS_PAGE_TIMELINE_ID = 'hosts-page'; - - return ( - <> - - {({ eventsOverTime, loading, id, inspect, refetch, totalCount }) => ( - - )} - - - - - ); -}; From 4d432f1d9c4512f5f7c223ce87fa5fb5ed4e5381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 1 Oct 2019 15:59:16 +0200 Subject: [PATCH 17/53] Update CODEOWNERS (#47029) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 516f5e71577e6d..bea2f4e74297e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,8 @@ # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui +/x-pack/test/functional/apps/apm/ @elastic/apm-ui +/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui # Beats /x-pack/legacy/plugins/beats_management/ @elastic/beats From c0465258d06b596991f45dcfadf49e25d29ff038 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 1 Oct 2019 16:59:37 +0300 Subject: [PATCH 18/53] Preparation for move core_plugins\kibana\common\field_formats into data plugin (#46921) * Preparation for move core_plugins\kibana\common\field_formats into data plugin Related to: #44973 * Fix PR Comments --- .../types/{boolean.js => boolean.ts} | 18 +++++--- .../content_types/html_content_type.ts | 24 +++++------ .../field_formats/content_types/index.ts | 4 +- .../content_types/text_content_type.ts | 12 +++--- .../common/field_formats/converters/custom.ts | 8 ++-- .../common/field_formats/field_format.test.ts | 6 +-- .../data/common/field_formats/field_format.ts | 43 +++++++++++-------- .../data/common/field_formats/types.ts | 7 +-- 8 files changed, 64 insertions(+), 58 deletions(-) rename src/legacy/core_plugins/kibana/common/field_formats/types/{boolean.js => boolean.ts} (82%) diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js b/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts similarity index 82% rename from src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js rename to src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts index 0e1e1ecf058b9e..c232f65143d859 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts @@ -17,11 +17,19 @@ * under the License. */ -import { asPrettyString } from '../../../../../../plugins/data/common/field_formats'; +import { + FieldFormat, + asPrettyString, + KBN_FIELD_TYPES, +} from '../../../../../../plugins/data/common'; -export function createBoolFormat(FieldFormat) { +export function createBoolFormat() { return class BoolFormat extends FieldFormat { - _convert(value) { + static id = 'boolean'; + static title = 'Boolean'; + static fieldType = [KBN_FIELD_TYPES.BOOLEAN, KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]; + + _convert(value: any): string { if (typeof value === 'string') { value = value.trim().toLowerCase(); } @@ -41,9 +49,5 @@ export function createBoolFormat(FieldFormat) { return asPrettyString(value); } } - - static id = 'boolean'; - static title = 'Boolean'; - static fieldType = ['boolean', 'number', 'string']; }; } diff --git a/src/plugins/data/common/field_formats/content_types/html_content_type.ts b/src/plugins/data/common/field_formats/content_types/html_content_type.ts index 65b7944c5978ac..6bad1ef2b56c73 100644 --- a/src/plugins/data/common/field_formats/content_types/html_content_type.ts +++ b/src/plugins/data/common/field_formats/content_types/html_content_type.ts @@ -17,17 +17,17 @@ * under the License. */ import { escape, isFunction } from 'lodash'; -import { FieldFormatConvert, IFieldFormat, HtmlConventTypeConvert } from '../types'; +import { FieldFormatConvert, IFieldFormat, HtmlContextTypeConvert } from '../types'; import { asPrettyString, getHighlightHtml } from '../utils'; -const CONTEXT_TYPE = 'html'; +export const HTML_CONTEXT_TYPE = 'html'; const getConvertFn = ( format: IFieldFormat, - fieldFormatConvert: FieldFormatConvert -): HtmlConventTypeConvert => { - const fallbackHtml: HtmlConventTypeConvert = (value, field, hit) => { + fieldFormatConvert: Partial +): HtmlContextTypeConvert => { + const fallbackHtml: HtmlContextTypeConvert = (value, field, hit) => { const formatted = escape(format.convert(value, 'text')); return !field || !hit || !hit.highlight || !hit.highlight[field.name] @@ -35,16 +35,16 @@ const getConvertFn = ( : getHighlightHtml(formatted, hit.highlight[field.name]); }; - return (fieldFormatConvert[CONTEXT_TYPE] || fallbackHtml) as HtmlConventTypeConvert; + return (fieldFormatConvert[HTML_CONTEXT_TYPE] || fallbackHtml) as HtmlContextTypeConvert; }; export const setup = ( format: IFieldFormat, - fieldFormatConvert: FieldFormatConvert -): FieldFormatConvert => { + fieldFormatConvert: Partial +): HtmlContextTypeConvert => { const convert = getConvertFn(format, fieldFormatConvert); - const recurse: HtmlConventTypeConvert = (value, field, hit, meta) => { + const recurse: HtmlContextTypeConvert = (value, field, hit, meta) => { if (value == null) { return asPrettyString(value); } @@ -63,11 +63,9 @@ export const setup = ( return subValues.join(',' + (useMultiLine ? '\n' : ' ')); }; - const wrap: HtmlConventTypeConvert = (value, field, hit, meta) => { + const wrap: HtmlContextTypeConvert = (value, field, hit, meta) => { return `${recurse(value, field, hit, meta)}`; }; - return { - [CONTEXT_TYPE]: wrap, - }; + return wrap; }; diff --git a/src/plugins/data/common/field_formats/content_types/index.ts b/src/plugins/data/common/field_formats/content_types/index.ts index b5d98a7bc83936..d391ba72d9f45d 100644 --- a/src/plugins/data/common/field_formats/content_types/index.ts +++ b/src/plugins/data/common/field_formats/content_types/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { setup as textContentTypeSetup } from './text_content_type'; -export { setup as htmlContentTypeSetup } from './html_content_type'; +export { setup as textContentTypeSetup, TEXT_CONTEXT_TYPE } from './text_content_type'; +export { setup as htmlContentTypeSetup, HTML_CONTEXT_TYPE } from './html_content_type'; diff --git a/src/plugins/data/common/field_formats/content_types/text_content_type.ts b/src/plugins/data/common/field_formats/content_types/text_content_type.ts index 40a450fae7067f..b0d3522f742a5b 100644 --- a/src/plugins/data/common/field_formats/content_types/text_content_type.ts +++ b/src/plugins/data/common/field_formats/content_types/text_content_type.ts @@ -22,15 +22,15 @@ import { IFieldFormat, FieldFormatConvert, TextContextTypeConvert } from '../typ import { asPrettyString } from '../utils'; -const CONTEXT_TYPE = 'text'; +export const TEXT_CONTEXT_TYPE = 'text'; -const getConvertFn = (fieldFormatConvert: FieldFormatConvert): TextContextTypeConvert => - (fieldFormatConvert[CONTEXT_TYPE] || asPrettyString) as TextContextTypeConvert; +const getConvertFn = (fieldFormatConvert: Partial): TextContextTypeConvert => + (fieldFormatConvert[TEXT_CONTEXT_TYPE] || asPrettyString) as TextContextTypeConvert; export const setup = ( format: IFieldFormat, - fieldFormatConvert: FieldFormatConvert -): FieldFormatConvert => { + fieldFormatConvert: Partial +): TextContextTypeConvert => { const convert = getConvertFn(fieldFormatConvert); const recurse: TextContextTypeConvert = value => { @@ -42,5 +42,5 @@ export const setup = ( return JSON.stringify(value.map(recurse)); }; - return { [CONTEXT_TYPE]: recurse }; + return recurse; }; diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index bc9b4211272281..3d562f97716be6 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -18,15 +18,13 @@ */ import { FieldFormat } from '../field_format'; -import { FieldFormatConvert } from '../types'; +import { FieldFormatConvertFunction } from '../types'; const ID = 'custom'; -export const createCustomFieldFormat = (convert: FieldFormatConvert) => +export const createCustomFieldFormat = (convert: FieldFormatConvertFunction) => class CustomFieldFormat extends FieldFormat { static id = ID; - public get _convert() { - return convert; - } + _convert = convert; }; diff --git a/src/plugins/data/common/field_formats/field_format.test.ts b/src/plugins/data/common/field_formats/field_format.test.ts index 8e66eac85eb3ce..e4a64e91d1fff4 100644 --- a/src/plugins/data/common/field_formats/field_format.test.ts +++ b/src/plugins/data/common/field_formats/field_format.test.ts @@ -23,7 +23,7 @@ import { FieldFormatConvert } from './types'; import { asPrettyString } from './utils/as_pretty_string'; const getTestFormat = ( - _convert: FieldFormatConvert = { + _convert: Partial = { text: (val: string) => asPrettyString(val), }, _params?: any @@ -32,9 +32,7 @@ const getTestFormat = ( static id = 'test-format'; static title = 'Test Format'; - public get _convert() { - return _convert; - } + _convert = _convert; })(_params); describe('FieldFormat class', () => { diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 226631660b8a0f..cdf82cd9eb9d1a 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -21,10 +21,18 @@ import { isFunction, transform, size, cloneDeep, get, defaults } from 'lodash'; import { createCustomFieldFormat } from './converters/custom'; import { ContentType, FieldFormatConvert, FieldFormatConvertFunction } from './types'; -import { htmlContentTypeSetup, textContentTypeSetup } from './content_types'; +import { + htmlContentTypeSetup, + textContentTypeSetup, + TEXT_CONTEXT_TYPE, + HTML_CONTEXT_TYPE, +} from './content_types'; const DEFAULT_CONTEXT_TYPE = 'text'; +export const isFieldFormatConvertFn = (convert: any): convert is FieldFormatConvertFunction => + isFunction(convert); + export abstract class FieldFormat { /** * @property {string} - Field Format Id @@ -43,19 +51,19 @@ export abstract class FieldFormat { * @property {string} - Field Format Type * @private */ - static fieldType: string; + static fieldType: string | string[]; /** * @property {FieldFormatConvert} * @private */ - _convert: FieldFormatConvert = FieldFormat.setupContentType(this, get(this, '_convert', {})); + private convertObject: FieldFormatConvert | undefined; /** * @property {Function} - ref to child class * @private */ - type: any = this.constructor; + public type: any = this.constructor; constructor(public _params: any = {}) {} @@ -88,11 +96,11 @@ export abstract class FieldFormat { getConverterFor( contentType: ContentType = DEFAULT_CONTEXT_TYPE ): FieldFormatConvertFunction | null { - if (this._convert) { - return this._convert[contentType]; + if (!this.convertObject) { + this.convertObject = FieldFormat.setupContentType(this, get(this, '_convert')); } - return null; + return this.convertObject[contentType] || null; } /** @@ -160,32 +168,31 @@ export abstract class FieldFormat { } static from(convertFn: FieldFormatConvertFunction) { - return createCustomFieldFormat(FieldFormat.toConvertObject(convertFn)); + return createCustomFieldFormat(convertFn); } private static setupContentType( fieldFormat: IFieldFormat, - convert: FieldFormatConvert | FieldFormatConvertFunction + convert: Partial | FieldFormatConvertFunction = {} ): FieldFormatConvert { - const convertObject = FieldFormat.toConvertObject(convert); + const convertObject = isFieldFormatConvertFn(convert) + ? FieldFormat.toConvertObject(convert) + : convert; return { - ...textContentTypeSetup(fieldFormat, convertObject), - ...htmlContentTypeSetup(fieldFormat, convertObject), + [TEXT_CONTEXT_TYPE]: textContentTypeSetup(fieldFormat, convertObject), + [HTML_CONTEXT_TYPE]: htmlContentTypeSetup(fieldFormat, convertObject), }; } - private static toConvertObject( - convert: FieldFormatConvert | FieldFormatConvertFunction - ): FieldFormatConvert { - if (isFunction(convert)) { + private static toConvertObject(convert: FieldFormatConvertFunction): Partial { + if (isFieldFormatConvertFn(convert)) { return { - [DEFAULT_CONTEXT_TYPE]: convert, + [TEXT_CONTEXT_TYPE]: convert, }; } return convert; } } -export type FieldFormatConvert = { [key: string]: Function } | FieldFormatConvertFunction; export type IFieldFormat = PublicMethodsOf; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 39cf34b7103945..626bab297392b4 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -24,7 +24,7 @@ export type ContentType = 'html' | 'text'; export { IFieldFormat } from './field_format'; /** @internal **/ -export type HtmlConventTypeConvert = ( +export type HtmlContextTypeConvert = ( value: any, field?: any, hit?: Record, @@ -35,9 +35,10 @@ export type HtmlConventTypeConvert = ( export type TextContextTypeConvert = (value: any) => string; /** @internal **/ -export type FieldFormatConvertFunction = HtmlConventTypeConvert | TextContextTypeConvert; +export type FieldFormatConvertFunction = HtmlContextTypeConvert | TextContextTypeConvert; /** @internal **/ export interface FieldFormatConvert { - [key: string]: FieldFormatConvertFunction; + text: TextContextTypeConvert; + html: HtmlContextTypeConvert; } From a325611c407358b6ef90bcd1bef07e1edf704b0b Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 1 Oct 2019 19:11:58 +0500 Subject: [PATCH 19/53] [Uptime] added aria label description for ping over time chart (#46689) Fixes #35352 To Improve accessibility added aria-label to chart container to make it readable. --- .../charts/chart_wrapper/chart_wrapper.tsx | 20 +++++++++++-- .../functional/charts/snapshot_histogram.tsx | 28 +++++++++++++------ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/chart_wrapper.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/chart_wrapper.tsx index 44b3cfba5d764f..dd2853b60c87fe 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/chart_wrapper.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/chart_wrapper/chart_wrapper.tsx @@ -4,15 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment } from 'react'; +import React, { FC, Fragment, HTMLAttributes } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart } from '@elastic/eui'; interface Props { + /** + * Height for the chart + */ height?: string; + /** + * if chart data source is still loading + */ loading?: boolean; + /** + * aria-label for accessibility + */ + 'aria-label'?: string; } -export const ChartWrapper: FC = ({ loading = false, height = '100%', children }) => { +export const ChartWrapper: FC = ({ + loading = false, + height = '100%', + children, + ...rest +}) => { const opacity = loading === true ? 0.3 : 1; return ( @@ -23,6 +38,7 @@ export const ChartWrapper: FC = ({ loading = false, height = '100%', chil opacity, transition: 'opacity 0.2s', }} + {...(rest as HTMLAttributes)} > {children}
diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx index 6d16526f3ac476..54aa6f8cf16443 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/snapshot_histogram.tsx @@ -16,8 +16,9 @@ import { } from '@elastic/charts'; import { EuiEmptyPrompt, EuiTitle, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; import { HistogramDataPoint } from '../../../../common/graphql/types'; import { getColorsMap } from './get_colors_map'; import { getChartDateLabel } from '../../../lib/helper'; @@ -44,7 +45,7 @@ export interface SnapshotHistogramProps { dangerColor: string; /** - * Height is needed, since by defauly charts takes height of 100% + * Height is needed, since by default charts takes height of 100% */ height?: string; } @@ -66,11 +67,11 @@ export const SnapshotHistogramComponent = ({ }: Props) => { if (!data || !data.histogram) /** - * TODO: the Fragment, EuiTitle, and EuiPanel should be extractec to a dumb component + * TODO: the Fragment, EuiTitle, and EuiPanel should be extracted to a dumb component * that we can reuse in the subsequent return statement at the bottom of this function. */ return ( - + <>
- + ); const { histogram } = data; const downMonitorsName = i18n.translate('xpack.uptime.snapshotHistogram.downMonitorsId', { @@ -114,7 +115,7 @@ export const SnapshotHistogramComponent = ({ }); const upSpecId = getSpecId(upMonitorsId); return ( - + <>
@@ -124,7 +125,18 @@ export const SnapshotHistogramComponent = ({ />
- +
-
+ ); }; From 68df39f758b4cacced4ca1e85795f31d50e779d0 Mon Sep 17 00:00:00 2001 From: igoristic Date: Tue, 1 Oct 2019 12:02:59 -0400 Subject: [PATCH 20/53] Issue 46223: Allow isCollectionEnabledUpdated to hang until data is available (#46279) * Fixed reason check * Continue render if reason is not null --- .../explanations/collection_enabled/collection_enabled.js | 4 ++-- .../public/lib/elasticsearch_settings/enabler.js | 2 +- .../plugins/monitoring/public/views/no_data/controller.js | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js b/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js index 48883e43e0ebf9..50b67cbda6f3de 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js +++ b/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js @@ -39,10 +39,10 @@ export class ExplainCollectionEnabled extends React.Component { const { enabler } = this.props; enabler.enableCollectionEnabled(); - // wait 19 seconds, show link to reload + // wait 22 seconds, show link to reload this.waitedTooLongTimer = setTimeout(() => { this.setState({ waitedTooLong: true }); - }, 19 * 1000); + }, 22 * 1000); } render() { diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js b/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js index 8c25a79f12fb8d..f6679611441235 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js @@ -35,7 +35,7 @@ export class Enabler { async enableCollectionEnabled() { try { - this.updateModel({ isCollectionEnabledUpdating: true, isCollectionEnabledUpdated: false }); + this.updateModel({ isCollectionEnabledUpdating: true }); await this.$http.put('../api/monitoring/v1/elasticsearch_settings/set/collection_enabled'); this.updateModel({ isCollectionEnabledUpdated: true, diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js index 5ad68268ed31fa..0fef0fbe125e2e 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js +++ b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js @@ -72,7 +72,12 @@ export class NoDataController extends MonitoringViewBaseController { //Need to set updateModel after super since there is no `this` otherwise const { updateModel } = new ModelUpdater($scope, this); const enabler = new Enabler($http, updateModel); - $scope.$watch(() => this, () => this.render(enabler), true); + $scope.$watch(() => this, () => { + if (this.isCollectionEnabledUpdated && !this.reason) { + return; + } + this.render(enabler); + }, true); } getDefaultModel() { From 29c5d6755bbf49e6d927a373ec368a0120ec3233 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 1 Oct 2019 12:36:48 -0400 Subject: [PATCH 21/53] [Uptime] Fix flaky unit test snapshot (#46492) * Extract test helper function for reuse. * Modify unit test to avoid flaky behavior. * Fix outputted error string in test helper. * Add test file for test helper function. * run x-pack-intake job 40 times * Revert "run x-pack-intake job 40 times" This reverts commit 53b520c98f563e849afc01c92cdf1310bc6d8b14. --- ...asticsearch_monitor_states_adapter.test.ts | 25 +++++++++++++++++++ .../elasticsearch_monitors_adapter.test.ts | 7 +----- .../assert_close_to.test.ts.snap | 3 +++ .../helper/__test__/assert_close_to.test.ts | 21 ++++++++++++++++ .../server/lib/helper/assert_close_to.ts | 11 ++++++++ .../plugins/uptime/server/lib/helper/index.ts | 1 + 6 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts create mode 100644 x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/elasticsearch_monitor_states_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/elasticsearch_monitor_states_adapter.test.ts index 67a66362e4cc50..1a90699090b29f 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/elasticsearch_monitor_states_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/__tests__/elasticsearch_monitor_states_adapter.test.ts @@ -8,6 +8,8 @@ import { DatabaseAdapter } from '../../database'; import exampleFilter from './example_filter.json'; import monitorState from './monitor_states_docs.json'; import { ElasticsearchMonitorStatesAdapter } from '../elasticsearch_monitor_states_adapter'; +import { get, set } from 'lodash'; +import { assertCloseTo } from '../../../helper'; describe('ElasticsearchMonitorStatesAdapter', () => { let database: DatabaseAdapter; @@ -37,6 +39,7 @@ describe('ElasticsearchMonitorStatesAdapter', () => { }); it('applies an appropriate filter section to the query based on filters', async () => { + expect.assertions(3); const adapter = new ElasticsearchMonitorStatesAdapter(database); await adapter.legacyGetMonitorStates( {}, @@ -45,6 +48,28 @@ describe('ElasticsearchMonitorStatesAdapter', () => { JSON.stringify(exampleFilter), 'down' ); + expect(searchMock).toHaveBeenCalledTimes(3); + const fixedInterval = parseInt( + get( + searchMock.mock.calls[2][1], + 'body.aggs.by_id.aggs.histogram.date_histogram.fixed_interval', + '' + ).split('ms')[0], + 10 + ); + expect(fixedInterval).not.toBeNaN(); + /** + * This value can sometimes be off by 1 as a result of fuzzy calculation. + * + * It had no implications in practice, but from a test standpoint can cause flaky + * snapshot failures. + */ + assertCloseTo(fixedInterval, 36000, 100); + set( + searchMock.mock.calls[2][1], + 'body.aggs.by_id.aggs.histogram.date_histogram.fixed_interval', + '36000ms' + ); expect(searchMock.mock.calls).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts index 9fce667adf613c..b21fb982bfb3bb 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/__tests__/elasticsearch_monitors_adapter.test.ts @@ -8,12 +8,7 @@ import { get, set } from 'lodash'; import { ElasticsearchMonitorsAdapter } from '../elasticsearch_monitors_adapter'; import { CountParams, CountResponse } from 'elasticsearch'; import mockChartsData from './monitor_charts_mock.json'; - -const assertCloseTo = (actual: number, expected: number, precision: number) => { - if (Math.abs(expected - actual) > precision) { - throw new Error(`expected [${actual}] to be within ${precision} of ${actual}`); - } -}; +import { assertCloseTo } from '../../../helper'; // FIXME: there are many untested functions in this adapter. They should be tested. describe('ElasticsearchMonitorsAdapter', () => { diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap new file mode 100644 index 00000000000000..7dfb8c88be1ca4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/__snapshots__/assert_close_to.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`assertCloseTo throws an error when expected value is outside of precision range 1`] = `"expected [12500] to be within 100 of 10000"`; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts new file mode 100644 index 00000000000000..6ccedcf7153dc4 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/__test__/assert_close_to.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { assertCloseTo } from '../assert_close_to'; + +describe('assertCloseTo', () => { + it('does not throw an error when expected value is correct', () => { + assertCloseTo(10000, 10001, 100); + }); + + it('does not throw an error when expected value is under actual, but within precision threshold', () => { + assertCloseTo(10000, 9875, 300); + }); + + it('throws an error when expected value is outside of precision range', () => { + expect(() => assertCloseTo(10000, 12500, 100)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts new file mode 100644 index 00000000000000..13b6f3688809c5 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/assert_close_to.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export const assertCloseTo = (actual: number, expected: number, precision: number) => { + if (Math.abs(expected - actual) > precision) { + throw new Error(`expected [${expected}] to be within ${precision} of ${actual}`); + } +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts index 19d5a3b00237df..a2a72825c6b98d 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/helper/index.ts @@ -9,3 +9,4 @@ export { formatEsBucketsForHistogram } from './format_es_buckets_for_histogram'; export { getFilterClause } from './get_filter_clause'; export { getHistogramInterval } from './get_histogram_interval'; export { parseFilterQuery } from './parse_filter_query'; +export { assertCloseTo } from './assert_close_to'; From 073d1b07d97838614a0520164da1646a583eb3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 1 Oct 2019 19:24:05 +0200 Subject: [PATCH 22/53] Removing 0 sufix if array contains only one element (#47036) --- .../__test__/DottedKeyValueTable.test.tsx | 27 +++++++++++++++++++ .../shared/DottedKeyValueTable/index.tsx | 4 ++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/__test__/DottedKeyValueTable.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/__test__/DottedKeyValueTable.test.tsx index deecd955293bb4..aebc86352a7745 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/__test__/DottedKeyValueTable.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/__test__/DottedKeyValueTable.test.tsx @@ -104,4 +104,31 @@ describe('DottedKeyValueTable', () => { 'top.name.last' ]); }); + + it('should not add 0 sufix if value is an array with one element', () => { + const data = { + a: { + b: { + c1: ['foo', 'bar'], + c2: ['foo'] + } + }, + b: { + c: ['foo'] + }, + c: { + d: ['foo', 'bar'] + } + }; + const output = render(); + + expect(getKeys(output)).toEqual([ + 'a.b.c1.0', + 'a.b.c1.1', + 'a.b.c2', + 'b.c', + 'c.d.0', + 'c.d.1' + ]); + }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/index.tsx index baeea829f401f2..674df14c62eb0b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DottedKeyValueTable/index.tsx @@ -34,10 +34,12 @@ export function pathify( item: StringMap, { maxDepth, parentKey = '', depth = 0 }: PathifyOptions ): PathifyResult { + const isArrayWithSingleValue = Array.isArray(item) && item.length === 1; return Object.keys(item) .sort() .reduce((pathified, key) => { - const currentKey = compact([parentKey, key]).join('.'); + const childKey = isArrayWithSingleValue ? '' : key; + const currentKey = compact([parentKey, childKey]).join('.'); if ((!maxDepth || depth + 1 <= maxDepth) && isObject(item[key])) { return { ...pathified, From 072f332385d0500659cf5966f5eae221ea4841af Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 1 Oct 2019 13:02:52 -0500 Subject: [PATCH 23/53] time picker to filter (#47055) --- src/legacy/core_plugins/kibana/ui_setting_defaults.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 8592242e7ff20c..191ae7309f46ff 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -811,7 +811,7 @@ export function getUiSettingDefaults() { }, 'timepicker:timeDefaults': { name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { - defaultMessage: 'Time picker defaults', + defaultMessage: 'Time filter defaults', }), value: `{ @@ -826,7 +826,7 @@ export function getUiSettingDefaults() { }, 'timepicker:refreshIntervalDefaults': { name: i18n.translate('kbn.advancedSettings.timepicker.refreshIntervalDefaultsTitle', { - defaultMessage: 'Time picker refresh interval', + defaultMessage: 'Time filter refresh interval', }), value: `{ @@ -841,7 +841,7 @@ export function getUiSettingDefaults() { }, 'timepicker:quickRanges': { name: i18n.translate('kbn.advancedSettings.timepicker.quickRangesTitle', { - defaultMessage: 'Time picker quick ranges', + defaultMessage: 'Time filter quick ranges', }), value: JSON.stringify([ { @@ -897,7 +897,7 @@ export function getUiSettingDefaults() { type: 'json', description: i18n.translate('kbn.advancedSettings.timepicker.quickRangesText', { defaultMessage: - 'The list of ranges to show in the Quick section of the time picker. This should be an array of objects, ' + + 'The list of ranges to show in the Quick section of the time filter. This should be an array of objects, ' + 'with each object containing "from", "to" (see {acceptedFormatsLink}), and ' + '"display" (the title to be displayed).', description: From de841f72808a66024d300c0f0fd10c8ef2411721 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Tue, 1 Oct 2019 15:04:00 -0400 Subject: [PATCH 24/53] [Lens] Default to stacked bar and reduce number of suggestions (#46721) --- .../editor_frame/_suggestion_panel.scss | 1 + x-pack/legacy/plugins/lens/public/index.scss | 1 + .../metric_visualization_plugin/index.scss | 9 + .../metric_expression.test.tsx | 60 ++---- .../metric_expression.tsx | 13 +- .../__snapshots__/xy_expression.test.tsx.snap | 7 - .../_xy_expression.scss | 3 +- .../xy_visualization_plugin/xy_expression.tsx | 4 +- .../xy_suggestions.test.ts | 195 +++++++++++------- .../xy_visualization_plugin/xy_suggestions.ts | 89 ++++---- .../xy_visualization.test.ts | 4 +- .../xy_visualization.tsx | 2 +- 12 files changed, 200 insertions(+), 188 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss index a44fe7ee68dd09..01a6954671809d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss @@ -39,6 +39,7 @@ } .lnsSuggestionPanel__chartWrapper { + display: flex; height: $lnsSuggestionHeight - $euiSize; pointer-events: none; margin: 0 $euiSizeS; diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 9864690cff0936..f646b1ed0a9ae7 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -12,3 +12,4 @@ @import './editor_frame_plugin/index'; @import './indexpattern_plugin/index'; @import './xy_visualization_plugin/index'; +@import './metric_visualization_plugin/index'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss new file mode 100644 index 00000000000000..23390315399971 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss @@ -0,0 +1,9 @@ +.lnsMetricExpression__container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + text-align: center; +} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index ccda4fb3484ad0..f82def178261b0 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -53,18 +53,8 @@ describe('metric_expression', () => { expect(shallow( x as FieldFormat} />)) .toMatchInlineSnapshot(`
{ /> ) ).toMatchInlineSnapshot(` - - -
- 10110 -
-
-
- `); + + +
+ 10110 +
+
+
+ `); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index 6abaf7d63d54dd..68de585771a798 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -106,18 +106,7 @@ export function MetricChart({ } return ( - +
{value} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index 048cbb0811432d..f9a5a73eda2702 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -2,7 +2,6 @@ exports[`xy_expression XYChart component it renders area 1`] = ` + ); @@ -177,7 +177,7 @@ export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderPr } return ( - + { expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "area_stacked", - "splitAccessor": "aaa", - "x": "date", - "y": Array [ - "bytes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": "aaa", + "x": "date", + "y": Array [ + "bytes", + ], + }, + ] + `); }); test('does not suggest multiple splits', () => { @@ -161,18 +161,18 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "area_stacked", - "splitAccessor": "product", - "x": "date", - "y": Array [ - "price", - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": "product", + "x": "date", + "y": Array [ + "price", + "quantity", + ], + }, + ] + `); }); test('uses datasource provided title if available', () => { @@ -232,7 +232,43 @@ describe('xy_suggestions', () => { expect(suggestion.hide).toBeFalsy(); }); - test('suggests an area chart for unchanged table and existing bar chart on non-ordinal x axis', () => { + test('only makes a seriesType suggestion for unchanged table without split', () => { + (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); + const currentState: XYState = { + isHorizontal: false, + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'dummyCol', + xAccessor: 'date', + }, + ], + }; + const suggestions = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('price'), dateCol('date')], + layerId: 'first', + changeType: 'unchanged', + }, + state: currentState, + }); + + expect(suggestions).toHaveLength(1); + + expect(suggestions[0].state).toEqual({ + ...currentState, + preferredSeriesType: 'line', + layers: [{ ...currentState.layers[0], seriesType: 'line' }], + }); + expect(suggestions[0].title).toEqual('Line chart'); + }); + + test('suggests seriesType and stacking when there is a split', () => { const currentState: XYState = { isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, @@ -247,7 +283,7 @@ describe('xy_suggestions', () => { }, ], }; - const [suggestion, ...rest] = getSuggestions({ + const [seriesSuggestion, stackSuggestion, ...rest] = getSuggestions({ table: { isMultiRow: true, columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], @@ -257,14 +293,19 @@ describe('xy_suggestions', () => { state: currentState, }); - expect(rest).toHaveLength(1); - expect(suggestion.state).toEqual({ + expect(rest).toHaveLength(0); + expect(seriesSuggestion.state).toEqual({ + ...currentState, + preferredSeriesType: 'line', + layers: [{ ...currentState.layers[0], seriesType: 'line' }], + }); + expect(stackSuggestion.state).toEqual({ ...currentState, - preferredSeriesType: 'area', - layers: [{ ...currentState.layers[0], seriesType: 'area' }], + preferredSeriesType: 'bar_stacked', + layers: [{ ...currentState.layers[0], seriesType: 'bar_stacked' }], }); - expect(suggestion.previewIcon).toEqual('visArea'); - expect(suggestion.title).toEqual('Area chart'); + expect(seriesSuggestion.title).toEqual('Line chart'); + expect(stackSuggestion.title).toEqual('Stacked'); }); test('suggests a flipped chart for unchanged table and existing bar chart on ordinal x axis', () => { @@ -293,7 +334,7 @@ describe('xy_suggestions', () => { state: currentState, }); - expect(rest).toHaveLength(1); + expect(rest).toHaveLength(0); expect(suggestion.state).toEqual({ ...currentState, isHorizontal: true, @@ -301,48 +342,42 @@ describe('xy_suggestions', () => { expect(suggestion.title).toEqual('Flip'); }); - test('suggests a stacked chart for unchanged table and unstacked chart', () => { - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); + test('suggests stacking for unchanged table that has a split', () => { const currentState: XYState = { isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ { - accessors: ['price', 'quantity'], + accessors: ['price'], layerId: 'first', seriesType: 'bar', - splitAccessor: 'dummyCol', + splitAccessor: 'date', xAccessor: 'product', }, ], }; - const suggestion = getSuggestions({ + const suggestions = getSuggestions({ table: { isMultiRow: true, - columns: [numCol('price'), numCol('quantity'), strCol('product')], + columns: [numCol('price'), dateCol('date'), strCol('product')], layerId: 'first', changeType: 'unchanged', }, state: currentState, - })[1]; + }); + + const suggestion = suggestions[suggestions.length - 1]; expect(suggestion.state).toEqual({ ...currentState, preferredSeriesType: 'bar_stacked', - layers: [ - { - ...currentState.layers[0], - seriesType: 'bar_stacked', - }, - ], + layers: [{ ...currentState.layers[0], seriesType: 'bar_stacked' }], }); expect(suggestion.title).toEqual('Stacked'); }); test('keeps column to dimension mappings on extended tables', () => { - (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, @@ -431,17 +466,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "quantity", - "y": Array [ - "price", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": "ddd", + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] + `); }); test('handles ip', () => { @@ -467,17 +502,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "ddd", - "x": "myip", - "y": Array [ - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": "ddd", + "x": "myip", + "y": Array [ + "quantity", + ], + }, + ] + `); }); test('handles unbucketed suggestions', () => { @@ -502,16 +537,16 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar_stacked", - "splitAccessor": "eee", - "x": "mybool", - "y": Array [ - "num votes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar_stacked", + "splitAccessor": "eee", + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] + `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index 1f3bc9d701114e..2f28e20ebd274a 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -187,7 +187,7 @@ function getSuggestionsForLayer( // if current state is using the same data, suggest same chart with different presentational configuration - if (xValue.operation.scale === 'ordinal') { + if (seriesType !== 'line' && xValue.operation.scale === 'ordinal') { // flip between horizontal/vertical for ordinal scales sameStateSuggestions.push( buildSuggestion({ @@ -198,36 +198,38 @@ function getSuggestionsForLayer( ); } else { // change chart type for interval or ratio scales on x axis - const newSeriesType = flipSeriesType(seriesType); + const newSeriesType = altSeriesType(seriesType); sameStateSuggestions.push( buildSuggestion({ ...options, seriesType: newSeriesType, - title: newSeriesType.startsWith('area') - ? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { - defaultMessage: 'Area chart', - }) - : i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + title: newSeriesType.startsWith('bar') + ? i18n.translate('xpack.lens.xySuggestions.barChartTitle', { defaultMessage: 'Bar chart', + }) + : i18n.translate('xpack.lens.xySuggestions.lineChartTitle', { + defaultMessage: 'Line chart', }), }) ); } - // flip between stacked/unstacked - sameStateSuggestions.push( - buildSuggestion({ - ...options, - seriesType: toggleStackSeriesType(seriesType), - title: seriesType.endsWith('stacked') - ? i18n.translate('xpack.lens.xySuggestions.unstackedChartTitle', { - defaultMessage: 'Unstacked', - }) - : i18n.translate('xpack.lens.xySuggestions.stackedChartTitle', { - defaultMessage: 'Stacked', - }), - }) - ); + if (seriesType !== 'line' && splitBy) { + // flip between stacked/unstacked + sameStateSuggestions.push( + buildSuggestion({ + ...options, + seriesType: toggleStackSeriesType(seriesType), + title: seriesType.endsWith('stacked') + ? i18n.translate('xpack.lens.xySuggestions.unstackedChartTitle', { + defaultMessage: 'Unstacked', + }) + : i18n.translate('xpack.lens.xySuggestions.stackedChartTitle', { + defaultMessage: 'Stacked', + }), + }) + ); + } return sameStateSuggestions; } @@ -247,18 +249,21 @@ function toggleStackSeriesType(oldSeriesType: SeriesType) { } } -function flipSeriesType(oldSeriesType: SeriesType) { +// Until the area chart rendering bug is fixed, avoid suggesting area charts +// https://github.com/elastic/elastic-charts/issues/388 +function altSeriesType(oldSeriesType: SeriesType) { switch (oldSeriesType) { case 'area': - return 'bar'; + return 'line'; case 'area_stacked': return 'bar_stacked'; case 'bar': - return 'area'; + return 'line'; case 'bar_stacked': - return 'area_stacked'; + return 'line'; + case 'line': default: - return 'bar'; + return 'bar_stacked'; } } @@ -268,27 +273,25 @@ function getSeriesType( xValue: TableSuggestionColumn, changeType: TableChangeType ): SeriesType { - const defaultType = xValue.operation.dataType === 'date' ? 'area_stacked' : 'bar_stacked'; - const preferredSeriesType = (currentState && currentState.preferredSeriesType) || defaultType; - const isDateCompatible = - preferredSeriesType === 'area' || - preferredSeriesType === 'line' || - preferredSeriesType === 'area_stacked'; - - if (changeType !== 'initial') { - const oldLayer = getExistingLayer(currentState, layerId); - return ( - (oldLayer && oldLayer.seriesType) || - (currentState && currentState.preferredSeriesType) || - defaultType - ); + const defaultType = 'bar_stacked'; + + const oldLayer = getExistingLayer(currentState, layerId); + const oldLayerSeriesType = oldLayer ? oldLayer.seriesType : false; + + const closestSeriesType = + oldLayerSeriesType || (currentState && currentState.preferredSeriesType) || defaultType; + + // Attempt to keep the seriesType consistent on initial add of a layer + // Ordinal scales should always use a bar because there is no interpolation between buckets + if (xValue.operation.scale && xValue.operation.scale === 'ordinal') { + return closestSeriesType.startsWith('bar') ? closestSeriesType : defaultType; } - if (xValue.operation.dataType === 'date') { - return isDateCompatible ? preferredSeriesType : defaultType; + if (changeType === 'initial') { + return defaultType; } - return isDateCompatible ? defaultType : preferredSeriesType; + return closestSeriesType !== defaultType ? closestSeriesType : defaultType; } function getSuggestionTitle( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 8d9092f63f59b3..8bc7b0c9116f70 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -56,7 +56,7 @@ describe('xy_visualization', () => { ], "layerId": "", "position": "top", - "seriesType": "bar", + "seriesType": "bar_stacked", "showGridlines": false, "splitAccessor": "test-id2", "xAccessor": "test-id3", @@ -66,7 +66,7 @@ describe('xy_visualization', () => { "isVisible": true, "position": "right", }, - "preferredSeriesType": "bar", + "preferredSeriesType": "bar_stacked", "title": "Empty XY Chart", } `); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 14c35c21c73330..69cb93bb1903d0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -18,7 +18,7 @@ import { toExpression, toPreviewExpression } from './to_expression'; import { generateId } from '../id_generator'; const defaultIcon = 'visBarVertical'; -const defaultSeriesType = 'bar'; +const defaultSeriesType = 'bar_stacked'; function getDescription(state?: State) { if (!state) { From 027476f9a7f9901e63ebd2c15fa13004a5a69510 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 1 Oct 2019 13:54:17 -0600 Subject: [PATCH 25/53] [SIEM][Detection Engine] Temporary re-indexer and starting folder (#47006) ## Summary Start the detection engine work by adding experimental backend re-indexer and placeholder ECS data. * Added README.md with instructions on how to setup alerts and actions * Added temporary re-indexer with painless scripts * Added example query to move the re-indexer towards. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ ~~- [ ] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~~ ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ ~~- [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ --- x-pack/legacy/plugins/siem/index.ts | 9 + .../server/lib/detection_engine/README.md | 126 ++ .../alerts/build_events_query.ts | 88 + .../alerts/build_events_reindex.ts | 141 ++ .../alerts/signals_alert_type.ts | 82 + .../lib/detection_engine/signals_mapping.json | 1644 +++++++++++++++++ 6 files changed, 2090 insertions(+) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 78fc0a52dc3d67..4c0997e1d6181f 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -23,6 +23,7 @@ import { DEFAULT_FROM, DEFAULT_TO, } from './common/constants'; +import { signalsAlertType } from './server/lib/detection_engine/alerts/signals_alert_type'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function siem(kibana: any) { @@ -31,6 +32,11 @@ export function siem(kibana: any) { configPrefix: 'xpack.siem', publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch'], + // Uncomment these lines to turn on alerting and action for detection engine and comment the other + // require statement out. These are hidden behind feature flags at the moment so if you turn + // these on without the feature flags turned on then Kibana will crash since we are a legacy plugin + // and legacy plugins cannot have optional requirements. + // require: ['kibana', 'elasticsearch', 'alerting', 'actions'], uiExports: { app: { description: i18n.translate('xpack.siem.securityDescription', { @@ -115,6 +121,9 @@ export function siem(kibana: any) { mappings: savedObjectMappings, }, init(server: Server) { + if (server.plugins.alerting != null) { + server.plugins.alerting.registerType(signalsAlertType); + } server.injectUiAppVars('siem', async () => server.getInjectedUiAppVars('kibana')); initServerWithKibana(server); }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md new file mode 100644 index 00000000000000..063e7c1975b25b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/README.md @@ -0,0 +1,126 @@ +Temporary README.md for developers working on the backend detection engine +for how to get started. + +See these two other pages for references: +https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/alerting/README.md +https://github.com/elastic/kibana/tree/master/x-pack/legacy/plugins/actions + +Since there is no UI yet and a lot of backend areas that are not created, you +should install the kbn-action and kbn-alert project from here: +https://github.com/pmuellr/kbn-action + +Add your signal mappings into your Kibana instance manually by opening + +``` +x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json +``` + +And copying that to your DEV tools so it looks something like: +``` +PUT /.siem-signals-10-01-2019 +{ + "mappings": { + "dynamic": false, +... +``` + +We will solve the above issue here: +https://github.com/elastic/kibana/issues/47002 + +Add these lines to your `kibana.dev.yml` to turn on the feature toggles of alerting and actions: +``` +# Feature flag to turn on alerting +xpack.alerting.enabled: true + +# Feature flag to turn on actions which goes with alerting +xpack.actions.enabled: true + +# White list everything for ease of development (do not do in production) +xpack.actions.whitelistedHosts: ['*'] +``` + +Open `x-pack/legacy/plugins/siem/index.ts` and find these lines and add the require statement +while commenting out the other require statement: + +``` +// Uncomment these lines to turn on alerting and action for detection engine and comment the other +// require statement out. These are hidden behind feature flags at the moment so if you turn +// these on without the feature flags turned on then Kibana will crash since we are a legacy plugin +// and legacy plugins cannot have optional requirements. +// require: ['kibana', 'elasticsearch', 'alerting', 'actions'], +``` + +Restart Kibana and you should see alerting and actions starting up +``` +server log [22:05:22.277] [info][status][plugin:alerting@8.0.0] Status changed from uninitialized to green - Ready +server log [22:05:22.270] [info][status][plugin:actions@8.0.0] Status changed from uninitialized to green - Ready +``` + +Open a terminal and run + +```sh +kbn-alert ls-types +``` + +You should see the new alert type of: + +```ts +[ + { + "id": "siem.signals", + "name": "SIEM Signals" + } +] +``` + +Setup SIEM Alerts Log action through + +```ts +kbn-action create .server-log "SIEM Alerts Log" {} {} +{ + "id": "7edd7e98-9286-4fdb-a5c5-16de776bc7c7", + "actionTypeId": ".server-log", + "description": "SIEM Alerts Log", + "config": {} +} +``` + +Take note of the `id` GUID above and copy and paste that into a create alert like so + +```ts +kbn-alert create siem.signals 5m '{}' "[{group:default id:'7edd7e98-9286-4fdb-a5c5-16de776bc7c7' params:{message: 'SIEM Alert Fired'}}]" +``` + +You should get back a response like so +```ts +{ + "id": "908a6af1-ac63-4d52-a856-fc635a00db0f", + "alertTypeId": "siem.signals", + "interval": "5m", + "actions": [ + { + "group": "default", + "params": { + "message": "SIEM Alert Fired" + }, + "id": "7edd7e98-9286-4fdb-a5c5-16de776bc7c7" + } + ], + "alertTypeParams": {}, + "enabled": true, + "throttle": null, + "createdBy": "elastic", + "updatedBy": "elastic", + "apiKeyOwner": "elastic", + "scheduledTaskId": "4f401ca0-e402-11e9-94ed-051d758a6c79" +} +``` + +Every 5 minutes you should see this message in your terminal now: + +``` +server log [22:17:33.945] [info][alerting] SIEM Alert Fired +``` + +Add the `.siem-signals-10-01-2019` to your advanced SIEM settings to see any signals +created which should update once every 5 minutes at this point. \ No newline at end of file diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts new file mode 100644 index 00000000000000..6f780744b17b69 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts @@ -0,0 +1,88 @@ +/* + * 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. + */ + +// TODO: See build_events_reindex.ts for all the spots to make things "configurable" +// here but this is intended to replace the build_events_reindex.ts +export const buildEventsQuery = () => { + return { + allowNoIndices: true, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match_phrase: { + 'user.name': 'root', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: 1567317600000, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: 1569909599999, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + }, + }, + { + match_all: {}, + }, + ], + }, + }, + size: 26, + track_total_hits: true, + sort: [ + { + '@timestamp': 'desc', + }, + { + _doc: 'desc', + }, + ], + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts new file mode 100644 index 00000000000000..84cedca0855e0d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts @@ -0,0 +1,141 @@ +/* + * 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. + */ + +// TODO: Re-index is just a temporary solution in order to speed up development +// of any front end pieces. This should be replaced with a combination of the file +// build_events_query.ts and any scrolling/scaling solutions from that particular +// file. + +interface BuildEventsReIndexParams { + index: string[]; + from: number; + to: number; + signalsIndex: string; + maxDocs: number; + kqlFilter: {}; + severity: number; + description: string; + name: string; + timeDetected: number; + ruleRevision: number; + ruleId: string; + ruleType: string; + references: string[]; +} + +export const buildEventsReIndex = ({ + index, + from, + to, + signalsIndex, + maxDocs, + kqlFilter, + severity, + description, + name, + timeDetected, + ruleRevision, + ruleId, + ruleType, + references, +}: BuildEventsReIndexParams) => { + const indexPatterns = index.map(element => `"${element}"`).join(','); + const refs = references.map(element => `"${element}"`).join(','); + const filter = [ + kqlFilter, + { + bool: { + filter: [ + { + bool: { + should: [ + { + range: { + '@timestamp': { + gte: from, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + range: { + '@timestamp': { + lte: to, + }, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ]; + return { + body: { + source: { + index, + sort: [ + { + '@timestamp': 'desc', + }, + { + _doc: 'desc', + }, + ], + query: { + bool: { + filter: [ + ...filter, + { + match_all: {}, + }, + ], + }, + }, + }, + dest: { + index: signalsIndex, + }, + script: { + source: ` + String[] indexPatterns = new String[] {${indexPatterns}}; + String[] references = new String[] {${refs}}; + + def parent = [ + "id": ctx._id, + "type": "event", + "depth": 1 + ]; + + def signal = [ + "rule_revision": "${ruleRevision}", + "rule_id": "${ruleId}", + "rule_type": "${ruleType}", + "parent": parent, + "name": "${name}", + "severity": ${severity}, + "description": "${description}", + "time_detected": "${timeDetected}", + "index_patterns": indexPatterns, + "references": references + ]; + + ctx._source.signal = signal; + `, + lang: 'painless', + }, + max_docs: maxDocs, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts new file mode 100644 index 00000000000000..d0b631f66d54d7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -0,0 +1,82 @@ +/* + * 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 moment from 'moment'; +import { APP_ID } from '../../../../common/constants'; +import { AlertType, AlertExecutorOptions } from '../../../../../alerting'; + +// TODO: Remove this for the build_events_query call eventually +import { buildEventsReIndex } from './build_events_reindex'; + +// TODO: Comment this in and use this instead of the reIndex API +// once scrolling and other things are done with it. +// import { buildEventsQuery } from './build_events_query'; + +export const signalsAlertType: AlertType = { + id: `${APP_ID}.signals`, + name: 'SIEM Signals', + actionGroups: ['default'], + async executor({ services, params, state }: AlertExecutorOptions) { + // TODO: We need to swap out this arbitrary number of siem-signal id for an injected + // data driven instance id through passed in parameters. + const instance = services.alertInstanceFactory('siem-signals'); + + // TODO: Comment this in eventually and use the buildEventsQuery() + // for scrolling and other fun stuff instead of using the buildEventsReIndex() + // const query = buildEventsQuery(); + + // TODO: Turn these options being sent in into a template for the alert type + const reIndex = buildEventsReIndex({ + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + from: moment() + .subtract(5, 'minutes') + .valueOf(), + to: Date.now(), + signalsIndex: '.siem-signals-10-01-2019', + severity: 2, + description: 'User root activity', + name: 'User Rule', + timeDetected: Date.now(), + kqlFilter: { + bool: { + should: [ + { + match_phrase: { + 'user.name': 'root', + }, + }, + ], + minimum_should_match: 1, + }, + }, + maxDocs: 100, + ruleRevision: 1, + ruleId: '1', + ruleType: 'KQL', + references: ['https://www.elastic.co', 'https://example.com'], + }); + + try { + services.log(['info', 'SIEM'], 'Starting SIEM signal job'); + + // TODO: Comment this in eventually and use this for manual insertion of the + // signals instead of the ReIndex() api + // const result = await services.callCluster('search', query); + // eslint-disable-next-line + const result = await services.callCluster('reindex', reIndex); + + // TODO: Error handling here and writing of any errors that come back from ES by + services.log(['info', 'SIEM'], `Result of reindex: ${JSON.stringify(result, null, 2)}`); + } catch (err) { + // TODO: Error handling and writing of errors into a signal that has error + // handling/conditions + services.log(['error', 'SIEM'], `You encountered an error of: ${err.message}`); + } + + // Schedule the default action which is nothing if it's a plain signal. + instance.scheduleActions('default'); + }, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json new file mode 100644 index 00000000000000..4f1e07e2f5d762 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals_mapping.json @@ -0,0 +1,1644 @@ +{ + "mappings": { + "dynamic": false, + "properties": { + "@timestamp": { + "type": "date" + }, + "signal": { + "properties": { + "parent": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "depth": { + "type": "long" + } + } + }, + "time_detected": { + "type": "date" + }, + "rule_revision": { + "type": "long" + }, + "rule_id": { + "type": "keyword" + }, + "rule_type": { + "type": "keyword" + }, + "rule_query": { + "type": "keyword" + }, + "index_patterns": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "severity": { + "type": "long" + }, + "references": { + "type": "text" + }, + "error": { + "properties": { + "code": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "message": { + "type": "text", + "norms": false + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "agent": { + "properties": { + "ephemeral_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "hostname": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "client": { + "properties": { + "address": { + "type": "keyword", + "ignore_above": 1024 + }, + "bytes": { + "type": "long" + }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "type": "keyword", + "ignore_above": 1024 + }, + "full_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "availability_zone": { + "type": "keyword", + "ignore_above": 1024 + }, + "instance": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "machine": { + "properties": { + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "project": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "provider": { + "type": "keyword", + "ignore_above": 1024 + }, + "region": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "container": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "image": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "tag": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "runtime": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "destination": { + "properties": { + "address": { + "type": "keyword", + "ignore_above": 1024 + }, + "bytes": { + "type": "long" + }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "packets": { + "type": "long" + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "type": "keyword", + "ignore_above": 1024 + }, + "full_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "error": { + "properties": { + "code": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "message": { + "type": "text", + "norms": false + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "event": { + "properties": { + "action": { + "type": "keyword", + "ignore_above": 1024 + }, + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword", + "ignore_above": 1024 + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "kind": { + "type": "keyword", + "ignore_above": 1024 + }, + "module": { + "type": "keyword", + "ignore_above": 1024 + }, + "origin": { + "type": "keyword", + "ignore_above": 1024 + }, + "original": { + "type": "keyword", + "ignore_above": 1024 + }, + "outcome": { + "type": "keyword", + "ignore_above": 1024 + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "file": { + "properties": { + "ctime": { + "type": "date" + }, + "device": { + "type": "keyword", + "ignore_above": 1024 + }, + "extension": { + "type": "keyword", + "ignore_above": 1024 + }, + "gid": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "type": "keyword", + "ignore_above": 1024 + }, + "inode": { + "type": "keyword", + "ignore_above": 1024 + }, + "mode": { + "type": "keyword", + "ignore_above": 1024 + }, + "mtime": { + "type": "date" + }, + "origin": { + "type": "keyword", + "fields": { + "raw": { + "type": "keyword", + "ignore_above": 1024 + } + }, + "ignore_above": 1024 + }, + "owner": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "selinux": { + "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "level": { + "type": "keyword", + "ignore_above": 1024 + }, + "role": { + "type": "keyword", + "ignore_above": 1024 + }, + "user": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "setgid": { + "type": "boolean" + }, + "setuid": { + "type": "boolean" + }, + "size": { + "type": "long" + }, + "target_path": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "uid": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "properties": { + "blake2b_256": { + "type": "keyword", + "ignore_above": 1024 + }, + "blake2b_384": { + "type": "keyword", + "ignore_above": 1024 + }, + "blake2b_512": { + "type": "keyword", + "ignore_above": 1024 + }, + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha224": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha384": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha3_224": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha3_256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha3_384": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha3_512": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512_224": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512_256": { + "type": "keyword", + "ignore_above": 1024 + }, + "xxh64": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "host": { + "properties": { + "architecture": { + "type": "keyword", + "ignore_above": 1024 + }, + "containerized": { + "type": "boolean" + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hostname": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "os": { + "properties": { + "build": { + "type": "keyword", + "ignore_above": 1024 + }, + "codename": { + "type": "keyword", + "ignore_above": 1024 + }, + "family": { + "type": "keyword", + "ignore_above": 1024 + }, + "full": { + "type": "keyword", + "ignore_above": 1024 + }, + "kernel": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "platform": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "user": { + "properties": { + "email": { + "type": "keyword", + "ignore_above": 1024 + }, + "full_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "type": "keyword", + "ignore_above": 1024 + }, + "referrer": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "type": "keyword", + "ignore_above": 1024 + }, + "original": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "message": { + "type": "text", + "norms": false + }, + "network": { + "properties": { + "application": { + "type": "keyword", + "ignore_above": 1024 + }, + "bytes": { + "type": "long" + }, + "community_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "direction": { + "type": "keyword", + "ignore_above": 1024 + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "packets": { + "type": "long" + }, + "protocol": { + "type": "keyword", + "ignore_above": 1024 + }, + "transport": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hostname": { + "type": "keyword", + "ignore_above": 1024 + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "os": { + "properties": { + "family": { + "type": "keyword", + "ignore_above": 1024 + }, + "full": { + "type": "keyword", + "ignore_above": 1024 + }, + "kernel": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "platform": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "serial_number": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "vendor": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "organization": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "os": { + "properties": { + "family": { + "type": "keyword", + "ignore_above": 1024 + }, + "full": { + "type": "keyword", + "ignore_above": 1024 + }, + "kernel": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "platform": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "process": { + "properties": { + "args": { + "type": "keyword", + "ignore_above": 1024 + }, + "created": { + "type": "keyword", + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "sha1": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + }, + "title": { + "type": "keyword", + "ignore_above": 1024 + }, + "working_directory": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "related": { + "properties": { + "ip": { + "type": "ip" + } + } + }, + "server": { + "properties": { + "address": { + "type": "keyword", + "ignore_above": 1024 + }, + "bytes": { + "type": "long" + }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "type": "keyword", + "ignore_above": 1024 + }, + "full_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "state": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "source": { + "properties": { + "address": { + "type": "keyword", + "ignore_above": 1024 + }, + "bytes": { + "type": "long" + }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "geo": { + "properties": { + "city_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent": { + "type": "keyword", + "ignore_above": 1024 + }, + "continent_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "country_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_iso_code": { + "type": "keyword", + "ignore_above": 1024 + }, + "region_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "type": "keyword", + "ignore_above": 1024 + }, + "packets": { + "type": "long" + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "port": { + "type": "long" + }, + "user": { + "properties": { + "email": { + "type": "keyword", + "ignore_above": 1024 + }, + "full_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "tags": { + "type": "keyword", + "ignore_above": 1024 + }, + "url": { + "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "fragment": { + "type": "keyword", + "ignore_above": 1024 + }, + "full": { + "type": "keyword", + "ignore_above": 1024 + }, + "original": { + "type": "keyword", + "ignore_above": 1024 + }, + "password": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "port": { + "type": "long" + }, + "query": { + "type": "keyword", + "ignore_above": 1024 + }, + "scheme": { + "type": "keyword", + "ignore_above": 1024 + }, + "username": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "user": { + "properties": { + "audit": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "effective": { + "properties": { + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "email": { + "type": "keyword", + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "filesystem": { + "properties": { + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "full_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "name_map": { + "type": "object" + }, + "saved": { + "properties": { + "group": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "selinux": { + "properties": { + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "level": { + "type": "keyword", + "ignore_above": 1024 + }, + "role": { + "type": "keyword", + "ignore_above": 1024 + }, + "user": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "terminal": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "original": { + "type": "keyword", + "ignore_above": 1024 + }, + "os": { + "properties": { + "family": { + "type": "keyword", + "ignore_above": 1024 + }, + "full": { + "type": "keyword", + "ignore_above": 1024 + }, + "kernel": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "platform": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + } +} From 1381e9a2efd54c34baee48dfaf7e2413e254ed16 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 1 Oct 2019 17:46:20 -0500 Subject: [PATCH 26/53] Make check_core_api_changes script faster (#47068) --- src/dev/run_check_core_api_changes.ts | 78 +++++++++++++++------------ 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/src/dev/run_check_core_api_changes.ts b/src/dev/run_check_core_api_changes.ts index 4d0be7f3884665..d2c75c86ce7442 100644 --- a/src/dev/run_check_core_api_changes.ts +++ b/src/dev/run_check_core_api_changes.ts @@ -139,14 +139,51 @@ const runApiExtractor = ( return Extractor.invoke(config, options); }; -async function run(folder: string): Promise { +interface Options { + accept: boolean; + docs: boolean; + help: boolean; +} + +async function run( + folder: string, + { log, opts }: { log: ToolingLog; opts: Options } +): Promise { + log.info(`Core ${folder} API: checking for changes in API signature...`); + + const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); + + // If we're not accepting changes and there's a failure, exit. + if (!opts.accept && !succeeded) { + return false; + } + + // Attempt to generate docs even if api-extractor didn't succeed + if ((opts.accept && apiReportChanged) || opts.docs) { + try { + await renameExtractedApiPackageName(folder); + await runApiDocumenter(folder); + } catch (e) { + log.error(e); + return false; + } + log.info(`Core ${folder} API: updated documentation ✔`); + } + + // If the api signature changed or any errors or warnings occured, exit with an error + // NOTE: Because of https://github.com/Microsoft/web-build-tools/issues/1258 + // api-extractor will not return `succeeded: false` when the API changes. + return !apiReportChanged && succeeded; +} + +(async () => { const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); const extraFlags: string[] = []; - const opts = getopts(process.argv.slice(2), { + const opts = (getopts(process.argv.slice(2), { boolean: ['accept', 'docs', 'help'], default: { project: undefined, @@ -155,7 +192,7 @@ async function run(folder: string): Promise { extraFlags.push(name); return false; }, - }); + }) as any) as Options; if (extraFlags.length > 0) { for (const flag of extraFlags) { @@ -193,45 +230,18 @@ async function run(folder: string): Promise { return !(extraFlags.length > 0); } - log.info(`Core ${folder} API: checking for changes in API signature...`); - try { + log.info(`Core: Building types...`); await runBuildTypes(); } catch (e) { log.error(e); return false; } - const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); - - // If we're not accepting changes and there's a failure, exit. - if (!opts.accept && !succeeded) { - return false; - } - - // Attempt to generate docs even if api-extractor didn't succeed - if ((opts.accept && apiReportChanged) || opts.docs) { - try { - await renameExtractedApiPackageName(folder); - await runApiDocumenter(folder); - } catch (e) { - log.error(e); - return false; - } - log.info(`Core ${folder} API: updated documentation ✔`); - } - - // If the api signature changed or any errors or warnings occured, exit with an error - // NOTE: Because of https://github.com/Microsoft/web-build-tools/issues/1258 - // api-extractor will not return `succeeded: false` when the API changes. - return !apiReportChanged && succeeded; -} - -(async () => { - const publicSucceeded = await run('public'); - const serverSucceeded = await run('server'); + const folders = ['public', 'server']; + const results = await Promise.all(folders.map(folder => run(folder, { log, opts }))); - if (!publicSucceeded || !serverSucceeded) { + if (results.find(r => r === false) !== undefined) { process.exitCode = 1; } })(); From bb9b4c792de5a8fd176097cc04a49b30f347d9b4 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Wed, 2 Oct 2019 10:04:00 +0200 Subject: [PATCH 27/53] [ML] Update data-test-subj attributes for action buttons and add ml namespace (#47032) This PR updates a couple ML data-test-subj attributes. --- .../components/job_actions/management.js | 18 ++++-- .../components/job_details/job_details.js | 30 ++++----- .../job_details/job_details_pane.js | 2 +- .../components/job_group/job_group.js | 2 +- .../components/jobs_list/jobs_list.js | 18 +++--- .../detector_title/detector_title.tsx | 2 +- .../influencers/influencers_select.tsx | 2 +- .../multi_metric_view/chart_grid.tsx | 2 +- .../components/population_view/chart_grid.tsx | 2 +- .../components/split_cards/split_cards.tsx | 16 +++-- .../components/split_field/by_field.tsx | 2 +- .../components/split_field/split_field.tsx | 4 +- .../time_range_step/time_range_picker.tsx | 2 +- .../services/machine_learning/job_table.ts | 63 ++++++------------- .../machine_learning/job_wizard_common.ts | 36 +++-------- .../job_wizard_multi_metric.ts | 14 ++--- .../machine_learning/job_wizard_population.ts | 24 ++++--- 17 files changed, 106 insertions(+), 133 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js index ba13b06821a1f2..7d97c8589e7d10 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/management.js @@ -40,7 +40,8 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt onClick: (item) => { showStartDatafeedModal([item]); closeMenu(); - } + }, + 'data-test-subj': 'mlActionButtonStartDatafeed' }, { name: i18n.translate('xpack.ml.jobsList.managementActions.stopDatafeedLabel', { defaultMessage: 'Stop datafeed' @@ -54,7 +55,8 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt onClick: (item) => { stopDatafeeds([item], refreshJobs); closeMenu(true); - } + }, + 'data-test-subj': 'mlActionButtonStopDatafeed' }, { name: i18n.translate('xpack.ml.jobsList.managementActions.closeJobLabel', { defaultMessage: 'Close job' @@ -68,7 +70,8 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt onClick: (item) => { closeJobs([item], refreshJobs); closeMenu(true); - } + }, + 'data-test-subj': 'mlActionButtonCloseJob' }, { name: i18n.translate('xpack.ml.jobsList.managementActions.cloneJobLabel', { defaultMessage: 'Clone job' @@ -92,7 +95,8 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt onClick: (item) => { cloneJob(item.id); closeMenu(true); - } + }, + 'data-test-subj': 'mlActionButtonCloneJob' }, { name: i18n.translate('xpack.ml.jobsList.managementActions.editJobLabel', { defaultMessage: 'Edit job' @@ -105,7 +109,8 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt onClick: (item) => { showEditJobFlyout(item); closeMenu(); - } + }, + 'data-test-subj': 'mlActionButtonEditJob' }, { name: i18n.translate('xpack.ml.jobsList.managementActions.deleteJobLabel', { defaultMessage: 'Delete job' @@ -119,7 +124,8 @@ export function actionsMenuContent(showEditJobFlyout, showDeleteJobModal, showSt onClick: (item) => { showDeleteJobModal([item]); closeMenu(); - } + }, + 'data-test-subj': 'mlActionButtonDeleteJob' } ]; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js index 97e7c1b72e5651..192310937c0e93 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js @@ -51,7 +51,7 @@ class JobDetailsUI extends Component { const { job } = this.state; if (job === undefined) { return ( -
+
); @@ -77,35 +77,35 @@ class JobDetailsUI extends Component { const tabs = [{ id: 'job-settings', - 'data-test-subj': 'tab-job-settings', + 'data-test-subj': 'mlJobListTab-job-settings', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jobSettingsLabel', defaultMessage: 'Job settings' }), - content: , + content: , time: job.open_time }, { id: 'job-config', - 'data-test-subj': 'tab-job-config', + 'data-test-subj': 'mlJobListTab-job-config', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jobConfigLabel', defaultMessage: 'Job config' }), content: , }, { id: 'counts', - 'data-test-subj': 'tab-counts', + 'data-test-subj': 'mlJobListTab-counts', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.countsLabel', defaultMessage: 'Counts' }), - content: , + content: , }, { id: 'json', - 'data-test-subj': 'tab-json', + 'data-test-subj': 'mlJobListTab-json', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jsonLabel', defaultMessage: 'JSON' @@ -113,7 +113,7 @@ class JobDetailsUI extends Component { content: , }, { id: 'job-messages', - 'data-test-subj': 'tab-job-messages', + 'data-test-subj': 'mlJobListTab-job-messages', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', defaultMessage: 'Job messages' @@ -126,17 +126,17 @@ class JobDetailsUI extends Component { // Datafeed should be at index 2 in tabs array for full details tabs.splice(2, 0, { id: 'datafeed', - 'data-test-subj': 'tab-datafeed', + 'data-test-subj': 'mlJobListTab-datafeed', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', defaultMessage: 'Datafeed' }), - content: , + content: , }); tabs.push({ id: 'datafeed-preview', - 'data-test-subj': 'tab-datafeed-preview', + 'data-test-subj': 'mlJobListTab-datafeed-preview', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', defaultMessage: 'Datafeed preview' @@ -144,7 +144,7 @@ class JobDetailsUI extends Component { content: , }, { id: 'forecasts', - 'data-test-subj': 'tab-forecasts', + 'data-test-subj': 'mlJobListTab-forecasts', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', defaultMessage: 'Forecasts' @@ -156,7 +156,7 @@ class JobDetailsUI extends Component { if (mlAnnotationsEnabled && showFullDetails) { tabs.push({ id: 'annotations', - 'data-test-subj': 'tab-annotations', + 'data-test-subj': 'mlJobListTab-annotations', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', defaultMessage: 'Annotations' @@ -171,7 +171,7 @@ class JobDetailsUI extends Component { } return ( -
+

{section.title}

-
+
{ section.items.map((item, i) => ()) } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js index 9ba7aba9dc8765..b0e10a975b8633 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js @@ -16,7 +16,7 @@ export function JobGroup({ name }) { return (
), width: '3%' }, { field: 'id', - 'data-test-subj': 'id', + 'data-test-subj': 'mlJobListColumnId', name: intl.formatMessage({ id: 'xpack.ml.jobsList.idLabel', defaultMessage: 'ID' @@ -195,7 +195,7 @@ class JobsListUI extends Component { }), sortable: true, field: 'description', - 'data-test-subj': 'description', + 'data-test-subj': 'mlJobListColumnDescription', render: (description, item) => ( ), @@ -203,7 +203,7 @@ class JobsListUI extends Component { width: '20%' }, { field: 'processed_record_count', - 'data-test-subj': 'recordCount', + 'data-test-subj': 'mlJobListColumnRecordCount', name: intl.formatMessage({ id: 'xpack.ml.jobsList.processedRecordsLabel', defaultMessage: 'Processed records' @@ -215,7 +215,7 @@ class JobsListUI extends Component { width: '10%' }, { field: 'memory_status', - 'data-test-subj': 'memoryStatus', + 'data-test-subj': 'mlJobListColumnMemoryStatus', name: intl.formatMessage({ id: 'xpack.ml.jobsList.memoryStatusLabel', defaultMessage: 'Memory status' @@ -225,7 +225,7 @@ class JobsListUI extends Component { width: '5%' }, { field: 'jobState', - 'data-test-subj': 'jobState', + 'data-test-subj': 'mlJobListColumnJobState', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobStateLabel', defaultMessage: 'Job state' @@ -235,7 +235,7 @@ class JobsListUI extends Component { width: '8%' }, { field: 'datafeedState', - 'data-test-subj': 'datafeedState', + 'data-test-subj': 'mlJobListColumnDatafeedState', name: intl.formatMessage({ id: 'xpack.ml.jobsList.datafeedStateLabel', defaultMessage: 'Datafeed state' @@ -278,7 +278,7 @@ class JobsListUI extends Component { }), truncateText: false, field: 'latestTimestampSortValue', - 'data-test-subj': 'latestTimestamp', + 'data-test-subj': 'mlJobListColumnLatestTimestamp', sortable: true, render: (time, item) => ( @@ -355,7 +355,7 @@ class JobsListUI extends Component { sorting={sorting} hasActions={true} rowProps={item => ({ - 'data-test-subj': `row row-${item.id}` + 'data-test-subj': `mlJobListRow row-${item.id}` })} /> ); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx index 040d980cefab71..feac3b30dfa3bd 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/detector_title/detector_title.tsx @@ -33,7 +33,7 @@ export const DetectorTitle: FC = ({ return ( - + {getTitle(agg, field, splitField)} diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 63f1936dd5f3e8..f03565fd3a0330 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -35,7 +35,7 @@ export const InfluencersSelect: FC = ({ fields, changeHandler, selectedIn selectedOptions={selection} onChange={onChange} isClearable={false} - data-test-subj="influencerSelect" + data-test-subj="mlInfluencerSelect" /> ); }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx index cb2ca459da4fe7..e4dd46b159a6c4 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -54,7 +54,7 @@ export const ChartGrid: FC = ({ > {aggFieldPairList.map((af, i) => ( - + = ({ return ( {aggFieldPairList.map((af, i) => ( - + diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index a3c13fce53a9b6..1b3eac2626e471 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -70,10 +70,14 @@ export const SplitCards: FC = memo( }; return (
storePanels(ref, marginBottom)} style={style}> - +
{fieldName}
@@ -85,7 +89,7 @@ export const SplitCards: FC = memo( return ( - + {(fieldValues.length === 0 || numberOfDetectors === 0) && {children}} {fieldValues.length > 0 && numberOfDetectors > 0 && splitField !== null && ( @@ -93,7 +97,7 @@ export const SplitCards: FC = memo(
= memo(
{fieldValues[0]}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx index 0acabe60b00497..63576bb23f2422 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/by_field.tsx @@ -58,7 +58,7 @@ export const ByFieldSelector: FC = ({ detectorIndex }) => { changeHandler={setByField} selectedField={byField} isClearable={true} - testSubject="byFieldSelect" + testSubject="mlByFieldSelect" placeholder={i18n.translate( 'xpack.ml.newJob.wizard.pickFieldsStep.populationField.placeholder', { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx index 229072c314cc5d..37ffa25e0eabfe 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/split_field/split_field.tsx @@ -50,9 +50,9 @@ export const SplitFieldSelector: FC = () => { isClearable={canClearSelection} testSubject={ isMultiMetricJobCreator(jc) - ? 'multiMetricSplitFieldSelect' + ? 'mlMultiMetricSplitFieldSelect' : isPopulationJobCreator(jc) - ? 'populationSplitFieldSelect' + ? 'mlPopulationSplitFieldSelect' : undefined } /> diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx index b17b47308643c7..26140b9557e908 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/time_range_step/time_range_picker.tsx @@ -56,7 +56,7 @@ export const TimeRangePicker: FC = ({ setTimeRange, timeRange }) => { return ( -
+
{ // click counts tab - await testSubjects.click(this.detailsSelector(jobId, 'tab-counts')); + await testSubjects.click(this.detailsSelector(jobId, 'mlJobListTab-counts')); const countsTable = await testSubjects.find( - this.detailsSelector(jobId, 'details-counts > counts') + this.detailsSelector(jobId, 'mlJobDetails-counts > mlJobRowDetailsSection-counts') ); const modelSizeStatsTable = await testSubjects.find( - this.detailsSelector(jobId, 'details-counts > modelSizeStats') + this.detailsSelector(jobId, 'mlJobDetails-counts > mlJobRowDetailsSection-modelSizeStats') ); // parse a table by reading each row @@ -142,7 +143,7 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte public async ensureDetailsOpen(jobId: string) { await retry.try(async () => { if (!(await testSubjects.exists(this.detailsSelector(jobId)))) { - await testSubjects.click(this.rowSelector(jobId, 'detailsToggle')); + await testSubjects.click(this.rowSelector(jobId, 'mlJobListRowDetailsToggle')); } await testSubjects.existOrFail(this.detailsSelector(jobId)); @@ -152,7 +153,7 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte public async ensureDetailsClosed(jobId: string) { await retry.try(async () => { if (await testSubjects.exists(this.detailsSelector(jobId))) { - await testSubjects.click(this.rowSelector(jobId, 'detailsToggle')); + await testSubjects.click(this.rowSelector(jobId, 'mlJobListRowDetailsToggle')); await testSubjects.missingOrFail(this.detailsSelector(jobId)); } }); @@ -214,40 +215,16 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async clickActionsMenu(jobId: string) { - const jobRow = await testSubjects.find(this.rowSelector(jobId)); - const actionsCell = await jobRow.findByCssSelector(`[id=${jobId}-actions]`); - const actionsMenuButton = await actionsCell.findByTagName('button'); - - log.debug(`Clicking actions menu button for job id ${jobId}`); - await actionsMenuButton.click(); + await testSubjects.click(this.rowSelector(jobId, 'euiCollapsedItemActionsButton')); if (!(await find.existsByDisplayedByCssSelector('[class~=euiContextMenuPanel]'))) { throw new Error(`expected euiContextMenuPanel to exist`); } } - public async clickActionsMenuEntry(jobId: string, entryText: string) { - await this.clickActionsMenu(jobId); - const actionsMenuPanel = await find.byCssSelector('[class~=euiContextMenuPanel]'); - const actionButtons = await actionsMenuPanel.findAllByTagName('button'); - - const filteredButtons = []; - for (const button of actionButtons) { - if ((await button.getVisibleText()) === entryText) { - filteredButtons.push(button); - } - } - - if (!(filteredButtons.length === 1)) { - throw new Error( - `expected action button ${entryText} to exist exactly once, but found ${filteredButtons.length} matching buttons` - ); - } - log.debug(`Clicking action button ${entryText} for job id ${jobId}`); - await filteredButtons[0].click(); - } - public async clickCloneJobAction(jobId: string) { - await this.clickActionsMenuEntry(jobId, 'Clone job'); + await this.clickActionsMenu(jobId); + await testSubjects.click('mlActionButtonCloneJob'); + await testSubjects.existOrFail('~mlPageJobWizard'); } })(); } diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index f0ecb1289c9c8d..7c85115a42aa0f 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -230,11 +230,11 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid }, async assertInfluencerInputExists() { - await testSubjects.existOrFail('influencerSelect > comboBoxInput'); + await testSubjects.existOrFail('mlInfluencerSelect > comboBoxInput'); }, async getSelectedInfluencers(): Promise { - return await comboBox.getComboBoxSelectedOptions('influencerSelect > comboBoxInput'); + return await comboBox.getComboBoxSelectedOptions('mlInfluencerSelect > comboBoxInput'); }, async assertInfluencerSelection(influencers: string[]) { @@ -242,7 +242,7 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid }, async addInfluencer(influencer: string) { - await comboBox.setCustom('influencerSelect > comboBoxInput', influencer); + await comboBox.setCustom('mlInfluencerSelect > comboBoxInput', influencer); expect(await this.getSelectedInfluencers()).to.contain(influencer); }, @@ -256,31 +256,13 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid detectorPosition: number, chartType: string ) { - await testSubjects.existOrFail(`detector ${detectorPosition}`); - await testSubjects.existOrFail(`detector ${detectorPosition} > detectorTitle`); + await testSubjects.existOrFail(`mlDetector ${detectorPosition}`); + await testSubjects.existOrFail(`mlDetector ${detectorPosition} > mlDetectorTitle`); expect( - await testSubjects.getVisibleText(`detector ${detectorPosition} > detectorTitle`) + await testSubjects.getVisibleText(`mlDetector ${detectorPosition} > mlDetectorTitle`) ).to.eql(aggAndFieldIdentifier); - await this.assertAnomalyChartExists(chartType, `detector ${detectorPosition}`); - }, - - async assertDetectorSplitExists(splitField: string) { - await testSubjects.existOrFail(`dataSplit > dataSplitTitle ${splitField}`); - await testSubjects.existOrFail(`dataSplit > splitCard front`); - await testSubjects.existOrFail(`dataSplit > splitCard back`); - }, - - async assertDetectorSplitFrontCardTitle(frontCardTitle: string) { - expect( - await testSubjects.getVisibleText(`dataSplit > splitCard front > splitCardTitle`) - ).to.eql(frontCardTitle); - }, - - async assertDetectorSplitNumberOfBackCards(numberOfBackCards: number) { - expect(await testSubjects.findAll(`dataSplit > splitCard back`)).to.have.length( - numberOfBackCards - ); + await this.assertAnomalyChartExists(chartType, `mlDetector ${detectorPosition}`); }, async assertCreateJobButtonExists() { @@ -288,11 +270,11 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid }, async assertDateRangeSelectionExists() { - await testSubjects.existOrFail('jobWizardDateRange'); + await testSubjects.existOrFail('mlJobWizardDateRange'); }, async getSelectedDateRange() { - const dateRange = await testSubjects.find('jobWizardDateRange'); + const dateRange = await testSubjects.find('mlJobWizardDateRange'); const [startPicker, endPicker] = await dateRange.findAllByClassName('euiFieldText'); return { startDate: await startPicker.getAttribute('value'), diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts b/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts index d57e92aaa34688..d9df6a9d682a76 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts @@ -13,35 +13,35 @@ export function MachineLearningJobWizardMultiMetricProvider({ getService }: FtrP return { async assertSplitFieldInputExists() { - await testSubjects.existOrFail('multiMetricSplitFieldSelect > comboBoxInput'); + await testSubjects.existOrFail('mlMultiMetricSplitFieldSelect > comboBoxInput'); }, async assertSplitFieldSelection(identifier: string) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'multiMetricSplitFieldSelect > comboBoxInput' + 'mlMultiMetricSplitFieldSelect > comboBoxInput' ); expect(comboBoxSelectedOptions.length).to.eql(1); expect(comboBoxSelectedOptions[0]).to.eql(identifier); }, async selectSplitField(identifier: string) { - await comboBox.set('multiMetricSplitFieldSelect > comboBoxInput', identifier); + await comboBox.set('mlMultiMetricSplitFieldSelect > comboBoxInput', identifier); await this.assertSplitFieldSelection(identifier); }, async assertDetectorSplitExists(splitField: string) { - await testSubjects.existOrFail(`dataSplit > dataSplitTitle ${splitField}`); - await testSubjects.existOrFail(`dataSplit > splitCard front`); + await testSubjects.existOrFail(`mlDataSplit > mlDataSplitTitle ${splitField}`); + await testSubjects.existOrFail(`mlDataSplit > mlSplitCard front`); }, async assertDetectorSplitFrontCardTitle(frontCardTitle: string) { expect( - await testSubjects.getVisibleText(`dataSplit > splitCard front > splitCardTitle`) + await testSubjects.getVisibleText(`mlDataSplit > mlSplitCard front > mlSplitCardTitle`) ).to.eql(frontCardTitle); }, async assertDetectorSplitNumberOfBackCards(numberOfBackCards: number) { - expect(await testSubjects.findAll(`dataSplit > splitCard back`)).to.have.length( + expect(await testSubjects.findAll(`mlDataSplit > mlSplitCard back`)).to.have.length( numberOfBackCards ); }, diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_population.ts b/x-pack/test/functional/services/machine_learning/job_wizard_population.ts index 2b0a8c6521bd17..892bdaf394936a 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_population.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_population.ts @@ -13,31 +13,31 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr return { async assertPopulationFieldInputExists() { - await testSubjects.existOrFail('populationSplitFieldSelect > comboBoxInput'); + await testSubjects.existOrFail('mlPopulationSplitFieldSelect > comboBoxInput'); }, async assertPopulationFieldSelection(identifier: string) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'populationSplitFieldSelect > comboBoxInput' + 'mlPopulationSplitFieldSelect > comboBoxInput' ); expect(comboBoxSelectedOptions.length).to.eql(1); expect(comboBoxSelectedOptions[0]).to.eql(identifier); }, async selectPopulationField(identifier: string) { - await comboBox.set('populationSplitFieldSelect > comboBoxInput', identifier); + await comboBox.set('mlPopulationSplitFieldSelect > comboBoxInput', identifier); await this.assertPopulationFieldSelection(identifier); }, async assertDetectorSplitFieldInputExists(detectorPosition: number) { await testSubjects.existOrFail( - `detector ${detectorPosition} > byFieldSelect > comboBoxInput` + `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput` ); }, async assertDetectorSplitFieldSelection(detectorPosition: number, identifier: string) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - `detector ${detectorPosition} > byFieldSelect > comboBoxInput` + `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput` ); expect(comboBoxSelectedOptions.length).to.eql(1); expect(comboBoxSelectedOptions[0]).to.eql(identifier); @@ -45,21 +45,23 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr async selectDetectorSplitField(detectorPosition: number, identifier: string) { await comboBox.set( - `detector ${detectorPosition} > byFieldSelect > comboBoxInput`, + `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput`, identifier ); await this.assertDetectorSplitFieldSelection(detectorPosition, identifier); }, async assertDetectorSplitExists(detectorPosition: number) { - await testSubjects.existOrFail(`detector ${detectorPosition} > dataSplit`); - await testSubjects.existOrFail(`detector ${detectorPosition} > dataSplit > splitCard front`); + await testSubjects.existOrFail(`mlDetector ${detectorPosition} > mlDataSplit`); + await testSubjects.existOrFail( + `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard front` + ); }, async assertDetectorSplitFrontCardTitle(detectorPosition: number, frontCardTitle: string) { expect( await testSubjects.getVisibleText( - `detector ${detectorPosition} > dataSplit > splitCard front > splitCardTitle` + `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard front > mlSplitCardTitle` ) ).to.eql(frontCardTitle); }, @@ -69,7 +71,9 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr numberOfBackCards: number ) { expect( - await testSubjects.findAll(`detector ${detectorPosition} > dataSplit > splitCard back`) + await testSubjects.findAll( + `mlDetector ${detectorPosition} > mlDataSplit > mlSplitCard back` + ) ).to.have.length(numberOfBackCards); }, }; From 03ccb43e312d980c8f7ced725e71a163419f2ba0 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 2 Oct 2019 10:11:05 +0200 Subject: [PATCH 28/53] [Discover] De-angularize side bar search field (#46679) * Add DiscoverFieldSearch react component * Adapt field_chooser.js * Add jest test * Adapt scss --- .../kibana/public/discover/_discover.scss | 21 ++- .../kibana/public/discover/_hacks.scss | 14 -- .../field_chooser/_field_chooser.scss | 9 + .../discover_field_search.test.tsx | 58 +++++++ .../field_chooser/discover_field_search.tsx | 91 ++++++++++ .../discover_field_search_directive.ts | 33 ++++ .../field_chooser/field_chooser.html | 164 ++++++++---------- .../components/field_chooser/field_chooser.js | 65 ++++--- .../kibana/public/discover/index.html | 27 +-- .../styles/_legacy/components/_sidebar.scss | 4 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 12 files changed, 328 insertions(+), 160 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx create mode 100644 src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/_discover.scss index abf24241071c21..12cac1c89275b3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_discover.scss @@ -19,9 +19,15 @@ discover-app { margin-top: 5px; } -// SASSTODO: replace the padding-top value with a variable .dscFieldList--popular { - padding-top: 10px; + padding-top: $euiSizeS; +} + +.dscFieldList--selected, +.dscFieldList--unpopular, +.dscFieldList--popular { + padding-left: $euiSizeS; + padding-right: $euiSizeS; } // SASSTODO: replace the z-index value with a variable @@ -151,9 +157,16 @@ discover-app { } } -// SASSTODO: replace the padding value with a variable +.dscFieldSearch { + padding: $euiSizeS; +} + +.dscFieldFilter { + margin-top: $euiSizeS; +} + .dscFieldDetails { - padding: 10px; + padding: $euiSizeS; background-color: $euiColorLightestShade; color: $euiTextColor; } diff --git a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss index cdc8e04dff5789..baf27bb9f82da1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss @@ -3,18 +3,4 @@ overflow: hidden; } -// SASSTODO: these are Angular Bootstrap classes. Will be replaced by EUI -.dscFieldDetails { - .progress { - background-color: shade($euiColorLightestShade, 5%); - margin-bottom: 0; - border-radius: 0; - } - .progress-bar { - padding-left: 10px; - text-align: right; - line-height: 20px; - max-width: 100%; - } -} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss index 1946cccd319a4d..22f53512be46b1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss @@ -1,3 +1,8 @@ +.dscFieldChooser { + padding-left: $euiSizeS !important; + padding-right: $euiSizeS !important; +} + .dscFieldChooser__toggle { color: $euiColorMediumShade; margin-left: $euiSizeS !important; @@ -15,3 +20,7 @@ .dscProgressBarTooltip__anchor { display: block; } + +.dscToggleFieldFilterButton { + min-height: $euiSizeXL; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx new file mode 100644 index 00000000000000..cf853d798a8abb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { DiscoverFieldSearch } from './discover_field_search'; + +describe('DiscoverFieldSearch', () => { + function mountComponent() { + const props = { + onChange: jest.fn(), + onShowFilter: jest.fn(), + showFilter: false, + value: 'test', + }; + const comp = mountWithIntl(); + const input = findTestSubject(comp, 'fieldFilterSearchInput'); + const btn = findTestSubject(comp, 'toggleFieldFilterButton'); + return { comp, input, btn, props }; + } + + test('enter value', () => { + const { input, props } = mountComponent(); + input.simulate('change', { target: { value: 'new filter' } }); + expect(props.onChange).toBeCalledTimes(1); + }); + + // this should work, but doesn't, have to do some research + test('click toggle filter button', () => { + const { btn, props } = mountComponent(); + btn.simulate('click'); + expect(props.onShowFilter).toBeCalledTimes(1); + }); + + test('change showFilter value should change button label', () => { + const { btn, comp } = mountComponent(); + const prevFilterBtnHTML = btn.html(); + comp.setProps({ showFilter: true }); + expect(btn.html()).not.toBe(prevFilterBtnHTML); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx new file mode 100644 index 00000000000000..666ccf0acfc7a3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +export interface Props { + /** + * triggered on input of user into search field + */ + onChange: (field: string, value: string) => void; + /** + * triggered when the "additional filter btn" is clicked + */ + onShowFilter: () => void; + /** + * determines whether additional filter fields are displayed + */ + showFilter: boolean; + /** + * the input value of the user + */ + value?: string; +} + +/** + * Component is Discover's side bar to search of available fields + * Additionally there's a button displayed that allows the user to show/hide more filter fields + */ +export function DiscoverFieldSearch({ showFilter, onChange, onShowFilter, value }: Props) { + if (typeof value !== 'string') { + // at initial rendering value is undefined (angular related), this catches the warning + // should be removed once all is react + return null; + } + const filterBtnAriaLabel = showFilter + ? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { + defaultMessage: 'Hide field filter settings', + }) + : i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { + defaultMessage: 'Show field filter settings', + }); + const searchPlaceholder = i18n.translate('kbn.discover.fieldChooser.searchPlaceHolder', { + defaultMessage: 'Search fields', + }); + + return ( + + + onChange('name', event.currentTarget.value)} + placeholder={searchPlaceholder} + value={value} + /> + + + + onShowFilter()} + size="m" + /> + + + + ); +} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts new file mode 100644 index 00000000000000..baf8f3040d6b02 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { wrapInI18nContext } from 'ui/i18n'; +import { DiscoverFieldSearch } from './discover_field_search'; + +const app = uiModules.get('apps/discover'); + +app.directive('discoverFieldSearch', function(reactDirective: any) { + return reactDirective(wrapInI18nContext(DiscoverFieldSearch), [ + ['onChange', { watchDepth: 'reference' }], + ['onShowFilter', { watchDepth: 'reference' }], + ['showFilter', { watchDepth: 'value' }], + ['value', { watchDepth: 'value' }], + ]); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html index 96aa1582b5243e..2043dc44c147e7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html @@ -5,6 +5,75 @@ index-pattern-list="indexPatternList" > + -
- -
-
- -
{ - $scope.toggleFieldFilterButtonAriaLabel = $scope.$parent.showFilter - ? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { - defaultMessage: 'Hide field settings', - }) - : i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { - defaultMessage: 'Show field settings', - }); - }); + + $scope.showFilter = false; + $scope.toggleShowFilter = () => $scope.showFilter = !$scope.showFilter; $scope.selectedIndexPattern = $scope.indexPatternList.find( (pattern) => pattern.id === $scope.indexPattern.id @@ -81,40 +76,28 @@ app.directive('discFieldChooser', function ($location, config, $route) { ], defaults: { missing: true, - type: 'any' + type: 'any', + name: '' }, boolOpts: [ { label: 'any', value: undefined }, { label: 'yes', value: true }, { label: 'no', value: false } ], - toggleVal: function (name, def) { - if (filter.vals[name] !== def) filter.vals[name] = def; - else filter.vals[name] = undefined; - }, reset: function () { filter.vals = _.clone(filter.defaults); }, - isFieldSelected: function (field) { - return field.display; - }, - isFieldFiltered: function (field) { - const matchFilter = (filter.vals.type === 'any' || field.type === filter.vals.type); - const isAggregatable = (filter.vals.aggregatable == null || field.aggregatable === filter.vals.aggregatable); - const isSearchable = (filter.vals.searchable == null || field.searchable === filter.vals.searchable); - const scriptedOrMissing = (!filter.vals.missing || field.scripted || field.rowCount > 0); - const matchName = (!filter.vals.name || field.name.indexOf(filter.vals.name) !== -1); - - return !field.display - && matchFilter - && isAggregatable - && isSearchable - && scriptedOrMissing - && matchName - ; + /** + * filter for fields that are displayed / selected for the data table + */ + isFieldFilteredAndDisplayed: function (field) { + return field.display && isFieldFiltered(field); }, - popularity: function (field) { - return field.count > 0; + /** + * filter for fields that are not displayed / selected for the data table + */ + isFieldFilteredAndNotDisplayed: function (field) { + return !field.display && isFieldFiltered(field) && field.type !== '_source'; }, getActive: function () { return _.some(filter.props, function (prop) { @@ -123,6 +106,20 @@ app.directive('discFieldChooser', function ($location, config, $route) { } }; + function isFieldFiltered(field) { + const matchFilter = (filter.vals.type === 'any' || field.type === filter.vals.type); + const isAggregatable = (filter.vals.aggregatable == null || field.aggregatable === filter.vals.aggregatable); + const isSearchable = (filter.vals.searchable == null || field.searchable === filter.vals.searchable); + const scriptedOrMissing = !filter.vals.missing || field.type === '_source' || field.scripted || field.rowCount > 0; + const matchName = (!filter.vals.name || field.name.indexOf(filter.vals.name) !== -1); + + return matchFilter && isAggregatable && isSearchable && scriptedOrMissing && matchName; + } + + $scope.setFilterValue = (name, value) => { + filter.vals[name] = value; + }; + // set the initial values to the defaults filter.reset(); diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index a9172a107e7ba5..4cce312accf5c4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -28,19 +28,20 @@
diff --git a/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss b/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss index a21f8fed525e7d..571064a1f29c4e 100644 --- a/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss +++ b/src/legacy/ui/public/styles/_legacy/components/_sidebar.scss @@ -1,8 +1,8 @@ // ONLY USED IN DISCOVER .sidebar-container { - padding-left: $euiSizeS !important; - padding-right: $euiSizeS !important; + padding-left: 0 !important; + padding-right: 0 !important; background-color: $euiColorLightestShade; border-right-color: transparent; border-bottom-color: transparent; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b57ab465b707b..57632ab6f640f4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1535,7 +1535,6 @@ "kbn.discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドは Elasticsearch マッピングに表示されますが、ドキュメントテーブルの {hitsLength} 件のドキュメントには含まれません。可視化や検索は可能な場合があります。", "kbn.discover.fieldChooser.filter.aggregatableLabel": "集約可能", "kbn.discover.fieldChooser.filter.availableFieldsTitle": "利用可能なフィールド", - "kbn.discover.fieldChooser.filter.fieldNameLabel": "フィールド名", "kbn.discover.fieldChooser.filter.hideMissingFieldsLabel": "未入力のフィールドを非表示", "kbn.discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "インデックスとフィールド", "kbn.discover.fieldChooser.filter.popularTitle": "人気", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3cd3ad3fdcd35e..606abe57875235 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1536,7 +1536,6 @@ "kbn.discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于其进行可视化或搜索。", "kbn.discover.fieldChooser.filter.aggregatableLabel": "可聚合", "kbn.discover.fieldChooser.filter.availableFieldsTitle": "可用字段", - "kbn.discover.fieldChooser.filter.fieldNameLabel": "字段名称", "kbn.discover.fieldChooser.filter.hideMissingFieldsLabel": "隐藏缺失字段", "kbn.discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "索引和字段", "kbn.discover.fieldChooser.filter.popularTitle": "常用", From 28c4bbfca6eea59161ca29688838d2ed3791de3b Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Wed, 2 Oct 2019 10:43:57 +0200 Subject: [PATCH 29/53] [ML] Job type page (#46933) * [ML] wip job types to react * [ML] delete angular job_type page * [ML] TS refactoring * [ML] restrict page width * [ML] refactor with CreateJobLinkCard * [ML] data-test-subj for functional tests * [ML] fix recognized results * [ML] missing i18n * [ML] add custom logo support for create job link card, change data recognizer layout * [ML] rename iconType prop * [ML] remove unused styles * [ML] fix page background * [ML] data recognizer wrappers * [ML] fix IE issue, IndexPatternSavedObject * [ML] fix callout * [ML] job type directive test * [ML] fix types --- x-pack/legacy/plugins/ml/public/_hacks.scss | 13 +- .../public/{breadcrumbs.js => breadcrumbs.ts} | 16 +- .../create_job_link_card.tsx | 82 ++++- .../data_recognizer/_data_recognizer.scss | 31 -- .../components/data_recognizer/_index.scss | 1 - .../data_recognizer/data_recognizer.d.ts | 9 +- .../data_recognizer/data_recognizer.js | 4 +- .../data_recognizer/recognized_result.js | 36 +- .../actions_panel/actions_panel.tsx | 13 +- x-pack/legacy/plugins/ml/public/index.scss | 1 - .../jobs/{breadcrumbs.js => breadcrumbs.ts} | 71 ++-- .../public/jobs/new_job/wizard/_wizard.scss | 53 --- .../ml/public/jobs/new_job/wizard/index.js | 1 - .../job_type/__tests__/job_type_controller.js | 46 --- .../new_job/wizard/steps/job_type/index.js | 9 - .../wizard/steps/job_type/job_type.html | 274 ---------------- .../steps/job_type/job_type_controller.js | 103 ------ .../ml/public/jobs/new_job_new/index.ts | 2 + .../pages/job_type/__test__/directive.js | 45 +++ .../new_job_new/pages/job_type/directive.tsx | 65 ++++ .../jobs/new_job_new/pages/job_type/page.tsx | 308 ++++++++++++++++++ .../jobs/new_job_new/pages/job_type/route.ts | 27 ++ .../jobs/new_job_new/pages/new_job/route.ts | 2 - .../plugins/ml/public/util/index_utils.js | 108 ------ .../plugins/ml/public/util/index_utils.ts | 110 +++++++ ...ently_accessed.js => recently_accessed.ts} | 12 +- 26 files changed, 703 insertions(+), 739 deletions(-) rename x-pack/legacy/plugins/ml/public/{breadcrumbs.js => breadcrumbs.ts} (76%) delete mode 100644 x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss delete mode 100644 x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss rename x-pack/legacy/plugins/ml/public/jobs/{breadcrumbs.js => breadcrumbs.ts} (55%) delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx create mode 100644 x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts delete mode 100644 x-pack/legacy/plugins/ml/public/util/index_utils.js create mode 100644 x-pack/legacy/plugins/ml/public/util/index_utils.ts rename x-pack/legacy/plugins/ml/public/util/{recently_accessed.js => recently_accessed.ts} (80%) diff --git a/x-pack/legacy/plugins/ml/public/_hacks.scss b/x-pack/legacy/plugins/ml/public/_hacks.scss index b0a8a43d23096d..39740360d8a840 100644 --- a/x-pack/legacy/plugins/ml/public/_hacks.scss +++ b/x-pack/legacy/plugins/ml/public/_hacks.scss @@ -1,21 +1,10 @@ .tab-datavisualizer_index_select, .tab-timeseriesexplorer, -.tab-explorer, -.tab-jobs { +.tab-explorer, { // Make all page background white until More of the pages use EuiPage to wrap in panel-like components background-color: $euiColorEmptyShade; } -.tab-jobs { - label { - display: inline-block; - } - - .validation-error { - margin-top: $euiSizeXS; - } -} - // ML specific bootstrap hacks .button-wrapper { display: inline; diff --git a/x-pack/legacy/plugins/ml/public/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/breadcrumbs.ts similarity index 76% rename from x-pack/legacy/plugins/ml/public/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/breadcrumbs.ts index bdde734be7c1a3..ba4703d4818ff5 100644 --- a/x-pack/legacy/plugins/ml/public/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/breadcrumbs.ts @@ -8,28 +8,28 @@ import { i18n } from '@kbn/i18n'; export const ML_BREADCRUMB = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { - defaultMessage: 'Machine Learning' + defaultMessage: 'Machine Learning', }), - href: '#/' + href: '#/', }); export const SETTINGS = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { - defaultMessage: 'Settings' + defaultMessage: 'Settings', }), - href: '#/settings?' + href: '#/settings?', }); export const ANOMALY_DETECTION_BREADCRUMB = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { - defaultMessage: 'Anomaly Detection' + defaultMessage: 'Anomaly Detection', }), - href: '#/jobs?' + href: '#/jobs?', }); export const DATA_VISUALIZER_BREADCRUMB = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { - defaultMessage: 'Data Visualizer' + defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer?' + href: '#/datavisualizer?', }); diff --git a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx index 6549df35ba381c..07a924caae7724 100644 --- a/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx +++ b/x-pack/legacy/plugins/ml/public/components/create_job_link_card/create_job_link_card.tsx @@ -4,25 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, ReactElement } from 'react'; -import { EuiCard, EuiIcon, IconType } from '@elastic/eui'; +import { + EuiIcon, + IconType, + EuiText, + EuiTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPanel, + EuiLink, +} from '@elastic/eui'; interface Props { - iconType: IconType; - title: string; - description: string; - onClick(): void; + icon: IconType | ReactElement; + iconAreaLabel?: string; + title: any; + description: any; + href?: string; + onClick?: () => void; + isDisabled?: boolean; + 'data-test-subj'?: string; } // Component for rendering a card which links to the Create Job page, displaying an // icon, card title, description and link. -export const CreateJobLinkCard: FC = ({ iconType, title, description, onClick }) => ( - } - title={title} - description={description} - onClick={onClick} - /> -); +export const CreateJobLinkCard: FC = ({ + icon, + iconAreaLabel, + title, + description, + onClick, + href, + isDisabled, + 'data-test-subj': dateTestSubj, +}) => { + const linkHrefAndOnClickProps = { + ...(href ? { href } : {}), + ...(onClick ? { onClick } : {}), + }; + return ( + + + + + {typeof icon === 'string' ? ( + + ) : ( + icon + )} + + + +

{title}

+
+ +

{description}

+
+
+
+
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss b/x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss deleted file mode 100644 index b915be2ab84536..00000000000000 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_data_recognizer.scss +++ /dev/null @@ -1,31 +0,0 @@ -ml-data-recognizer { - .ml-data-recognizer-logo { - width: $euiSizeXL; - } -} - -// Moved here from /home since it's no longer being used there -.synopsis { - display: flex; - flex-grow: 1; - cursor: pointer; - - &:hover, - &:focus { - text-decoration: none; - - .synopsisTitle { - text-decoration: underline; - } - } -} - -.synopsisTitle { - font-size: $euiSize; - font-weight: normal; - color: $euiColorPrimary; -} - -.synopsisIcon { - padding-top: $euiSizeS; -} diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss b/x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss deleted file mode 100644 index 67cc4372ea6225..00000000000000 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'data_recognizer'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts index c8a7bba2d189f5..e7d191a31e034e 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.d.ts @@ -7,9 +7,14 @@ import { FC } from 'react'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; declare const DataRecognizer: FC<{ indexPattern: IndexPattern; - results: any; - className: string; + savedSearch?: SavedSearch; + results: { + count: number; + onChange?: Function; + }; + className?: string; }>; diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js index fd754ee5191046..b303ed9b7f008b 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer.js @@ -57,9 +57,9 @@ export class DataRecognizer extends Component { render() { return ( -
+ <> {this.state.results} -
+ ); } } diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js b/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js index 60dc38f2291f89..6f511abf89e310 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/recognized_result.js @@ -11,7 +11,9 @@ import PropTypes from 'prop-types'; import { EuiIcon, + EuiFlexItem } from '@elastic/eui'; +import { CreateJobLinkCard } from '../create_job_link_card'; export const RecognizedResult = ({ config, @@ -28,35 +30,23 @@ export const RecognizedResult = ({ // if a logo is available, use that, otherwise display the id // the logo should be a base64 encoded image or an eui icon if(config.logo && config.logo.icon) { - logo =
; + logo = ; } else if (config.logo && config.logo.src) { - logo =
; + logo = ; } else { logo =

{config.id}

; } return ( - + + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 9a291cabf558f9..c8295a1e3d8db3 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from 'ui/index_patterns'; -import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; import { useUiChromeContext } from '../../../../contexts/ui/use_ui_chrome_context'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; @@ -63,11 +63,9 @@ export const ActionsPanel: FC = ({ indexPattern }) => {

- + + +
@@ -80,7 +78,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { = ({ indexPattern }) => { 'Use the full range of options to create a job for more advanced use cases', })} onClick={openAdvancedJobWizard} + href={`${basePath}/app/ml#/jobs/new_job/advanced?index=${indexPattern}`} /> ); diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index 4cab633d5fa569..a3fefb7b1fac86 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -36,7 +36,6 @@ @import 'components/chart_tooltip/index'; @import 'components/confirm_modal/index'; @import 'components/controls/index'; - @import 'components/data_recognizer/index'; @import 'components/documentation_help_link/index'; @import 'components/entity_cell/index'; @import 'components/field_title_bar/index'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts similarity index 55% rename from x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.js rename to x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts index d066a524d70aab..35e9c3326a4ccf 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.js +++ b/x-pack/legacy/plugins/ml/public/jobs/breadcrumbs.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ - -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../breadcrumbs'; import { i18n } from '@kbn/i18n'; +import { Breadcrumb } from 'ui/chrome'; +import { + ANOMALY_DETECTION_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + ML_BREADCRUMB, +} from '../breadcrumbs'; - -export function getJobManagementBreadcrumbs() { +export function getJobManagementBreadcrumbs(): Breadcrumb[] { // Whilst top level nav menu with tabs remains, // use root ML breadcrumb. return [ @@ -17,93 +20,93 @@ export function getJobManagementBreadcrumbs() { ANOMALY_DETECTION_BREADCRUMB, { text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management' + defaultMessage: 'Job Management', }), - href: '' - } + href: '', + }, ]; } -export function getCreateJobBreadcrumbs() { +export function getCreateJobBreadcrumbs(): Breadcrumb[] { return [ ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, { text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { - defaultMessage: 'Create job' + defaultMessage: 'Create job', }), - href: '#/jobs/new_job' - } + href: '#/jobs/new_job', + }, ]; } -export function getCreateSingleMetricJobBreadcrumbs() { +export function getCreateSingleMetricJobBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { - defaultMessage: 'Single metric' + defaultMessage: 'Single metric', }), - href: '' - } + href: '', + }, ]; } -export function getCreateMultiMetricJobBreadcrumbs() { +export function getCreateMultiMetricJobBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { - defaultMessage: 'Multi metric' + defaultMessage: 'Multi metric', }), - href: '' - } + href: '', + }, ]; } -export function getCreatePopulationJobBreadcrumbs() { +export function getCreatePopulationJobBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { - defaultMessage: 'Population' + defaultMessage: 'Population', }), - href: '' - } + href: '', + }, ]; } -export function getAdvancedJobConfigurationBreadcrumbs() { +export function getAdvancedJobConfigurationBreadcrumbs(): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { - defaultMessage: 'Advanced configuration' + defaultMessage: 'Advanced configuration', }), - href: '' - } + href: '', + }, ]; } -export function getCreateRecognizerJobBreadcrumbs($routeParams) { +export function getCreateRecognizerJobBreadcrumbs($routeParams: any): Breadcrumb[] { return [ ...getCreateJobBreadcrumbs(), { text: $routeParams.id, - href: '' - } + href: '', + }, ]; } -export function getDataVisualizerIndexOrSearchBreadcrumbs() { +export function getDataVisualizerIndexOrSearchBreadcrumbs(): Breadcrumb[] { return [ ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { - defaultMessage: 'Select index or search' + defaultMessage: 'Select index or search', }), - href: '' - } + href: '', + }, ]; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss index 7eadb9a8ce77a0..def24f6d6a7476 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/_wizard.scss @@ -1,56 +1,3 @@ -.job-type-gallery { - width: 100%; - padding-right: $euiSizeS; - padding-left: $euiSizeS; - background-color: $euiColorLightestShade; - flex: 1 0 auto; - - .job-types-content { - max-width: 1200px; // SASSTODO: Proper calc - margin-right: auto; - margin-left: auto; - } - - .synopsis { - display: flex; - flex-grow: 1; - - .synopsisTitle { - font-size: $euiFontSize; - font-weight: normal; - color: $euiColorPrimary; - } - - .synopsisIcon { - padding-top: $euiSizeS; - } - } - - .synopsis:hover { - text-decoration: none; - - .synopsisTitle { - text-decoration: underline; - } - } - - .euiFlexItem.disabled { - cursor: not-allowed; - } - - .synopsis.disabled { - pointer-events: none; - - .synopsisTitle { - color: $euiColorDarkShade; - } - } - - .index-warning { - border: $euiBorderThin; - } -} - .index-or-saved-search-selection { .kuiBarSection .kuiButtonGroup { display: none; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js index c2fb4646306692..61ce488f69014b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/index.js @@ -9,5 +9,4 @@ // SASS TODO: Import wizard.scss instead // import 'plugins/kibana/visualize/wizard/wizard.less'; import './steps/index_or_search'; -import './steps/job_type'; import 'plugins/ml/components/data_recognizer'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js deleted file mode 100644 index f8fd13b2ae36e1..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/__tests__/job_type_controller.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from 'plugins/ml/util/index_utils'; - -describe('ML - Job Type Controller', () => { - beforeEach(() => { - ngMock.module('kibana'); - }); - - it('Initialize Job Type Controller', (done) => { - const stub = sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function ($rootScope, $controller, $route) { - // Set up the $route current props required for the tests. - $route.current = { - locals: { - indexPattern: { - id: 'test_id', - title: 'test_pattern' - }, - savedSearch: {} - } - }; - - const scope = $rootScope.$new(); - - expect(() => { - $controller('MlNewJobStepJobType', { $scope: scope }); - }).to.not.throwError(); - - expect(scope.indexWarningTitle).to.eql('Index pattern test_pattern is not time based'); - stub.restore(); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js deleted file mode 100644 index 9a9fa7e73b9f1e..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * 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 './job_type_controller'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html deleted file mode 100644 index 1dc3aea215d93a..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html +++ /dev/null @@ -1,274 +0,0 @@ - - - -
- -
-

-
- -
-
-
-
- - - - - - - {{indexWarningTitle}} -
-
-

- -
- -

-
-
-
-
-
- -
-
-

-

-

-
-
- -
-
- -
-

-

-
- -
- - - -
- -
-

-

-
-
- - - -
- -
-
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js deleted file mode 100644 index df7768ee9f0c84..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type_controller.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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. - */ - - - -/* - * Controller for the second step in the Create Job wizard, allowing - * the user to select the type of job they wish to create. - */ - -import uiRoutes from 'ui/routes'; -import { i18n } from '@kbn/i18n'; -import { checkLicenseExpired } from 'plugins/ml/license/check_license'; -import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege'; -import { getCreateJobBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs'; -import { SearchItemsProvider } from 'plugins/ml/jobs/new_job/utils/new_job_utils'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch, timeBasedIndexCheck } from 'plugins/ml/util/index_utils'; -import { addItemToRecentlyAccessed } from 'plugins/ml/util/recently_accessed'; -import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; -import template from './job_type.html'; -import { timefilter } from 'ui/timefilter'; - -uiRoutes - .when('/jobs/new_job/step/job_type', { - template, - k7Breadcrumbs: getCreateJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - } - }); - - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.controller('MlNewJobStepJobType', - function ($scope, Private) { - - timefilter.disableTimeRangeSelector(); // remove time picker from top of page - timefilter.disableAutoRefreshSelector(); // remove time picker from top of page - - const createSearchItems = Private(SearchItemsProvider); - const { - indexPattern, - savedSearch } = createSearchItems(); - - // check to see that the index pattern is time based. - // if it isn't, display a warning and disable all links - $scope.indexWarningTitle = ''; - $scope.isTimeBasedIndex = timeBasedIndexCheck(indexPattern); - if ($scope.isTimeBasedIndex === false) { - $scope.indexWarningTitle = (savedSearch.id === undefined) ? - i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { - defaultMessage: 'Index pattern {indexPatternTitle} is not time based', - values: { indexPatternTitle: indexPattern.title } - }) - : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', { - defaultMessage: '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', - values: { - savedSearchTitle: savedSearch.title, - indexPatternTitle: indexPattern.title - } - }); - } - - $scope.indexPattern = indexPattern; - $scope.savedSearch = savedSearch; - $scope.recognizerResults = { - count: 0, - onChange() { - $scope.$applyAsync(); - } - }; - - $scope.pageTitleLabel = (savedSearch.id !== undefined) ? - i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title } - }) - : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { - defaultMessage: 'index pattern {indexPatternTitle}', - values: { indexPatternTitle: indexPattern.title } - }); - - $scope.getUrl = function (basePath) { - return (savedSearch.id === undefined) ? `${basePath}?index=${indexPattern.id}` : - `${basePath}?savedSearchId=${savedSearch.id}`; - }; - - $scope.addSelectionToRecentlyAccessed = function () { - const title = (savedSearch.id === undefined) ? indexPattern.title : savedSearch.title; - const url = $scope.getUrl(''); - addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); - }; - - }); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts index d3feaf087524c8..2366f2c655000d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/index.ts @@ -6,3 +6,5 @@ import './pages/new_job/route'; import './pages/new_job/directive'; +import './pages/job_type/route'; +import './pages/job_type/directive'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js new file mode 100644 index 00000000000000..5be526f2eb2c02 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/__test__/directive.js @@ -0,0 +1,45 @@ +/* + * 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 ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +// Import this way to be able to stub/mock functions later on in the tests using sinon. +import * as indexUtils from 'plugins/ml/util/index_utils'; + +describe('ML - Job Type Directive', () => { + let $scope; + let $compile; + let $element; + + beforeEach(ngMock.module('kibana')); + beforeEach(() => { + ngMock.inject(function ($injector) { + $compile = $injector.get('$compile'); + const $rootScope = $injector.get('$rootScope'); + $scope = $rootScope.$new(); + }); + }); + + afterEach(() => { + $scope.$destroy(); + }); + + it('Initialize Job Type Directive', done => { + sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); + ngMock.inject(function () { + expect(() => { + $element = $compile('')($scope); + }).to.not.throwError(); + + // directive has scope: false + const scope = $element.isolateScope(); + expect(scope).to.eql(undefined); + done(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx new file mode 100644 index 00000000000000..4ad689a943160c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx @@ -0,0 +1,65 @@ +/* + * 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 ReactDOM from 'react-dom'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +const module = uiModules.get('apps/ml', ['react']); +import { timefilter } from 'ui/timefilter'; +import { IndexPatterns } from 'ui/index_patterns'; + +import { I18nContext } from 'ui/i18n'; +import { IPrivate } from 'ui/private'; +import { InjectorService } from '../../../../../common/types/angular'; + +import { SearchItemsProvider } from '../../../new_job/utils/new_job_utils'; +import { Page } from './page'; + +import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; + +module.directive('mlJobTypePage', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + // remove time picker from top of page + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + const indexPatterns = $injector.get('indexPatterns'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kibanaConfig = $injector.get('config'); + const Private = $injector.get('Private'); + + const createSearchItems = Private(SearchItemsProvider); + const { indexPattern, savedSearch, combinedQuery } = createSearchItems(); + const kibanaContext = { + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns, + kbnBaseUrl, + kibanaConfig, + }; + + ReactDOM.render( + + + {React.createElement(Page)} + + , + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx new file mode 100644 index 00000000000000..4991039ffa2886 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/page.tsx @@ -0,0 +1,308 @@ +/* + * 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, { FC, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiTitle, + EuiSpacer, + EuiCallOut, + EuiText, + EuiFlexGrid, + EuiFlexItem, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibanaContext } from '../../../../contexts/kibana'; +import { DataRecognizer } from '../../../../components/data_recognizer'; +import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; +import { timeBasedIndexCheck } from '../../../../util/index_utils'; +import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; + +export const Page: FC = () => { + const kibanaContext = useKibanaContext(); + const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); + + const { currentSavedSearch, currentIndexPattern } = kibanaContext; + + const isTimeBasedIndex = timeBasedIndexCheck(currentIndexPattern); + const indexWarningTitle = + !isTimeBasedIndex && currentSavedSearch.id === undefined + ? i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { + defaultMessage: 'Index pattern {indexPatternTitle} is not time based', + values: { indexPatternTitle: currentIndexPattern.title }, + }) + : i18n.translate( + 'xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', + { + defaultMessage: + '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', + values: { + savedSearchTitle: currentSavedSearch.title, + indexPatternTitle: currentIndexPattern.title, + }, + } + ); + const pageTitleLabel = + currentSavedSearch.id !== undefined + ? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: currentSavedSearch.title }, + }) + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: currentIndexPattern.title }, + }); + + const recognizerResults = { + count: 0, + onChange() { + setRecognizerResultsCount(recognizerResults.count); + }, + }; + + const getUrl = (basePath: string) => { + return currentSavedSearch.id === undefined + ? `${basePath}?index=${currentIndexPattern.id}` + : `${basePath}?savedSearchId=${currentSavedSearch.id}`; + }; + + const addSelectionToRecentlyAccessed = () => { + const title = + currentSavedSearch.id === undefined ? currentIndexPattern.title : currentSavedSearch.title; + const url = getUrl(''); + addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); + + window.location.href = getUrl('#jobs/new_job/datavisualizer'); + }; + + const jobTypes = [ + { + href: getUrl('#jobs/new_job/single_metric'), + icon: { + type: 'createSingleMetricJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricAriaLabel', { + defaultMessage: 'Single metric job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricTitle', { + defaultMessage: 'Single metric', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.singleMetricDescription', { + defaultMessage: 'Detect anomalies in a single time series.', + }), + id: 'mlJobTypeLinkSingleMetricJob', + }, + { + href: getUrl('#jobs/new_job/multi_metric'), + icon: { + type: 'createMultiMetricJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricAriaLabel', { + defaultMessage: 'Multi metric job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricTitle', { + defaultMessage: 'Multi metric', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.multiMetricDescription', { + defaultMessage: + 'Detect anomalies in multiple metrics by splitting a time series by a categorical field.', + }), + id: 'mlJobTypeLinkMultiMetricJob', + }, + { + href: getUrl('#jobs/new_job/population'), + icon: { + type: 'createPopulationJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.populationAriaLabel', { + defaultMessage: 'Population job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.populationTitle', { + defaultMessage: 'Population', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.populationDescription', { + defaultMessage: + 'Detect activity that is unusual compared to the behavior of the population.', + }), + id: 'mlJobTypeLinkPopulationJob', + }, + { + href: getUrl('#jobs/new_job/advanced'), + icon: { + type: 'createAdvancedJob', + ariaLabel: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedAriaLabel', { + defaultMessage: 'Advanced job', + }), + }, + title: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedTitle', { + defaultMessage: 'Advanced', + }), + description: i18n.translate('xpack.ml.newJob.wizard.jobType.advancedDescription', { + defaultMessage: + 'Use the full range of options to create a job for more advanced use cases.', + }), + id: 'mlJobTypeLinkAdvancedJob', + }, + ]; + + return ( + + + +

+ +

+
+ + + {isTimeBasedIndex === false && ( + <> + + +
+ + + +
+ + + )} + + + + + +

+ +

+
+ +

+ +

+
+ + + + + {jobTypes.map(({ href, icon, title, description, id }) => ( + + + + ))} + + + + + + +

+ +

+
+ +

+ +

+
+ + + + + + + } + description={ + + } + onClick={addSelectionToRecentlyAccessed} + href={getUrl('#jobs/new_job/datavisualizer')} + /> + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts new file mode 100644 index 00000000000000..b61424998705d6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/route.ts @@ -0,0 +1,27 @@ +/* + * 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 uiRoutes from 'ui/routes'; + +// @ts-ignore +import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes'; +// @ts-ignore +import { checkLicenseExpired } from '../../../../license/check_license'; +import { checkCreateJobsPrivilege } from '../../../../privilege/check_privilege'; +import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; +import { getCreateJobBreadcrumbs } from '../../../breadcrumbs'; + +uiRoutes.when('/jobs/new_job/step/job_type', { + template: '', + k7Breadcrumbs: getCreateJobBreadcrumbs, + resolve: { + CheckLicense: checkLicenseExpired, + privileges: checkCreateJobsPrivilege, + indexPattern: loadCurrentIndexPattern, + savedSearch: loadCurrentSavedSearch, + checkMlNodesAvailable, + }, +}); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts index cdca3a810fcdd8..08f05e6884bb35 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts @@ -9,14 +9,12 @@ import uiRoutes from 'ui/routes'; // @ts-ignore import { checkFullLicense } from '../../../../license/check_license'; import { checkGetJobsPrivilege } from '../../../../privilege/check_privilege'; -// @ts-ignore import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; import { getCreateSingleMetricJobBreadcrumbs, getCreateMultiMetricJobBreadcrumbs, getCreatePopulationJobBreadcrumbs, - // @ts-ignore } from '../../../breadcrumbs'; import { Route } from '../../../../../common/types/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/util/index_utils.js b/x-pack/legacy/plugins/ml/public/util/index_utils.js deleted file mode 100644 index dfc6a7735616a6..00000000000000 --- a/x-pack/legacy/plugins/ml/public/util/index_utils.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 { toastNotifications } from 'ui/notify'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { i18n } from '@kbn/i18n'; - -let indexPatternCache = []; -let fullIndexPatterns = []; -let currentIndexPattern = null; -let currentSavedSearch = null; - -export let refreshIndexPatterns = null; - -export function loadIndexPatterns(Private, indexPatterns) { - fullIndexPatterns = indexPatterns; - const savedObjectsClient = Private(SavedObjectsClientProvider); - return savedObjectsClient.find({ - type: 'index-pattern', - fields: ['id', 'title', 'type', 'fields'], - perPage: 10000 - }).then((response) => { - indexPatternCache = response.savedObjects; - - if (refreshIndexPatterns === null) { - refreshIndexPatterns = () => { - return new Promise((resolve, reject) => { - loadIndexPatterns(Private, indexPatterns) - .then((resp) => { - resolve(resp); - }) - .catch((error) => { - reject(error); - }); - }); - }; - } - - return indexPatternCache; - }); -} - -export function getIndexPatterns() { - return indexPatternCache; -} - -export function getIndexPatternNames() { - return indexPatternCache.map(i => (i.attributes && i.attributes.title)); -} - -export function getIndexPatternIdFromName(name) { - for (let j = 0; j < indexPatternCache.length; j++) { - if (indexPatternCache[j].get('title') === name) { - return indexPatternCache[j].id; - } - } - return name; -} - -export function loadCurrentIndexPattern(indexPatterns, $route) { - fullIndexPatterns = indexPatterns; - currentIndexPattern = fullIndexPatterns.get($route.current.params.index); - return currentIndexPattern; -} - -export function getIndexPatternById(id) { - return fullIndexPatterns.get(id); -} - -export function loadCurrentSavedSearch(savedSearches, $route) { - currentSavedSearch = savedSearches.get($route.current.params.savedSearchId); - return currentSavedSearch; -} - -export function getCurrentIndexPattern() { - return currentIndexPattern; -} - -export function getCurrentSavedSearch() { - return currentSavedSearch; -} - -// returns true if the index passed in is time based -// an optional flag will trigger the display a notification at the top of the page -// warning that the index is not time based -export function timeBasedIndexCheck(indexPattern, showNotification = false) { - if (indexPattern.isTimeBased() === false) { - if (showNotification) { - toastNotifications.addWarning({ - title: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationTitle', { - defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', - values: { indexPatternTitle: indexPattern.title } - }), - text: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationDescription', { - defaultMessage: 'Anomaly detection only runs over time-based indices' - }), - }); - } - return false; - } else { - return true; - } -} diff --git a/x-pack/legacy/plugins/ml/public/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/util/index_utils.ts new file mode 100644 index 00000000000000..41dd13555726c4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/util/index_utils.ts @@ -0,0 +1,110 @@ +/* + * 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 { toastNotifications } from 'ui/notify'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatterns } from 'ui/index_patterns'; +import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; +import chrome from 'ui/chrome'; +import { SavedSearchLoader } from '../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { setup as data } from '../../../../../../src/legacy/core_plugins/data/public/legacy'; + +type IndexPatternSavedObject = SimpleSavedObject; + +let indexPatternCache: IndexPatternSavedObject[] = []; +let fullIndexPatterns: IndexPatterns | null = null; + +export let refreshIndexPatterns: (() => Promise) | null = null; + +export function loadIndexPatterns() { + fullIndexPatterns = data.indexPatterns.indexPatterns; + const savedObjectsClient = chrome.getSavedObjectsClient(); + return savedObjectsClient + .find({ + type: 'index-pattern', + fields: ['id', 'title', 'type', 'fields'], + perPage: 10000, + }) + .then(response => { + indexPatternCache = response.savedObjects; + if (refreshIndexPatterns === null) { + refreshIndexPatterns = () => { + return new Promise((resolve, reject) => { + loadIndexPatterns() + .then(resp => { + resolve(resp); + }) + .catch(error => { + reject(error); + }); + }); + }; + } + + return indexPatternCache; + }); +} + +export function getIndexPatterns() { + return indexPatternCache; +} + +export function getIndexPatternNames() { + return indexPatternCache.map(i => i.attributes && i.attributes.title); +} + +export function getIndexPatternIdFromName(name: string) { + for (let j = 0; j < indexPatternCache.length; j++) { + if (indexPatternCache[j].get('title') === name) { + return indexPatternCache[j].id; + } + } + return name; +} + +export function loadCurrentIndexPattern(indexPatterns: IndexPatterns, $route: Record) { + fullIndexPatterns = indexPatterns; + return fullIndexPatterns.get($route.current.params.index); +} + +export function getIndexPatternById(id: string): IndexPattern { + if (fullIndexPatterns !== null) { + return fullIndexPatterns.get(id); + } else { + throw new Error('Index patterns are not initialized!'); + } +} + +export function loadCurrentSavedSearch( + savedSearches: SavedSearchLoader, + $route: Record +) { + return savedSearches.get($route.current.params.savedSearchId); +} + +/** + * Returns true if the index passed in is time based + * an optional flag will trigger the display a notification at the top of the page + * warning that the index is not time based + */ +export function timeBasedIndexCheck(indexPattern: IndexPattern, showNotification = false) { + if (!indexPattern.isTimeBased()) { + if (showNotification) { + toastNotifications.addWarning({ + title: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationTitle', { + defaultMessage: 'The index pattern {indexPatternTitle} is not based on a time series', + values: { indexPatternTitle: indexPattern.title }, + }), + text: i18n.translate('xpack.ml.indexPatternNotBasedOnTimeSeriesNotificationDescription', { + defaultMessage: 'Anomaly detection only runs over time-based indices', + }), + }); + } + return false; + } else { + return true; + } +} diff --git a/x-pack/legacy/plugins/ml/public/util/recently_accessed.js b/x-pack/legacy/plugins/ml/public/util/recently_accessed.ts similarity index 80% rename from x-pack/legacy/plugins/ml/public/util/recently_accessed.js rename to x-pack/legacy/plugins/ml/public/util/recently_accessed.ts index b642be7d1226a1..9a3d3089dff2bf 100644 --- a/x-pack/legacy/plugins/ml/public/util/recently_accessed.js +++ b/x-pack/legacy/plugins/ml/public/util/recently_accessed.ts @@ -4,38 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ - - // utility functions for managing which links get added to kibana's recently accessed list import { recentlyAccessed } from 'ui/persisted_log'; import { i18n } from '@kbn/i18n'; -export function addItemToRecentlyAccessed(page, itemId, url) { +export function addItemToRecentlyAccessed(page: string, itemId: string, url: string) { let pageLabel = ''; let id = `ml-job-${itemId}`; switch (page) { case 'explorer': pageLabel = i18n.translate('xpack.ml.anomalyExplorerPageLabel', { - defaultMessage: 'Anomaly Explorer' + defaultMessage: 'Anomaly Explorer', }); break; case 'timeseriesexplorer': pageLabel = i18n.translate('xpack.ml.singleMetricViewerPageLabel', { - defaultMessage: 'Single Metric Viewer' + defaultMessage: 'Single Metric Viewer', }); break; case 'jobs/new_job/datavisualizer': pageLabel = i18n.translate('xpack.ml.dataVisualizerPageLabel', { - defaultMessage: 'Data Visualizer' + defaultMessage: 'Data Visualizer', }); id = `ml-datavisualizer-${itemId}`; break; default: + // eslint-disable-next-line no-console console.error('addItemToRecentlyAccessed - No page specified'); return; - break; } url = `ml#/${page}/${url}`; From 7279f064ca331be8a080c198c5de926cd78579e9 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 2 Oct 2019 02:00:58 -0700 Subject: [PATCH 30/53] [ML] Remove deprecated force parameter. (#46361) The force parameter was removed from start actions in elastic/elasticsearch#46414. This reflects the change in the UI API calls. --- .../ml/server/client/elasticsearch_ml.js | 5 +---- .../ml/server/models/data_frame/transforms.ts | 20 +++++++++---------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js b/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js index ae38eeada3124e..2a186988a750d6 100644 --- a/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js +++ b/x-pack/legacy/plugins/ml/server/client/elasticsearch_ml.js @@ -287,14 +287,11 @@ export const elasticsearchJsPlugin = (Client, config, components) => { ml.startDataFrameTransform = ca({ urls: [ { - fmt: '/_data_frame/transforms/<%=transformId%>/_start?&force=<%=force%>', + fmt: '/_data_frame/transforms/<%=transformId%>/_start', req: { transformId: { type: 'string' }, - force: { - type: 'boolean' - } } } ], diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame/transforms.ts b/x-pack/legacy/plugins/ml/server/models/data_frame/transforms.ts index 392fb4191eba04..f5bce55763498e 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_frame/transforms.ts +++ b/x-pack/legacy/plugins/ml/server/models/data_frame/transforms.ts @@ -19,9 +19,13 @@ enum TRANSFORM_ACTIONS { DELETE = 'delete', } -interface StartStopOptions { +interface StartTransformOptions { transformId: DataFrameTransformId; - force: boolean; +} + +interface StopTransformOptions { + transformId: DataFrameTransformId; + force?: boolean; waitForCompletion?: boolean; } @@ -30,11 +34,11 @@ export function transformServiceProvider(callWithRequest: callWithRequestType) { return callWithRequest('ml.deleteDataFrameTransform', { transformId }); } - async function stopTransform(options: StartStopOptions) { + async function stopTransform(options: StopTransformOptions) { return callWithRequest('ml.stopDataFrameTransform', options); } - async function startTransform(options: StartStopOptions) { + async function startTransform(options: StartTransformOptions) { return callWithRequest('ml.startDataFrameTransform', options); } @@ -86,13 +90,7 @@ export function transformServiceProvider(callWithRequest: callWithRequestType) { for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; try { - await startTransform({ - transformId, - force: - transformInfo.state !== undefined - ? transformInfo.state === DATA_FRAME_TRANSFORM_STATE.FAILED - : false, - }); + await startTransform({ transformId }); results[transformId] = { success: true }; } catch (e) { if (isRequestTimeout(e)) { From 1a452cdf9bfe5df507eec8a11881b6e83acf873a Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Wed, 2 Oct 2019 10:30:36 +0100 Subject: [PATCH 31/53] Removing default title from saving a new search in Discover (#47031) --- .../kibana/public/discover/saved_searches/_saved_search.js | 5 +---- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js index ebf830dfae6426..eed6bcad0ec5d4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js @@ -18,7 +18,6 @@ */ import 'ui/notify'; -import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { createLegacyClass } from 'ui/utils/legacy_class'; import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; @@ -38,9 +37,7 @@ module.factory('SavedSearch', function (Private) { id: id, defaults: { - title: i18n.translate('kbn.discover.savedSearch.newSavedSearchTitle', { - defaultMessage: 'New Saved Search', - }), + title: '', description: '', columns: [], hits: 0, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 57632ab6f640f4..16a338e2039821 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1580,7 +1580,6 @@ "kbn.discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。", "kbn.discover.painlessError.painlessScriptedFieldErrorMessage": "Painless スクリプトのフィールド「{script}」のエラー.", "kbn.discover.rootBreadcrumb": "ディスカバリ", - "kbn.discover.savedSearch.newSavedSearchTitle": "新しく保存された検索", "kbn.discover.savedSearch.savedObjectName": "保存された検索", "kbn.discover.scaledToDescription": "{bucketIntervalDescription} にスケーリング済み", "kbn.discover.searchingTitle": "検索中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 606abe57875235..d9988b34b272d9 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1581,7 +1581,6 @@ "kbn.discover.notifications.savedSearchTitle": "搜索 “{savedSearchTitle}” 已保存", "kbn.discover.painlessError.painlessScriptedFieldErrorMessage": "Painless 脚本字段 “{script}” 有错误。", "kbn.discover.rootBreadcrumb": "Discover", - "kbn.discover.savedSearch.newSavedSearchTitle": "新保存的搜索", "kbn.discover.savedSearch.savedObjectName": "已保存搜索", "kbn.discover.scaledToDescription": "已缩放至 {bucketIntervalDescription}", "kbn.discover.searchingTitle": "正在搜索", From d8a576536200b0c3f942ba876c1697c68576c824 Mon Sep 17 00:00:00 2001 From: Maryia Lapata Date: Wed, 2 Oct 2019 13:48:39 +0300 Subject: [PATCH 32/53] [Vis: Default Editor] Prevent disabling of the only metrics agg (#46575) * Prevent disabling of the only metrics agg * Add unit tests * Fix when aggs with different schema name --- .../editors/default/components/agg.test.tsx | 13 ++++++++++ .../vis/editors/default/components/agg.tsx | 4 +++ .../editors/default/components/agg_group.tsx | 16 +++++++++--- .../components/agg_group_helper.test.ts | 25 ++++++++++++++++++- .../default/components/agg_group_helper.tsx | 20 +++++++++------ .../ui/public/vis/editors/default/sidebar.js | 10 ++++++++ 6 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx index 8f49c93b5c152e..b87eb3f5fb303c 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx @@ -61,6 +61,7 @@ describe('DefaultEditorAgg component', () => { dragHandleProps: null, formIsTouched: false, groupName: AggGroupNames.Metrics, + isDisabled: false, isDraggable: false, isLastBucket: false, isRemovable: false, @@ -200,6 +201,18 @@ describe('DefaultEditorAgg component', () => { expect(defaultProps.onToggleEnableAgg).toBeCalledWith(defaultProps.agg, false); }); + it('should disable the disableAggregation button', () => { + defaultProps.isDisabled = true; + defaultProps.isRemovable = true; + const comp = mount(); + + expect( + comp + .find('EuiButtonIcon[data-test-subj="toggleDisableAggregationBtn disable"]') + .prop('disabled') + ).toBeTruthy(); + }); + it('should enable agg', () => { defaultProps.agg.enabled = false; const comp = mount(); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.tsx index e8b4839c8f85e1..345c9254ff6c15 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg.tsx @@ -37,6 +37,7 @@ export interface DefaultEditorAggProps extends DefaultEditorAggCommonProps { aggIndex: number; aggIsTooLow: boolean; dragHandleProps: {} | null; + isDisabled: boolean; isDraggable: boolean; isLastBucket: boolean; isRemovable: boolean; @@ -49,6 +50,7 @@ function DefaultEditorAgg({ dragHandleProps, formIsTouched, groupName, + isDisabled, isDraggable, isLastBucket, isRemovable, @@ -142,6 +144,7 @@ function DefaultEditorAgg({ actionIcons.push({ id: 'disableAggregation', color: 'text', + disabled: isDisabled, type: 'eye', onClick: () => onToggleEnableAgg(agg, false), tooltip: i18n.translate('common.ui.vis.editors.agg.disableAggButtonTooltip', { @@ -205,6 +208,7 @@ function DefaultEditorAgg({ return ( void; + addSchema: (schemas: Schema) => void; reorderAggs: (group: AggConfig[]) => void; } @@ -77,6 +82,10 @@ function DefaultEditorAggGroup({ const isGroupValid = Object.values(aggsState).every(item => item.valid); const isAllAggsTouched = isInvalidAggsTouched(aggsState); + const isMetricAggregationDisabled = useMemo( + () => groupName === AggGroupNames.Metrics && getEnabledMetricAggsCount(group) === 1, + [groupName, group] + ); useEffect(() => { // when isAllAggsTouched is true, it means that all invalid aggs are touched and we will set ngModel's touched to true @@ -155,6 +164,7 @@ function DefaultEditorAggGroup({ isDraggable={stats.count > 1} isLastBucket={groupName === AggGroupNames.Buckets && index === group.length - 1} isRemovable={isAggRemovable(agg, group)} + isDisabled={agg.schema.name === 'metric' && isMetricAggregationDisabled} lastParentPipelineAggTitle={lastParentPipelineAggTitle} metricAggs={metricAggs} state={state} diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.test.ts b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.test.ts index b2dac344950bd4..6bb27d4a0c14eb 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.test.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.test.ts @@ -18,7 +18,12 @@ */ import { AggConfig } from '../../../../agg_types/agg_config'; -import { isAggRemovable, calcAggIsTooLow, isInvalidAggsTouched } from './agg_group_helper'; +import { + isAggRemovable, + calcAggIsTooLow, + isInvalidAggsTouched, + getEnabledMetricAggsCount, +} from './agg_group_helper'; import { AggsState } from './agg_group_state'; describe('DefaultEditorGroup helpers', () => { @@ -46,6 +51,7 @@ describe('DefaultEditorGroup helpers', () => { } as AggConfig, ]; }); + describe('isAggRemovable', () => { it('should return true when the number of aggs with the same schema is above the min', () => { const isRemovable = isAggRemovable(group[0], group); @@ -60,6 +66,23 @@ describe('DefaultEditorGroup helpers', () => { }); }); + describe('getEnabledMetricAggsCount', () => { + it('should return 1 when there is the only enabled agg', () => { + group[0].enabled = true; + const enabledAggs = getEnabledMetricAggsCount(group); + + expect(enabledAggs).toBe(1); + }); + + it('should return 2 when there are multiple enabled aggs', () => { + group[0].enabled = true; + group[1].enabled = true; + const enabledAggs = getEnabledMetricAggsCount(group); + + expect(enabledAggs).toBe(2); + }); + }); + describe('calcAggIsTooLow', () => { it('should return false when agg.schema.mustBeFirst has falsy value', () => { const isRemovable = calcAggIsTooLow(group[1], 0, group); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx index 5bfb2cdfb48d8b..847aa0b87d2d30 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_helper.tsx @@ -17,22 +17,28 @@ * under the License. */ -import { findIndex, reduce, isEmpty } from 'lodash'; +import { findIndex, isEmpty } from 'lodash'; import { AggConfig } from '../../../../agg_types/agg_config'; import { AggsState } from './agg_group_state'; const isAggRemovable = (agg: AggConfig, group: AggConfig[]) => { - const metricCount = reduce( - group, - (count, aggregation: AggConfig) => { - return aggregation.schema.name === agg.schema.name ? ++count : count; - }, + const metricCount = group.reduce( + (count, aggregation: AggConfig) => + aggregation.schema.name === agg.schema.name ? ++count : count, 0 ); // make sure the the number of these aggs is above the min return metricCount > agg.schema.min; }; +const getEnabledMetricAggsCount = (group: AggConfig[]) => { + return group.reduce( + (count, aggregation: AggConfig) => + aggregation.schema.name === 'metric' && aggregation.enabled ? ++count : count, + 0 + ); +}; + const calcAggIsTooLow = (agg: AggConfig, aggIndex: number, group: AggConfig[]) => { if (!agg.schema.mustBeFirst) { return false; @@ -59,4 +65,4 @@ function isInvalidAggsTouched(aggsState: AggsState) { return invalidAggs.every(agg => agg.touched); } -export { isAggRemovable, calcAggIsTooLow, isInvalidAggsTouched }; +export { isAggRemovable, calcAggIsTooLow, isInvalidAggsTouched, getEnabledMetricAggsCount }; diff --git a/src/legacy/ui/public/vis/editors/default/sidebar.js b/src/legacy/ui/public/vis/editors/default/sidebar.js index ee33e0093f42b8..92cb99c56038db 100644 --- a/src/legacy/ui/public/vis/editors/default/sidebar.js +++ b/src/legacy/ui/public/vis/editors/default/sidebar.js @@ -24,6 +24,8 @@ import 'ui/directives/css_truncate'; import { uiModules } from '../../../modules'; import sidebarTemplate from './sidebar.html'; import { move } from '../../../utils/collection'; +import { AggGroupNames } from './agg_groups'; +import { getEnabledMetricAggsCount } from './components/agg_group_helper'; uiModules.get('app/visualize').directive('visEditorSidebar', function () { return { @@ -76,6 +78,14 @@ uiModules.get('app/visualize').directive('visEditorSidebar', function () { } aggs.splice(index, 1); + + if (agg.schema.group === AggGroupNames.Metrics) { + const metrics = $scope.state.aggs.bySchemaGroup(AggGroupNames.Metrics); + + if (getEnabledMetricAggsCount(metrics) === 0) { + metrics.find(aggregation => aggregation.schema.name === 'metric').enabled = true; + } + } }; $scope.onToggleEnableAgg = (agg, isEnable) => { From 2b06c0227e943aea21e73f1077d0d89fb3ccd926 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 2 Oct 2019 14:20:10 +0200 Subject: [PATCH 33/53] [Graph] Reactify visualization (#46799) --- x-pack/legacy/plugins/graph/public/_main.scss | 79 +------ .../public/angular/templates/_graph.scss | 47 ----- .../graph/public/angular/templates/index.html | 66 +----- x-pack/legacy/plugins/graph/public/app.js | 35 +--- .../graph/public/components/_index.scss | 1 + .../graph_visualization.test.tsx.snap | 173 ++++++++++++++++ .../_graph_visualization.scss | 60 ++++++ .../graph_visualization/_index.scss | 1 + .../graph_visualization.test.tsx | 164 +++++++++++++++ .../graph_visualization.tsx | 194 ++++++++++++++++++ .../components/graph_visualization/index.ts | 7 + .../components/legacy_icon/_legacy_icon.scss | 19 +- .../components/legacy_icon/legacy_icon.tsx | 7 +- .../graph/public/helpers/style_choices.ts | 5 +- .../services/persistence/deserialize.test.ts | 2 +- .../graph/public/types/workspace_state.ts | 2 + 16 files changed, 640 insertions(+), 222 deletions(-) create mode 100644 x-pack/legacy/plugins/graph/public/components/graph_visualization/__snapshots__/graph_visualization.test.tsx.snap create mode 100644 x-pack/legacy/plugins/graph/public/components/graph_visualization/_graph_visualization.scss create mode 100644 x-pack/legacy/plugins/graph/public/components/graph_visualization/_index.scss create mode 100644 x-pack/legacy/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx create mode 100644 x-pack/legacy/plugins/graph/public/components/graph_visualization/graph_visualization.tsx create mode 100644 x-pack/legacy/plugins/graph/public/components/graph_visualization/index.ts diff --git a/x-pack/legacy/plugins/graph/public/_main.scss b/x-pack/legacy/plugins/graph/public/_main.scss index 7e70ccd6f35d8b..2559b7d1aba5ca 100644 --- a/x-pack/legacy/plugins/graph/public/_main.scss +++ b/x-pack/legacy/plugins/graph/public/_main.scss @@ -1,32 +1,3 @@ -/** - * Nodes - */ - -.gphNode-disabled{ - opacity:0.3; -} - -.gphNode__circle { - fill: $euiColorMediumShade; - - // SASSTODO: Can't definitively change modifier class - // because it's not easy to tell what's a class and what's - // part of the data object since they are named the same - &.selectedNode { - stroke-width: $euiSizeXS; - stroke: transparentize($euiColorPrimary, .25); - } -} - -.gphNode__text { - fill: $euiColorDarkestShade; - - &--lowOpacity{ - fill-opacity: 0.5; - } -} - - /** * Forms */ @@ -35,64 +6,16 @@ margin-bottom: $euiSizeS; } -.gphColorPicker__color, -.gphIconPicker__icon { +.gphColorPicker__color { margin: $euiSizeXS; cursor: pointer; - &.selectedNode, &:hover, &:focus { transform: scale(1.4); } } -.gphIconPicker__icon { - opacity: .7; - - &.selectedNode, - &:hover, - &:focus { - opacity: 1; - } -} - -.gphIndexSelect{ - max-width: $euiSizeL * 10; - margin-right: $euiSizeXS; - - &-unselected { - @include euiFocusRing; - } -} - -.gphAddButton { - background: $euiColorPrimary; - color: $euiColorEmptyShade; - border-radius: 50%; - font-size: $euiFontSizeXS; - margin: 2px $euiSizeS 0 $euiSizeXS; - @include size(26px); // same as svg - - &:hover:not(:disabled) { - background: shadeOrTint($euiColorPrimary, 10%, 10%); - cursor: pointer; - } - - &:disabled { - background: $euiColorMediumShade; - cursor: not-allowed; - } - - &-focus { - @include euiFocusRing; - } -} - -.gphFieldList { - min-width: $euiSizeXL * 10; -} - /** * Utilities */ diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss b/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss index 20ca704b9282ba..0f2bf90a10d4f3 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss +++ b/x-pack/legacy/plugins/graph/public/angular/templates/_graph.scss @@ -27,53 +27,6 @@ flex: 1; } -.gphGraph { - // SASSTODO: Can't definitively change child class - // because it's not easy to tell what's a class and what's - // part of the data object since they are named the same - .edge { - fill: $euiColorMediumShade; - stroke: $euiColorMediumShade; - stroke-width: 2; - stroke-opacity: 0.5; - - &:hover { - stroke-opacity: 0.95; - cursor: pointer; - } - } - - .edge.selectedEdge { - stroke: $euiColorDarkShade; - stroke-opacity: 0.95; - } - - .edge.inferredEdge { - stroke-dasharray: 5,5; - } -} - -.gphNode__label { - @include gphSvgText; - cursor: pointer; -} - -.gphNode__label--html { - @include euiTextTruncate; - text-align: center; -} - -.gphNode__markerCircle { - fill: $euiColorDarkShade; - stroke: $euiColorEmptyShade; -} - -.gphNode__markerText { - @include gphSvgText; - font-size: $euiSizeS - 2px; - fill: $euiColorEmptyShade; -} - .gphGraph__menus { margin: $euiSizeS; } diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/index.html b/x-pack/legacy/plugins/graph/public/angular/templates/index.html index 52f901f7d169ed..267e7564fb8302 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -21,65 +21,13 @@ dispatch="reduxDispatch" > -
- - - - - - - - {{n.icon.code}} - - - - - {{n.label}} - - - - -

{{n.label}}

- -
- - - - {{n.numChildren}} - - - -
-
-
- - +
+ + + + +
+ + +
+
+ +
+ + + + + + + + `; exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-controls="kbnTypeahead__items" + aria-expanded={false} + aria-haspopup="true" + aria-owns="kbnTypeahead__items" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the test" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ - + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - Lucene - - - - - -
-
- -
-
-
-
-
-
-
- -
-
-
+
+ + + +
+ + + + + + + + + + + + + + + + + + + `; exports[`QueryBarInput Should render the given query 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-controls="kbnTypeahead__items" + aria-expanded={false} + aria-haspopup="true" + aria-owns="kbnTypeahead__items" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the test" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - KQL - - - - - -
-
- -
-
-
-
-
-
-
- -
-
-
+
+ + + +
+ + + + + + + + + + + + + + + + + + + `; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx index e66d71b9b08b4b..a66fb682063ece 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx @@ -25,12 +25,14 @@ import { import { EuiFieldText } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryBarInput, QueryBarInputUI } from './query_bar_input'; import { coreMock } from '../../../../../../../core/public/mocks'; const startMock = coreMock.createStart(); import { IndexPattern } from '../../../index'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { mount } from 'enzyme'; const noop = () => { return; @@ -78,64 +80,67 @@ const mockIndexPattern = { ], } as IndexPattern; +function wrapQueryBarInputInContext(testProps: any, store?: any) { + const defaultOptions = { + screenTitle: 'Another Screen', + intl: null as any, + }; + + const services = { + appName: testProps.appName || 'test', + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + store: store || createMockStorage(), + }; + + return ( + + + + + + ); +} + describe('QueryBarInput', () => { beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + }) ); expect(component).toMatchSnapshot(); }); it('Should pass the query language to the language switcher', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: luceneQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + }) ); expect(component).toMatchSnapshot(); }); it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + }) ); expect(component).toMatchSnapshot(); @@ -144,43 +149,32 @@ describe('QueryBarInput', () => { it('Should create a unique PersistedLog based on the appName and query language', () => { mockPersistedLogFactory.mockClear(); - mountWithIntl( - + mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + appName: 'discover', + }) ); - expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); }); it("On language selection, should store the user's preference in localstorage and reset the query", () => { const mockStorage = createMockStorage(); const mockCallback = jest.fn(); - - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext( + { + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + appName: 'discover', + }, + mockStorage + ) ); component @@ -194,23 +188,16 @@ describe('QueryBarInput', () => { it('Should call onSubmit when the user hits enter inside the query bar', () => { const mockCallback = jest.fn(); - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + }) ); - const instance = component.instance() as QueryBarInputUI; + const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -220,23 +207,17 @@ describe('QueryBarInput', () => { }); it('Should use PersistedLog for recent search suggestions', async () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + persistedLog: mockPersistedLog, + }) ); - const instance = component.instance() as QueryBarInputUI; + const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -250,22 +231,15 @@ describe('QueryBarInput', () => { it('Should accept index pattern strings and fetch the full object', () => { mockFetchIndexPatterns.mockClear(); - - mountWithIntl( - + mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: ['logstash-*'], + disableAutoFocus: true, + }) ); + expect(mockFetchIndexPatterns).toHaveBeenCalledWith( startMock.savedObjects.client, ['logstash-*'], diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx index 7a972a6068f6f6..6c91da7c28135f 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -23,19 +23,17 @@ import React from 'react'; import { EuiFieldText, EuiOutsideClickDetector, PopoverAnchorPosition } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { debounce, compact, isEqual, omit } from 'lodash'; +import { debounce, compact, isEqual } from 'lodash'; import { PersistedLog } from 'ui/persisted_log'; -import { Storage } from 'ui/storage'; -import { npStart } from 'ui/new_platform'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; + import { AutocompleteSuggestion, AutocompleteSuggestionType, } from '../../../../../../../plugins/data/public'; +import { + withKibana, + KibanaReactContextValue, +} from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern, StaticIndexPattern } from '../../../index_patterns'; import { Query } from '../index'; import { fromUser, matchPairs, toUser } from '../lib'; @@ -43,21 +41,13 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { SuggestionsComponent } from './typeahead/suggestions_component'; import { getQueryLog } from '../lib/get_query_log'; import { fetchIndexPatterns } from '../lib/fetch_index_patterns'; - -// todo: related to https://github.com/elastic/kibana/pull/45762/files -// Will be refactored after merge of related PR -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); +import { IDataPluginServices } from '../../../types'; interface Props { - uiSettings: UiSettingsClientContract; - indexPatterns: Array; - savedObjectsClient: SavedObjectsClientContract; - http: HttpServiceBase; - store: Storage; + kibana: KibanaReactContextValue; intl: InjectedIntl; + indexPatterns: Array; query: Query; - appName: string; disableAutoFocus?: boolean; screenTitle?: string; prepend?: React.ReactNode; @@ -67,6 +57,7 @@ interface Props { languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onChange?: (query: Query) => void; onSubmit?: (query: Query) => void; + dataTestSubj?: string; } interface State { @@ -107,6 +98,7 @@ export class QueryBarInputUI extends Component { public inputRef: HTMLInputElement | null = null; private persistedLog: PersistedLog | undefined; + private services = this.props.kibana.services; private componentIsUnmounting = false; private getQueryString = () => { @@ -122,9 +114,9 @@ export class QueryBarInputUI extends Component { ) as IndexPattern[]; const objectPatternsFromStrings = (await fetchIndexPatterns( - this.props.savedObjectsClient, + this.services.savedObjects!.client, stringPatterns, - this.props.uiSettings + this.services.uiSettings! )) as IndexPattern[]; this.setState({ @@ -137,13 +129,13 @@ export class QueryBarInputUI extends Component { return; } - const uiSettings = this.props.uiSettings; + const uiSettings = this.services.uiSettings; const language = this.props.query.language; const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const autocompleteProvider = getAutocompleteProvider(language); + const autocompleteProvider = this.services.autocomplete.getProvider(language); if ( !autocompleteProvider || !Array.isArray(this.state.indexPatterns) || @@ -369,11 +361,11 @@ export class QueryBarInputUI extends Component { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the // UI component's change handler. - this.props.http.post('/api/kibana/kql_opt_in_telemetry', { + this.services.http.post('/api/kibana/kql_opt_in_telemetry', { body: JSON.stringify({ opt_in: language === 'kuery' }), }); - this.props.store.set('kibana.userQueryLanguage', language); + this.services.store.set('kibana.userQueryLanguage', language); const newQuery = { query: '', language }; this.onChange(newQuery); @@ -406,7 +398,7 @@ export class QueryBarInputUI extends Component { this.persistedLog = this.props.persistedLog ? this.props.persistedLog - : getQueryLog(this.props.uiSettings, this.props.appName, this.props.query.language); + : getQueryLog(this.services.uiSettings, this.services.appName, this.props.query.language); this.fetchIndexPatterns().then(this.updateSuggestions); } @@ -419,7 +411,7 @@ export class QueryBarInputUI extends Component { this.persistedLog = this.props.persistedLog ? this.props.persistedLog - : getQueryLog(this.props.uiSettings, this.props.appName, this.props.query.language); + : getQueryLog(this.services.uiSettings, this.services.appName, this.props.query.language); if (!isEqual(prevProps.indexPatterns, this.props.indexPatterns)) { this.fetchIndexPatterns().then(this.updateSuggestions); @@ -446,24 +438,6 @@ export class QueryBarInputUI extends Component { } public render() { - const rest = omit(this.props, [ - 'indexPatterns', - 'intl', - 'query', - 'appName', - 'disableAutoFocus', - 'screenTitle', - 'prepend', - 'store', - 'persistedLog', - 'bubbleSubmitEvent', - 'languageSwitcherPopoverAnchorPosition', - 'onChange', - 'onSubmit', - 'uiSettings', - 'savedObjectsClient', - ]); - return (
{ }, { previouslyTranslatedPageTitle: this.props.screenTitle, - pageType: this.props.appName, + pageType: this.services.appName, } ) : undefined } type="text" - data-test-subj="queryInput" aria-autocomplete="list" aria-controls="kbnTypeahead__items" aria-activedescendant={ @@ -529,7 +502,7 @@ export class QueryBarInputUI extends Component { onSelectLanguage={this.onSelectLanguage} /> } - {...rest} + data-test-subj={this.props.dataTestSubj || 'queryInput'} />
@@ -548,4 +521,4 @@ export class QueryBarInputUI extends Component { } } -export const QueryBarInput = injectI18n(QueryBarInputUI); +export const QueryBarInput = injectI18n(withKibana(QueryBarInputUI)); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx index 0926af7b30ef78..337bb9f4861c3e 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx @@ -21,7 +21,6 @@ import { mockPersistedLogFactory } from './query_bar_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; -import './query_bar_top_row.test.mocks'; import { QueryBarTopRow } from './query_bar_top_row'; import { IndexPattern } from '../../../index'; @@ -97,42 +96,49 @@ const mockIndexPattern = { ], } as IndexPattern; -describe('QueryBarTopRowTopRow', () => { - const QUERY_INPUT_SELECTOR = 'InjectIntl(QueryBarInputUI)'; - const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; +function wrapQueryBarTopRowInContext(testProps: any) { + const defaultOptions = { + screenTitle: 'Another Screen', + onSubmit: noop, + onChange: noop, + intl: null as any, + }; + const services = { + appName: 'discover', uiSettings: startMock.uiSettings, savedObjects: startMock.savedObjects, notifications: startMock.notifications, http: startMock.http, - }; - const defaultOptions = { - appName: 'discover', - screenTitle: 'Another Screen', - onSubmit: noop, - onChange: noop, store: createMockStorage(), - intl: null as any, }; + return ( + + + + + + ); +} + +describe('QueryBarTopRowTopRow', () => { + const QUERY_INPUT_SELECTOR = 'QueryBarInputUI'; + const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; + beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + screenTitle: 'Another Screen', + isDirty: false, + indexPatterns: [mockIndexPattern], + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); @@ -141,19 +147,14 @@ describe('QueryBarTopRowTopRow', () => { it('Should create a unique PersistedLog based on the appName and query language', () => { mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + screenTitle: 'Another Screen', + indexPatterns: [mockIndexPattern], + timeHistory: timefilterSetupMock.history, + disableAutoFocus: true, + isDirty: false, + }) ); expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); @@ -161,15 +162,10 @@ describe('QueryBarTopRowTopRow', () => { it('Should render only timepicker when no options provided', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -178,16 +174,11 @@ describe('QueryBarTopRowTopRow', () => { it('Should not show timepicker when asked', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + isDirty: false, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -196,19 +187,14 @@ describe('QueryBarTopRowTopRow', () => { it('Should render timepicker with options', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: true, + dateRangeFrom: 'now-7d', + dateRangeTo: 'now', + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -217,19 +203,16 @@ describe('QueryBarTopRowTopRow', () => { it('Should render only query input bar', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + indexPatterns: [mockIndexPattern], + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: false, + dateRangeFrom: 'now-7d', + dateRangeTo: 'now', + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); @@ -238,20 +221,15 @@ describe('QueryBarTopRowTopRow', () => { it('Should NOT render query input bar if disabled', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + isDirty: false, + screenTitle: 'Another Screen', + indexPatterns: [mockIndexPattern], + showQueryInput: false, + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -260,17 +238,12 @@ describe('QueryBarTopRowTopRow', () => { it('Should NOT render query input bar if missing options', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index c25b596973174d..6895c9ecd018cb 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -21,7 +21,6 @@ import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query'; import classNames from 'classnames'; import React, { useState, useEffect } from 'react'; -import { Storage } from 'ui/storage'; import { documentationLinks } from 'ui/documentation_links'; import { PersistedLog } from 'ui/persisted_log'; @@ -30,6 +29,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSuperDatePicker } fro import { EuiSuperUpdateButton } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; +import { TimeRange } from 'src/plugins/data/public'; import { useKibana } from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern } from '../../../index_patterns'; @@ -37,21 +37,15 @@ import { QueryBarInput } from './query_bar_input'; import { getQueryLog } from '../lib/get_query_log'; import { Query } from '../index'; import { TimeHistoryContract } from '../../../timefilter'; - -interface DateRange { - from: string; - to: string; -} +import { IDataPluginServices } from '../../../types'; interface Props { query?: Query; - onSubmit: (payload: { dateRange: DateRange; query?: Query }) => void; - onChange: (payload: { dateRange: DateRange; query?: Query }) => void; + onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; disableAutoFocus?: boolean; - appName: string; screenTitle?: string; indexPatterns?: Array; - store?: Storage; intl: InjectedIntl; prepend?: React.ReactNode; showQueryInput?: boolean; @@ -70,15 +64,15 @@ interface Props { function QueryBarTopRowUI(props: Props) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); - const kibana = useKibana(); - const { uiSettings, http, notifications, savedObjects } = kibana.services; + const kibana = useKibana(); + const { uiSettings, notifications, store, appName } = kibana.services; const queryLanguage = props.query && props.query.language; let persistedLog: PersistedLog | undefined; useEffect(() => { if (!props.query) return; - persistedLog = getQueryLog(uiSettings!, props.appName, props.query.language); + persistedLog = getQueryLog(uiSettings!, appName, props.query.language); }, [queryLanguage]); function onClickSubmitButton(event: React.MouseEvent) { @@ -131,7 +125,7 @@ function QueryBarTopRowUI(props: Props) { } } - function onSubmit({ query, dateRange }: { query?: Query; dateRange: DateRange }) { + function onSubmit({ query, dateRange }: { query?: Query; dateRange: TimeRange }) { handleLuceneSyntaxWarning(); if (props.timeHistory) { @@ -153,19 +147,14 @@ function QueryBarTopRowUI(props: Props) { return ( ); @@ -176,7 +165,7 @@ function QueryBarTopRowUI(props: Props) { } function shouldRenderQueryInput(): boolean { - return Boolean(props.showQueryInput && props.indexPatterns && props.query && props.store); + return Boolean(props.showQueryInput && props.indexPatterns && props.query && store); } function renderUpdateButton() { @@ -251,7 +240,7 @@ function QueryBarTopRowUI(props: Props) { function handleLuceneSyntaxWarning() { if (!props.query) return; - const { intl, store } = props; + const { intl } = props; const { query, language } = props.query; if ( language === 'kuery' && @@ -300,8 +289,8 @@ function QueryBarTopRowUI(props: Props) { } function onLuceneSyntaxWarningOptOut(toast: Toast) { - if (!props.store) return; - props.store.set('kibana.luceneSyntaxWarningOptOut', true); + if (!store) return; + store.set('kibana.luceneSyntaxWarningOptOut', true); notifications!.toasts.remove(toast); } diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx new file mode 100644 index 00000000000000..add49e47971d34 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Filter } from '@kbn/es-query'; +import { CoreStart } from 'src/core/public'; +import { Storage } from 'ui/storage'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TimefilterSetup } from '../../../timefilter'; +import { FilterManager, SearchBar } from '../../../'; +import { SearchBarOwnProps } from '.'; + +interface StatefulSearchBarDeps { + core: CoreStart; + store: Storage; + timefilter: TimefilterSetup; + filterManager: FilterManager; + autocomplete: AutocompletePublicPluginStart; +} + +export type StatetfulSearchBarProps = SearchBarOwnProps & { + appName: string; +}; + +const defaultFiltersUpdated = (filterManager: FilterManager) => { + return (filters: Filter[]) => { + filterManager.setFilters(filters); + }; +}; + +const defaultOnRefreshChange = (timefilter: TimefilterSetup) => { + return (options: { isPaused: boolean; refreshInterval: number }) => { + timefilter.timefilter.setRefreshInterval({ + value: options.refreshInterval, + pause: options.isPaused, + }); + }; +}; + +export function createSearchBar({ + core, + store, + timefilter, + filterManager, + autocomplete, +}: StatefulSearchBarDeps) { + // App name should come from the core application service. + // Until it's available, we'll ask the user to provide it for the pre-wired component. + return (props: StatetfulSearchBarProps) => { + const timeRange = timefilter.timefilter.getTime(); + const refreshInterval = timefilter.timefilter.getRefreshInterval(); + + return ( + + + + ); + }; +} diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx index 24ffa939a746e9..accaac163acfcf 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx @@ -18,3 +18,4 @@ */ export * from './search_bar'; +export * from './create_search_bar'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 7d48977fab8a53..73e81a38572c39 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -18,14 +18,17 @@ */ import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { SearchBar } from './search_bar'; import { IndexPattern } from '../../../index_patterns'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; + import { coreMock } from '../../../../../../../../src/core/public/mocks'; const startMock = coreMock.createStart(); import { timefilterServiceMock } from '../../../timefilter/timefilter_service.mock'; +import { mount } from 'enzyme'; const timefilterSetupMock = timefilterServiceMock.createSetupContract(); jest.mock('../../../../../data/public', () => { @@ -41,13 +44,6 @@ jest.mock('../../../query/query_bar', () => { }; }); -jest.mock('ui/notify', () => ({ - toastNotifications: { - addSuccess: () => {}, - addDanger: () => {}, - }, -})); - const noop = jest.fn(); const createMockWebStorage = () => ({ @@ -87,26 +83,44 @@ const kqlQuery = { language: 'kuery', }; -describe('SearchBar', () => { - const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.filterBar'; - const QUERY_BAR = '.queryBar'; - - const options = { +function wrapSearchBarInContext(testProps: any) { + const defaultOptions = { appName: 'test', - savedObjects: startMock.savedObjects, - notifications: startMock.notifications, timeHistory: timefilterSetupMock.history, intl: null as any, }; + const services = { + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + store: createMockStorage(), + }; + + return ( + + + + + + ); +} + +describe('SearchBar', () => { + const SEARCH_BAR_ROOT = '.globalQueryBar'; + const FILTER_BAR = '.filterBar'; + const QUERY_BAR = '.queryBar'; + beforeEach(() => { jest.clearAllMocks(); }); it('Should render query bar when no options provided (in reality - timepicker)', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -115,12 +129,11 @@ describe('SearchBar', () => { }); it('Should render empty when timepicker is off and no options provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showDatePicker: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -129,14 +142,13 @@ describe('SearchBar', () => { }); it('Should render filter bar, when required fields are provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showDatePicker: false, + onFiltersUpdated: noop, + filters: [], + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -145,15 +157,14 @@ describe('SearchBar', () => { }); it('Should NOT render filter bar, if disabled', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showFilterBar: false, + filters: [], + onFiltersUpdated: noop, + showDatePicker: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -162,15 +173,13 @@ describe('SearchBar', () => { }); it('Should render query bar, when required fields are provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -179,16 +188,14 @@ describe('SearchBar', () => { }); it('Should NOT render query bar, if disabled', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + showQueryBar: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -197,17 +204,15 @@ describe('SearchBar', () => { }); it('Should render query bar and filter bar', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + filters: [], + onFiltersUpdated: noop, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index 6c73fa3614cc3c..ed2a6638aba114 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -22,10 +22,9 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -import { Storage } from 'ui/storage'; import { get, isEqual } from 'lodash'; -import { CoreStart } from 'src/core/public'; +import { TimeRange } from 'src/plugins/data/common/types'; import { IndexPattern, Query, FilterBar } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; @@ -34,54 +33,52 @@ import { SavedQueryManagementComponent } from './saved_query_management/saved_qu import { SavedQueryService } from '../lib/saved_query_service'; import { createSavedQueryService } from '../lib/saved_query_service'; import { TimeHistoryContract } from '../../../timefilter'; - -interface DateRange { - from: string; - to: string; -} - -/** - * NgReact lib requires that changes to the props need to be made in the directive config as well - * See [search_bar\directive\index.js] file - */ -export interface SearchBarProps { - appName: string; +import { + withKibana, + KibanaReactContextValue, +} from '../../../../../../../plugins/kibana_react/public'; +import { IDataPluginServices } from '../../../types'; + +interface SearchBarInjectedDeps { + kibana: KibanaReactContextValue; intl: InjectedIntl; - indexPatterns?: IndexPattern[]; - - // Query bar - showQueryBar?: boolean; - showQueryInput?: boolean; - screenTitle?: string; - store?: Storage; - query?: Query; - savedQuery?: SavedQuery; - onQuerySubmit?: (payload: { dateRange: DateRange; query?: Query }) => void; timeHistory: TimeHistoryContract; // Filter bar - showFilterBar?: boolean; - filters?: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; + filters?: Filter[]; // Date picker - showDatePicker?: boolean; dateRangeFrom?: string; dateRangeTo?: string; // Autorefresh + onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; isRefreshPaused?: boolean; refreshInterval?: number; +} + +export interface SearchBarOwnProps { + indexPatterns?: IndexPattern[]; + customSubmitButton?: React.ReactNode; + screenTitle?: string; + + // Togglers + showQueryBar?: boolean; + showQueryInput?: boolean; + showFilterBar?: boolean; + showDatePicker?: boolean; showAutoRefreshOnly?: boolean; showSaveQuery?: boolean; - onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; + + // Query bar - should be in SearchBarInjectedDeps + query?: Query; + savedQuery?: SavedQuery; + onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; onSaved?: (savedQuery: SavedQuery) => void; onSavedQueryUpdated?: (savedQuery: SavedQuery) => void; onClearSavedQuery?: () => void; - customSubmitButton?: React.ReactNode; - - // TODO: deprecate - savedObjects: CoreStart['savedObjects']; - notifications: CoreStart['notifications']; } +export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; + interface State { isFiltersVisible: boolean; showSaveQueryModal: boolean; @@ -102,7 +99,7 @@ class SearchBarUI extends Component { }; private savedQueryService!: SavedQueryService; - + private services = this.props.kibana.services; public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -253,7 +250,7 @@ class SearchBarUI extends Component { response = await this.savedQueryService.saveQuery(savedQueryAttributes); } - this.props.notifications.toasts.addSuccess( + this.services.notifications.toasts.addSuccess( `Your query "${response.attributes.title}" was saved` ); @@ -266,7 +263,7 @@ class SearchBarUI extends Component { this.props.onSaved(response); } } catch (error) { - this.props.notifications.toasts.addDanger( + this.services.notifications.toasts.addDanger( `An error occured while saving your query: ${error.message}` ); throw error; @@ -285,7 +282,7 @@ class SearchBarUI extends Component { }); }; - public onQueryBarChange = (queryAndDateRange: { dateRange: DateRange; query?: Query }) => { + public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: Query }) => { this.setState({ query: queryAndDateRange.query, dateRangeFrom: queryAndDateRange.dateRange.from, @@ -293,7 +290,7 @@ class SearchBarUI extends Component { }); }; - public onQueryBarSubmit = (queryAndDateRange: { dateRange?: DateRange; query?: Query }) => { + public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { this.setState( { query: queryAndDateRange.query, @@ -337,8 +334,8 @@ class SearchBarUI extends Component { this.setFilterBarHeight(); this.ro.observe(this.filterBarRef); } - if (this.props.savedObjects) { - this.savedQueryService = createSavedQueryService(this.props.savedObjects!.client); + if (this.services.savedObjects) { + this.savedQueryService = createSavedQueryService(this.services.savedObjects.client); } } @@ -370,9 +367,7 @@ class SearchBarUI extends Component { query={this.state.query} screenTitle={this.props.screenTitle} onSubmit={this.onQueryBarSubmit} - appName={this.props.appName} indexPatterns={this.props.indexPatterns} - store={this.props.store} prepend={this.props.showFilterBar ? savedQueryManagement : undefined} showDatePicker={this.props.showDatePicker} dateRangeFrom={this.state.dateRangeFrom} @@ -449,4 +444,4 @@ class SearchBarUI extends Component { } } -export const SearchBar = injectI18n(SearchBarUI); +export const SearchBar = injectI18n(withKibana(SearchBarUI)); diff --git a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts index 4289d56b33c605..126754388f13f2 100644 --- a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts +++ b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts @@ -18,7 +18,8 @@ */ import chrome from 'ui/chrome'; -import { CoreStart, Plugin } from '../../../../../../src/core/public'; +import { Storage } from 'ui/storage'; +import { Plugin } from '../../../../../../src/core/public'; import { initLegacyModule } from './legacy_module'; /** @internal */ @@ -26,6 +27,10 @@ export interface LegacyDependenciesPluginSetup { savedObjectsClient: any; } +export interface LegacyDependenciesPluginStart { + storage: Storage; +} + export class LegacyDependenciesPlugin implements Plugin { public setup() { initLegacyModule(); @@ -35,7 +40,9 @@ export class LegacyDependenciesPlugin implements Plugin { } as LegacyDependenciesPluginSetup; } - public start(core: CoreStart) { - // nothing to do here yet + public start() { + return { + storage: new Storage(window.localStorage), + } as LegacyDependenciesPluginStart; } } diff --git a/src/legacy/core_plugins/data/public/timefilter/get_time.ts b/src/legacy/core_plugins/data/public/timefilter/get_time.ts index e54725dd9ba486..18a43d789714d7 100644 --- a/src/legacy/core_plugins/data/public/timefilter/get_time.ts +++ b/src/legacy/core_plugins/data/public/timefilter/get_time.ts @@ -18,8 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { Field, IndexPattern } from 'ui/index_patterns'; import { TimeRange } from 'src/plugins/data/public'; +import { IndexPattern, Field } from '../index_patterns'; interface CalculateBoundsOptions { forceNow?: Date; diff --git a/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts deleted file mode 100644 index 7354916c3fc359..00000000000000 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { chromeServiceMock } from '../../../../../core/public/mocks'; - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); diff --git a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts index c08ea9043da927..64129ea2af5ffb 100644 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts @@ -25,7 +25,7 @@ import { IndexPattern, TimeHistoryContract } from '../index'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; import { parseQueryString } from './lib/parse_querystring'; import { calculateBounds, getTime } from './get_time'; -import { TimefilterConfig, InputTimeRange } from './types'; +import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; export class Timefilter { // Fired when isTimeRangeSelectorEnabled \ isAutoRefreshSelectorEnabled are toggled @@ -148,19 +148,19 @@ export class Timefilter { return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); }; - public getBounds = () => { + public getBounds(): TimeRangeBounds { return this.calculateBounds(this._time); - }; + } - public calculateBounds = (timeRange: TimeRange) => { + public calculateBounds(timeRange: TimeRange): TimeRangeBounds { return calculateBounds(timeRange, { forceNow: this.getForceNow() }); - }; + } - public getActiveBounds = () => { + public getActiveBounds(): TimeRangeBounds | undefined { if (this.isTimeRangeSelectorEnabled) { return this.getBounds(); } - }; + } /** * Show the time bounds selector part of the time filter diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx b/src/legacy/core_plugins/data/public/types.ts similarity index 63% rename from src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx rename to src/legacy/core_plugins/data/public/types.ts index 585fad0e058b73..4b7a5c1402ea72 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx +++ b/src/legacy/core_plugins/data/public/types.ts @@ -17,16 +17,15 @@ * under the License. */ -import { - fatalErrorsServiceMock, - notificationServiceMock, -} from '../../../../../../../core/public/mocks'; +import { UiSettingsClientContract, CoreStart } from 'src/core/public'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - notifications: notificationServiceMock.createSetupContract(), - }, - }, -})); +export interface IDataPluginServices extends Partial { + appName: string; + uiSettings: UiSettingsClientContract; + savedObjects: CoreStart['savedObjects']; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + store: Storage; + autocomplete: AutocompletePublicPluginStart; +} diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx index 39bf299cd8d125..21c5cef4ae9254 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,12 +22,20 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { coreMock } from '../../../../../core/public/mocks'; -const startMock = coreMock.createStart(); - import { timefilterServiceMock } from '../../../../core_plugins/data/public/timefilter/timefilter_service.mock'; const timefilterSetupMock = timefilterServiceMock.createSetupContract(); +jest.mock('ui/new_platform'); + +jest.mock('../../../../../../src/legacy/core_plugins/data/public/legacy', () => ({ + start: { + ui: { + SearchBar: () => {}, + }, + }, + setup: {}, +})); + jest.mock('../../../../core_plugins/data/public', () => { return { SearchBar: () =>
, @@ -57,34 +65,26 @@ describe('TopNavMenu', () => { ]; it('Should render nothing when no config is provided', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0); }); it('Should render 1 menu item', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(1); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0); }); it('Should render multiple menu items', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(menuItems.length); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0); }); it('Should render search bar', () => { const component = shallowWithIntl( - + ); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx index c99c71f97e1afe..aec91c2aa6bc64 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx @@ -21,27 +21,16 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; -import { UiSettingsClientContract, CoreStart } from 'src/core/public'; + import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; -import { - SearchBar, - SearchBarProps, - TimeHistoryContract, -} from '../../../../core_plugins/data/public'; +import { SearchBarProps } from '../../../../core_plugins/data/public'; +import { start as data } from '../../../data/public/legacy'; type Props = Partial & { - name: string; + appName: string; config?: TopNavMenuData[]; showSearchBar?: boolean; - - // Search Bar dependencies - uiSettings?: UiSettingsClientContract; - savedObjects?: CoreStart['savedObjects']; - notifications?: CoreStart['notifications']; - timeHistory?: TimeHistoryContract; - http?: CoreStart['http']; }; /* @@ -54,9 +43,11 @@ type Props = Partial & { **/ export function TopNavMenu(props: Props) { + const { SearchBar } = data.ui; + const { config, showSearchBar, ...searchBarProps } = props; function renderItems() { - if (!props.config) return; - return props.config.map((menuItem: TopNavMenuData, i: number) => { + if (!config) return; + return config.map((menuItem: TopNavMenuData, i: number) => { return ( @@ -67,53 +58,8 @@ export function TopNavMenu(props: Props) { function renderSearchBar() { // Validate presense of all required fields - if ( - !props.showSearchBar || - !props.savedObjects || - !props.http || - !props.notifications || - !props.timeHistory - ) - return; - return ( - - - - ); + if (!showSearchBar) return; + return ; } function renderLayout() { diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx index 9d2f68ddb74465..5bcb2961c42de2 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis_controller.test.tsx @@ -22,7 +22,7 @@ import { render, mount } from 'enzyme'; import { MarkdownVisWrapper } from './markdown_vis_controller'; // We need Markdown to do these tests, so mock data plugin -jest.mock('../../data/public', () => { +jest.mock('../../data/public/legacy', () => { return {}; }); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index a6c52d8760666f..389a84babae87b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -34,6 +34,7 @@ import { extractIndexPatterns } from '../../common/extract_index_patterns'; import { npStart } from 'ui/new_platform'; import { Storage } from 'ui/storage'; import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; +import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; const localStorage = new Storage(window.localStorage); import { timefilter } from 'ui/timefilter'; @@ -163,38 +164,48 @@ export class VisEditor extends Component { const { model } = this.state; if (model) { + //TODO: Remove CoreStartContextProvider, KibanaContextProvider should be raised to the top of the plugin. return ( -
-
- + +
+
+ +
+ +
+ + + +
- -
- - - -
-
+ ); } diff --git a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js index f2faeee75810ee..79365eb5cf1cc5 100644 --- a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js @@ -21,9 +21,6 @@ import 'ngreact'; import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; import { TopNavMenu } from '../../../core_plugins/kibana_react/public'; -import { Storage } from 'ui/storage'; -import { npStart } from 'ui/new_platform'; -import { start as data } from '../../../core_plugins/data/public/legacy'; const module = uiModules.get('kibana'); @@ -43,25 +40,10 @@ module.directive('kbnTopNav', () => { // of the config array's disableButton function return value changes. child.setAttribute('disabled-buttons', 'disabledButtons'); - // Pass in storage - const localStorage = new Storage(window.localStorage); - child.setAttribute('http', 'http'); - child.setAttribute('store', 'store'); - child.setAttribute('time-history', 'timeHistory'); - child.setAttribute('ui-settings', 'uiSettings'); - child.setAttribute('saved-objects', 'savedObjects'); - child.setAttribute('notifications', 'notifications'); - // Append helper directive elem.append(child); const linkFn = ($scope, _, $attr) => { - $scope.store = localStorage; - $scope.http = npStart.core.http; - $scope.uiSettings = npStart.core.uiSettings; - $scope.savedObjects = npStart.core.savedObjects; - $scope.notifications = npStart.core.notifications; - $scope.timeHistory = data.timefilter.history; // Watch config changes $scope.$watch(() => { @@ -95,20 +77,12 @@ module.directive('kbnTopNavHelper', (reactDirective) => { return reactDirective( wrapInI18nContext(TopNavMenu), [ - ['name', { watchDepth: 'reference' }], ['config', { watchDepth: 'value' }], ['disabledButtons', { watchDepth: 'reference' }], ['query', { watchDepth: 'reference' }], ['savedQuery', { watchDepth: 'reference' }], - ['store', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ['savedObjects', { watchDepth: 'reference' }], - ['notifications', { watchDepth: 'reference' }], ['intl', { watchDepth: 'reference' }], - ['timeHistory', { watchDepth: 'reference' }], - ['store', { watchDepth: 'reference' }], - ['http', { watchDepth: 'reference' }], ['onQuerySubmit', { watchDepth: 'reference' }], ['onFiltersUpdated', { watchDepth: 'reference' }], diff --git a/src/legacy/ui/public/vis/editors/default/controls/filter.tsx b/src/legacy/ui/public/vis/editors/default/controls/filter.tsx index 779d5acf5b411e..cceaf86b5d85ca 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/filter.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/filter.tsx @@ -24,6 +24,7 @@ import { Query, QueryBarInput } from 'plugins/data'; import { AggConfig } from '../../..'; import { npStart } from '../../../../new_platform'; import { Storage } from '../../../../storage'; +import { KibanaContextProvider } from '../../../../../../../plugins/kibana_react/public'; const localStorage = new Storage(window.localStorage); interface FilterRowProps { @@ -82,6 +83,7 @@ function FilterRow({
); + // TODO: KibanaContextProvider should be raised to the top of the vis plugin return ( - onChangeValue(id, query, customLabel)} - disableAutoFocus={!autoFocus} - data-test-subj={dataTestSubj} - bubbleSubmitEvent={true} - languageSwitcherPopoverAnchorPosition="leftDown" - store={localStorage} - uiSettings={npStart.core.uiSettings} - http={npStart.core.http} - savedObjectsClient={npStart.core.savedObjects.client} - /> + + onChangeValue(id, query, customLabel)} + disableAutoFocus={!autoFocus} + dataTestSubj={dataTestSubj} + bubbleSubmitEvent={true} + languageSwitcherPopoverAnchorPosition="leftDown" + /> + {showCustomLabel ? ( ; +export type AutocompletePublicPluginStart = Pick; /** @public **/ export type AutocompleteProvider = (args: { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 110ca3e68a97e1..91b94e09607ee0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -25,6 +25,7 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { DataPublicPlugin as Plugin }; +export { DataPublicPluginSetup, DataPublicPluginStart } from './types'; export * from '../common'; export * from './autocomplete_provider'; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index d1670ccb645dba..eb316477673605 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -19,14 +19,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { AutocompleteProviderRegister } from './autocomplete_provider'; - -export interface DataPublicPluginSetup { - autocomplete: Pick; -} - -export interface DataPublicPluginStart { - autocomplete: Pick; -} +import { DataPublicPluginSetup, DataPublicPluginStart } from './types'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index ea160d34dc1548..23308304b8ff8e 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -18,3 +18,12 @@ */ export * from './autocomplete_provider/types'; + +import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.'; +export interface DataPublicPluginSetup { + autocomplete: AutocompletePublicPluginSetup; +} + +export interface DataPublicPluginStart { + autocomplete: AutocompletePublicPluginStart; +} diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/index.html b/x-pack/legacy/plugins/graph/public/angular/templates/index.html index 267e7564fb8302..3ed9b390c6a787 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -11,14 +11,13 @@ current-index-pattern="selectedIndex" on-index-pattern-selected="uiSelectIndex" on-query-submit="submit" - saved-objects="pluginDependencies.savedObjects" - ui-settings="pluginDependencies.uiSettings" - http="pluginDependencies.http" - overlays="pluginDependencies.overlays" is-loading="loading" initial-query="initialQuery" state="reduxState" dispatch="reduxDispatch" + autocomplete-start="autocompleteStart" + core-start="coreStart" + store="store" >
diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 32fd24b8bed6d8..aa8f0be6231dfc 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -32,6 +32,7 @@ import { npStart } from 'ui/new_platform'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { capabilities } from 'ui/capabilities'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +import { Storage } from 'ui/storage'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; @@ -114,11 +115,10 @@ app.directive('graphApp', function (reactDirective) { ['isLoading', { watchDepth: 'reference' }], ['onIndexPatternSelected', { watchDepth: 'reference' }], ['onQuerySubmit', { watchDepth: 'reference' }], - ['savedObjects', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ['http', { watchDepth: 'reference' }], ['initialQuery', { watchDepth: 'reference' }], - ['overlays', { watchDepth: 'reference' }] + ['autocompleteStart', { watchDepth: 'reference' }], + ['coreStart', { watchDepth: 'reference' }], + ['store', { watchDepth: 'reference' }] ]); }); @@ -302,8 +302,10 @@ app.controller('graphuiPlugin', function ( } }; - $scope.pluginDependencies = npStart.core; + $scope.store = new Storage(window.localStorage); + $scope.coreStart = npStart.core; + $scope.autocompleteStart = npStart.plugins.data.autocomplete; $scope.loading = false; const updateScope = () => { diff --git a/x-pack/legacy/plugins/graph/public/components/app.tsx b/x-pack/legacy/plugins/graph/public/components/app.tsx index 7e75a13bb39e3b..907e7e4cecdcd5 100644 --- a/x-pack/legacy/plugins/graph/public/components/app.tsx +++ b/x-pack/legacy/plugins/graph/public/components/app.tsx @@ -6,22 +6,40 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; +import { Storage } from 'ui/storage'; +import { CoreStart } from 'kibana/public'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; import { FieldManagerProps, FieldManager } from './field_manager'; import { SearchBarProps, SearchBar } from './search_bar'; -export interface GraphAppProps extends FieldManagerProps, SearchBarProps {} +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; + +export interface GraphAppProps extends FieldManagerProps, SearchBarProps { + coreStart: CoreStart; + autocompleteStart: AutocompletePublicPluginStart; + store: Storage; +} export function GraphApp(props: GraphAppProps) { return ( -
- - - - - - - - -
+ +
+ + + + + + + + +
+
); } diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx index eb97d63a333951..80b1c3c3439427 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.test.tsx @@ -4,32 +4,54 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchBar } from './search_bar'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { SearchBar, SearchBarProps } from './search_bar'; import React, { ReactElement } from 'react'; import { CoreStart } from 'src/core/public'; import { act } from 'react-dom/test-utils'; import { IndexPattern, QueryBarInput } from 'src/legacy/core_plugins/data/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; + jest.mock('ui/new_platform'); + import { openSourceModal } from '../services/source_modal'; +import { mount } from 'enzyme'; jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); +function wrapSearchBarInContext(testProps: SearchBarProps) { + const services = { + uiSettings: { + get: (key: string) => { + return 10; + }, + } as CoreStart['uiSettings'], + savedObjects: {} as CoreStart['savedObjects'], + notifications: {} as CoreStart['notifications'], + http: {} as CoreStart['http'], + overlays: {} as CoreStart['overlays'], + }; + + return ( + + + + + + ); +} + describe('search_bar', () => { it('should render search bar and submit queryies', () => { const querySubmit = jest.fn(); - const instance = shallowWithIntl( - {}} - onQuerySubmit={querySubmit} - savedObjects={{} as CoreStart['savedObjects']} - uiSettings={{} as CoreStart['uiSettings']} - http={{} as CoreStart['http']} - overlays={{} as CoreStart['overlays']} - currentIndexPattern={{ title: 'Testpattern' } as IndexPattern} - /> + const instance = mount( + wrapSearchBarInContext({ + isLoading: false, + onIndexPatternSelected: () => {}, + onQuerySubmit: querySubmit, + currentIndexPattern: { title: 'Testpattern' } as IndexPattern, + }) ); act(() => { instance.find(QueryBarInput).prop('onChange')!({ language: 'lucene', query: 'testQuery' }); @@ -44,17 +66,13 @@ describe('search_bar', () => { it('should translate kql query into JSON dsl', () => { const querySubmit = jest.fn(); - const instance = shallowWithIntl( - {}} - onQuerySubmit={querySubmit} - savedObjects={{} as CoreStart['savedObjects']} - uiSettings={{} as CoreStart['uiSettings']} - http={{} as CoreStart['http']} - overlays={{} as CoreStart['overlays']} - currentIndexPattern={{ title: 'Testpattern', fields: [{ name: 'test' }] } as IndexPattern} - /> + const instance = mount( + wrapSearchBarInContext({ + isLoading: false, + onIndexPatternSelected: () => {}, + onQuerySubmit: querySubmit, + currentIndexPattern: { title: 'Testpattern', fields: [{ name: 'test' }] } as IndexPattern, + }) ); act(() => { instance.find(QueryBarInput).prop('onChange')!({ language: 'kuery', query: 'test: abc' }); @@ -72,17 +90,14 @@ describe('search_bar', () => { it('should open index pattern picker', () => { const indexPatternSelected = jest.fn(); - const instance = shallowWithIntl( - {}} - savedObjects={{} as CoreStart['savedObjects']} - uiSettings={{} as CoreStart['uiSettings']} - http={{} as CoreStart['http']} - overlays={{} as CoreStart['overlays']} - currentIndexPattern={{ title: 'Testpattern' } as IndexPattern} - /> + + const instance = mount( + wrapSearchBarInContext({ + isLoading: false, + onIndexPatternSelected: indexPatternSelected, + onQuerySubmit: () => {}, + currentIndexPattern: { title: 'Testpattern' } as IndexPattern, + }) ); // pick the button component out of the tree because diff --git a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx index 358d7d23d9ed4c..226f6f829d8a44 100644 --- a/x-pack/legacy/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/legacy/plugins/graph/public/components/search_bar.tsx @@ -7,11 +7,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import React, { useState } from 'react'; -import { Storage } from 'ui/storage'; -import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { IDataPluginServices } from 'src/legacy/core_plugins/data/public/types'; import { QueryBarInput, Query, @@ -19,8 +18,7 @@ import { } from '../../../../../../src/legacy/core_plugins/data/public'; import { IndexPatternSavedObject } from '../types/app_state'; import { openSourceModal } from '../services/source_modal'; - -const localStorage = new Storage(window.localStorage); +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; export interface SearchBarProps { isLoading: boolean; @@ -28,10 +26,6 @@ export interface SearchBarProps { initialQuery?: string; onIndexPatternSelected: (indexPattern: IndexPatternSavedObject) => void; onQuerySubmit: (query: string) => void; - savedObjects: CoreStart['savedObjects']; - uiSettings: CoreStart['uiSettings']; - http: CoreStart['http']; - overlays: CoreStart['overlays']; } function queryToString(query: Query, indexPattern: IndexPattern) { @@ -56,12 +50,13 @@ export function SearchBar(props: SearchBarProps) { onQuerySubmit, isLoading, onIndexPatternSelected, - uiSettings, - savedObjects, - http, initialQuery, } = props; const [query, setQuery] = useState({ language: 'kuery', query: initialQuery || '' }); + const kibana = useKibana(); + const { overlays, uiSettings, savedObjects } = kibana.services; + if (!overlays) return null; + return (
{ - openSourceModal(props, onIndexPatternSelected); + openSourceModal( + { + overlays, + savedObjects, + uiSettings, + }, + onIndexPatternSelected + ); }} > {currentIndexPattern diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 1e5d9eb39c578f..a8df5eafe71ffb 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -11,9 +11,11 @@ import { Storage } from 'ui/storage'; import { Document, SavedObjectStore } from '../persistence'; import { mount } from 'enzyme'; import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { coreMock } from 'src/core/public/mocks'; +const dataStartMock = dataPluginMock.createStartContract(); + jest.mock('../../../../../../src/legacy/core_plugins/data/public/query/query_bar', () => ({ QueryBarTopRow: jest.fn(() => null), })); @@ -37,16 +39,17 @@ describe('Lens App', () => { function makeDefaultArgs(): jest.Mocked<{ editorFrame: EditorFrameInstance; + data: typeof dataStartMock; core: typeof core; store: Storage; docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; - savedObjectsClient: SavedObjectsClientContract; }> { return ({ editorFrame: createMockFrame(), core, + data: dataStartMock, store: { get: jest.fn(), }, @@ -59,12 +62,12 @@ describe('Lens App', () => { savedObjectsClient: jest.fn(), } as unknown) as jest.Mocked<{ editorFrame: EditorFrameInstance; + data: typeof dataStartMock; core: typeof core; store: Storage; docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; - savedObjectsClient: SavedObjectsClientContract; }>; } diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 3e157fc394d309..9c484e19789e95 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Storage } from 'ui/storage'; import { CoreStart } from 'src/core/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { Query } from '../../../../../../src/legacy/core_plugins/data/public'; import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -51,6 +52,7 @@ function isLocalStateDirty( export function App({ editorFrame, + data, core, store, docId, @@ -58,6 +60,7 @@ export function App({ redirectTo, }: { editorFrame: EditorFrameInstance; + data: DataPublicPluginStart; core: CoreStart; store: Storage; docId?: string; @@ -156,10 +159,10 @@ export function App({
@@ -224,9 +227,7 @@ export function App({ setState({ ...state, localQueryBarState }); }} isDirty={isLocalStateDirty(state.localQueryBarState, state.query, state.dateRange)} - appName={'lens'} indexPatterns={state.indexPatternTitles} - store={store} showDatePicker={true} showQueryInput={true} query={state.localQueryBarState.query} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 9504e0b6e17522..5e81785132616d 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -11,6 +11,7 @@ import chrome from 'ui/chrome'; import { Storage } from 'ui/storage'; import { CoreSetup, CoreStart } from 'src/core/public'; import { npSetup, npStart } from 'ui/new_platform'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; import { SavedObjectIndexStore } from '../persistence'; @@ -23,6 +24,9 @@ import { import { App } from './app'; import { EditorFrameInstance } from '../types'; +export interface LensPluginStartDependencies { + data: DataPublicPluginStart; +} export class AppPlugin { private instance: EditorFrameInstance | null = null; private store: SavedObjectIndexStore | null = null; @@ -45,7 +49,7 @@ export class AppPlugin { editorFrameSetupInterface.registerVisualization(metricVisualization); } - start(core: CoreStart) { + start(core: CoreStart, { data }: LensPluginStartDependencies) { if (this.store === null) { throw new Error('Start lifecycle called before setup lifecycle'); } @@ -60,6 +64,7 @@ export class AppPlugin { return ( app.setup(npSetup.core); -export const appStart = () => app.start(npStart.core); +export const appStart = () => app.start(npStart.core, { data: npStart.plugins.data }); export const appStop = () => app.stop(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx index 32da61a95beb8c..63c6398e93997d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx @@ -80,16 +80,7 @@ export const filterRatioOperation: OperationDefinition { + paramEditor: ({ state, setState, currentColumn, layerId }) => { const [hasDenominator, setDenominator] = useState( !isEqual(currentColumn.params.denominator, initialQuery) ); @@ -102,14 +93,8 @@ export const filterRatioOperation: OperationDefinition { setState( updateColumnParam({ @@ -168,14 +153,8 @@ export const filterRatioOperation: OperationDefinition {hasDenominator ? ( { setState( updateColumnParam({ diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index 49439fa9d64e62..4c9ef61478ab48 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -1,103 +1,119 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LayerPanel is rendered 1`] = ` - - - - - - - - - - + + + + + +

+ layer 1 +

+
+
+
+ +
+ -

- layer 1 -

- - - - + + +

+ + source prop1 + + + + you get one chance to set me + +

+
+
+
+
- - + + + + + + - -

- - source prop1 - - - - you get one chance to set me - -

-
-
+ /> + +
- -
-
- - - - - - - - -
-
- - - - + + + + `; exports[`LayerPanel should render empty panel when selectedLayer is null 1`] = `""`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index c534583ddb58fe..f736f87dc46e16 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -21,13 +21,11 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { indexPatternService } from '../../../kibana_services'; -import { Storage } from 'ui/storage'; -import { SearchBar } from 'plugins/data'; +import { start as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; +const { SearchBar } = data.ui; import { npStart } from 'ui/new_platform'; -import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -const localStorage = new Storage(window.localStorage); export class FilterEditor extends Component { state = { @@ -93,35 +91,26 @@ export class FilterEditor extends Component { anchorPosition="leftCenter" >
- - - - - } - /> - + + + + } + />
); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index 74ec80c0765e84..fb09ed342b8d35 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -14,11 +14,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { SearchBar } from 'plugins/data'; -import { Storage } from 'ui/storage'; import { npStart } from 'ui/new_platform'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; - -const localStorage = new Storage(window.localStorage); export class WhereExpression extends Component { @@ -80,35 +76,25 @@ export class WhereExpression extends Component { defaultMessage="Use a query to narrow right source." /> - - - - - - } - /> - + + + + } + />
); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js index f7e242a82b89d0..9efbfe45da29c0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js @@ -29,6 +29,13 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; + +import { Storage } from 'ui/storage'; +const localStorage = new Storage(window.localStorage); + +// This import will eventually become a dependency injected by the fully deangularized NP plugin. +import { npStart } from 'ui/new_platform'; export class LayerPanel extends React.Component { @@ -144,75 +151,84 @@ export class LayerPanel extends React.Component { } return ( - - - - - + + + + + + + + + + +

{this.state.displayName}

+
+
+
+ +
+ - - - - - - -

{this.state.displayName}

-
-
- - -
- - - - {this._renderSourceProperties()} - - -
- + + + {this._renderSourceProperties()} + +
+
+
-
-
+
+
- + - + - + - {this._renderFilterSection()} + {this._renderFilterSection()} - {this._renderJoinSection()} + {this._renderJoinSection()} - + +
-
- - - - + + + + + ); } } From 2f80cc5fb3e0ec69bb12d78eac32893964dad10a Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Wed, 2 Oct 2019 09:13:00 -0400 Subject: [PATCH 36/53] [Lens] Field item hovers, empty state graphic, and operation not applicable callout (#46864) 1. Better hover and focus states of field items 2. Added the empty state graphic 3. Fixed #46753 --- .../assets/lens_app_graphic_dark_2x.png | Bin 0 -> 82733 bytes .../assets/lens_app_graphic_light_2x.png | Bin 0 -> 94444 bytes .../editor_frame/editor_frame.tsx | 1 + .../editor_frame/suggestion_panel.tsx | 2 +- .../editor_frame/workspace_panel.test.tsx | 19 ++++-- .../editor_frame/workspace_panel.tsx | 33 +++++++-- .../indexpattern_plugin/_datapanel.scss | 9 ++- .../indexpattern_plugin/_field_item.scss | 63 ++++++++++++++---- .../public/indexpattern_plugin/datapanel.tsx | 24 ++++--- .../dimension_panel/dimension_panel.test.tsx | 8 +-- .../dimension_panel/popover_editor.tsx | 19 ++---- .../indexpattern_plugin/field_icon.test.tsx | 2 +- .../public/indexpattern_plugin/field_icon.tsx | 2 +- .../public/indexpattern_plugin/field_item.tsx | 32 ++++++--- .../indexpattern_suggestions.test.tsx | 4 +- .../operations/definitions/date_histogram.tsx | 4 +- .../definitions/filter_ratio.test.tsx | 2 +- .../operations/definitions/filter_ratio.tsx | 4 +- .../operations/definitions/terms.tsx | 2 +- .../operations/operations.test.ts | 2 +- .../indexpattern_plugin/state_helpers.test.ts | 8 +-- 21 files changed, 163 insertions(+), 77 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png create mode 100644 x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_light_2x.png diff --git a/x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png b/x-pack/legacy/plugins/lens/public/assets/lens_app_graphic_dark_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c2c71b82180a7574427c284cfffa91771a2296a GIT binary patch literal 82733 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

D zSCh;_Tk|IL*mq<@whL|Jl{9qhwWBM zFF_aQzz;-wZ@rA!qL}mW}R9UN#yXS!;xgGXcRs$@hqk&+r@eP8k4bEheDTD7V?$ z35vhGl=hlrwV!4GGcV4^P4GwPzq!M%>ad-%ZhhAGaJuC=h)hb?V1vV3w^?5Zp_u64 zosiZ>YIAAJ#K=aD`aXdBzzXAgFHOuwu4DQ#XiJ)er=~(6nWuF@>9H4Vza{ErC^8zAR{g%L>QH8igJ;!=i3%36rw)K(y!3Ar)=MEl3 zkgQ+uLXn;|TIBQe37oAZfD;@MM z4h7Wu!6K)6yR~4Yt9Q)0Cm9S1*9C0DGj3tkIb9T54{4#dQ_VhMG{;N$oh&q5sjEEU z4arF@l?9)tLJTwPo(8Q^clP1F99av$!XitzWY5c7$9MTy-eZ=VIO9={;Fu`{R9dIH zXDf+Ol;ZMNs!^VXR8?e8@BG)9L~8kOzA|@FSkbvNYRd)2UY4p_<|Yb3w%_#bOI3rM zNAX+W9i?gJ0v@W#rD-=Hs#l{_Av?d@*i}ZcdD7Hud@<$`Sa_@pr;{O>s@>MTMf=lJ zD6$8AM!E|UoF2U%^ZHU-p9~X4a$#xxU_hm!RP(|!K8wrzD@#7(oEm3T)CM^$z8+4{ zWD6$VnV!%oL^>0@9t$E0$jlGS$Hp|nYUVUE&i|>iR(Iv%hg->R#;!}s=nDY_mnB8w zuc%7hFCQqn)6{Dn{+#K1zXHNGq)&^k2&&-b1TL~Zvw_r{_`W26#G|sQW`eajToAWK z1*4B^X%8U>iL75dJEBLiS+5>`te!>&kYATC0y6uP#a2gnO%1A~08WM0s=d+|G`1)9LMsAK&ih zH`O&ZSJ8vtTrM`8-`PY&=E9~8?q1KpB5w9Nrl&U@SeOlu)sj~ju6jfmcJSftc-fvP z7;xGV44s8vk=xdqw0hFJtLHc-D&QK$=i*JsK6_+lfu*fY z$~bB^g&f_ENgi*`3uTJx56~zST@qMY_pM(UC-iDi9xpKZX3C2ug8z|PkKpsi*DYC1 zLwPs3El_G~l%E{WLEm}|jKhT}-hfYg4_I?MZf(x(8K7M4i;E5Bi_yadS-HGEl1~}l zNU>m#KkJga!&*{#CqFu~6FOuc-A%<6{ppn?8GrDxbp4()O^fKOWc9(w1Y%y8yyMPv zS#C>cNAoHGh(|I^8BpJY>%UF{PZheAv=Ce8>Y>)vbgk{-~{H(^Ys@ zeLATOW@;O=qW>m&p#kT)&GUD(GdrIR1A$B8#DIajOY-EDE7DS@3w zxE`Fk$f>x;9*CJ~S{iC|hleS=M@$ZlJkG;uNq~DVVPSW}dUp4S8*`~4Oz=z}(v*+M zEw9FzRc3HxwMest5RC`J*tkM2=ibyc(fF2=bHq`V_$Eq~)&^37pwl>;;iaJaT1A^7 zuJ|Lo3E_>se^tY?@`uI3r~pY8a2i!8*R1O9NE>*(X`?P`-ypqFnyXdAi60p9Rc3fK z@ZC~b%PF2h0^jTof7_EsF6^@!tCmgc9kExm`=u*3HRZVJy_U*o)HZ?Y!DEZ}#-3w& z=#}-=Xj}b44eobS$yC%SVWq^DW`)1ZTQ)Gn7JDFA@(!J05qSTttP>`!<&8V_LiPow~YsGonuu-P}Lxl^fN_9|7uxz zBex=dzfcPd_KFHZah)67HixO>X=OYGmd&?klTD6{N|iNa z7W?R{pl=k9KC>i+^Wj;Jcy^q~mzeB;?EFrny@QF0rUG-@tjV`{SL|`hAEV>oh4y!B z!_V}Do@q)I_g!q(h4I!LFN(1ZiA!wXSRYEov@{|A@|xugLHUdhO+zm(8@%f>%r1De zOs-`asLd4I*Q2)3GmfC1-!sfv(f6Ky%qq!#HPj|ox(V#w9-44MH^ewy)>|F)_sR#t zXV|Ox49QWDMBc;hq_0+aA>$n;&Rfsz`aOzV&H~WyDlE_)DG;2DF(58VC)fh%D;m%0 z9+W?3w?s-?+w5TA@1L-J3r5e|6g(1|frY$ff4lPyriQ$8ej0P&6VDn|SM!{0rgB6| ziR|mQ9}A_{@a*?)Ytdy>(`(bnS2_x3dKUSMN+rlP(B=acW#DY_*rbY;xpL`io(?qzS(QiD+{*GdJiK)JGj#~$seba(^M-kPRM^( z9y&aZSfJjYsv?878@-*(+`Y_-^x`We#5-zu=>8_mfA{7N%qKHYO6)Nf!cA}n? z*h!=JmX0x2oUsp^I&;!;`+?;{M_2peDKEzSx8_Epo(hM#rc0(5=<;`QoD$UZ;NB#5 zPV*$p0^7>|^gb224=%kFTK_HU>G_j-ZMw5($%jM^04?;LUlmUS$tL8xe~M!#aKL6J zyargGw{za6<)U~A&XOs@SK|AH2cO3*CpTh`B|; zvjNvl!J3UqXIN&IC1c%dE{@{)rK8endC`mw!thJ~plx=X^d2FXraI6lA^#geO zrS1r}u5hWD;r5>pV(Tl!R~h8J3aX@kyvRJ=5e(FlNt^9BSlry08+#S&DGX-!N2EAi zEChNJy3u!8+(qIYZq}c6{TgYn8iGGKHi{Z;TV@N+Bexed_Gk#uvT|LO!kctx^Wb?3 zt!REQ5lWwKI+9R_r_-2_#QATbif`|W7NX}XMyelN(=xLG+7A=MPRmgZ1*_R!HNej;%5KV9 zcwQv2UUA)fd%203h7GHkGQ5}2cRTxnto)%5A;)K`<*N@anzUmuES8F=PNnPGpM?f( z^c{Ikx(&0&@5Rry)u~HNMqs~Fzji1%+H?O^e(;F|P_y(-_)cz{pHp;4KQh%Ft)c^) zk1CCTZ%YiUK71TsjdZpzi6>;-?>0Ll-o-{c6inZ?T9Z}V8h1%A(Ef75G4g^FPp=L? zrSV*SFq$-X!g*SPq0bKbRVg6p7IpFM1zh7bTJ1PzNVw?MqL8W}TefGI4Zbb|ak1?EZwCquMj0(Xv;_@;T*9Moz|;tT>>{Kp92ngka-N@Oz`N89lvCfi^m%eU}*TOca!Y(*lmq zs_qX;X~x%XluhrLO&jyRvudRgT+yN={m$EEp-e~Va33yfzj!^7IAad^Y*`OfnxuG2 z$CAcSlZq3FCPq?m zL7Z+oj&sM1h_5u7GO?!d-%F@5q^(#j&+U8vdp8`z0Vw(F;pNrt1uOgny9O^QED+Oi z;jM-0I>r0_E{dsh7janFM z7x0cjW@w@8An$Ve>E<{2>JIZyBCA!6sVe z;hfqx-@kvRF>x34H%sOVNyu&O<#>yg-x4cOaYIxib7!*c{whv8`uaF;(e`k^3Kh`e z-P&4|-Ly*q1OJgC+@iSE^P2$ozMLRw9!jMROfpQy+H;b(XM}hJjk?RLJ|3Cv&IWIp zovG?y&Br48N<4?k3A0Vtv2^))%_py3a2?9wP7~~gK8rfl^$*E|L-rJ?6!@nrc7%jx zV5)fRjov1AgF+4GcXn?ZIZLcRL>zKbnKrsC9zFMyvP!tBaO~H)ryUc+WQXxyR4wl@ zdxo?9%HQA8JS);bc-D)y4CV=@_iouz8-HGONG)|BUGm@WeMKCp{u^IQCk$F*2;QRF zC^cC^QTL!vSFr85L42pUy23?^96veU%c%9^usuFc!CgP7Mf7Bam~Qq_xk8hA_bqma zVoK%-%@b3xH(|%C4WPS=w112l)&bLgQC}>(#Qo$>x9Hq!@Elye`~hAb8qv{oVP|>r zvpq{HV?SXy%{Pc+{L4O)m6BSKRT~fHFe5f?8&?X=x+B)B!8$JQnMFXjrPcalJm-J0 zpP#^>u7aCI`cYTPO#JxF?XrHKy5EO2yKmB7K+Iw1BgB()ylL=fX18y!6lATxqO7|U z);Uumxof=Xo;?}#Rfg$x4_t(G-S92fwaT2_Q$2mlz*Z5FA$J!6~60NBasnlCp{g~liPaqIu^SyHz%!m4MgAO zDrQ*b5+|D*oEW8Ld_A6 z^om~I1={2_4Y-qe$?#nR)WLlF`*Lj}uSiZ+ok$SBdB}29`-KJy7KrA#8HRCp&)|OhX~E}a;_4CXZO{3TZ2|}d*n#{C^MV;dB)~wELFL+QK=ZM^{oCi~*yl_R zwY^eMnwRR76Hruc{q7iVdH7(^f(H)lh@yCi3)p&jMl@I-ZcD*DAgFUJ z?Dy*OrFiu+39zskXr&R~W|)mJ#7Oh{#d|OSPf1cDbjU-YacEe~VzAA3xbiMp#PJ9A zVs&CGFpK z#&=@c@a)BpJtVaS(qx@3!aqA~nz`_kHHpEm`tb&hI+M0;XyTr)i7She zWrVnmMQDeZr@CyCrcA%VR;V|T0>X<^m%7}82{!YR0RxS@vjcOL=7JO^6Y>3VuD~nmp_vWa~b&Wj6 zLYASK+HG?Fqx|AwbA7nAh)xjH5Ds16Ir=^sbEhKWA^>TL)V$fEK* z#SUBNPeK|f)`!9T_dAzUxPzm%5A9zq$+AWpvp*qW)%)VqppSE4PHV@zE0ge!#%?(_ zf1pxQ+?vsZ-n_AaKJv-~?}G_~^`u0P)7t#z#GvX`LLJ=XRIS9>xCTRtRxXD0S+xl_ zPi@~^WU7vDFqo6)$98;&N(fTf&}G9exAUw=NA~`MkgA0T;u>@3bEz=1tADL%*Mgl8 zfG~QCd3pJ+ArS7U7u?Q1!t@FNT^m=BuJy|Y2K6g!X0o(wL-00BGa3NZDBc&hdMCwv zlSk{#3!>wevHq|)RgZHWlNt&6oB5H^EhY&#ly9Wu?pgD1@Y&>T6%+||t1%nE5EUjupAhukO)MhyC@Jw)(pyvwkr05hm z8fvv8TjZ-!*}py_Tr91E-I~W%dn5)6&uE~-%7R9VF`=w%*<*OMbilEQp=)#nd#o5v zMU2MByyayC_lJ`Tj-EC)?~W5sB}D75fZDaf57scM;86&IObrCg8B=F)u>v5lV*Er8geG~%7_|6_qMsdZIao5+Cnr`5`U5NGyX+6R7%v^{AEH3J*0|?7+4ih_ z`xUps_A91xGDY~c==egvhNXRoRE3P%Vd~!86tZ3+$HSOIDHH|#g8iX$qRy_T7kr&< zQ>v#;6%v-oqUt^~cho3V$nT2F14?>q$x)fOi!~>N_tghNC4_JGmB|K`?1-CP|O_fX_4E0kZR&h0LKLnVDbGXlv^&Aqtqarz`5UA^=^RX3e=~ z_j4Q?M+}wW(Fv{Zmc0nv4$(;Cs=!Y$CC{qI6fOyaQX5)$g zmki_)=5MOY4(zUg07Ft>1H9V-La2KOmuNJmFx?#GUHs~xO%W-j6gyp?;4vbVh+SY< zf0v4{g)3v6Qd6^&kYvZP&?o2!70vn_3;>M?~+h$xpZpZ zE8GVX6v6Q^=wV|~a-_&)a1YL__9)2$EpuYHFFcVL#V_T6jhv~@)k(yLK|jxP8^H~O;Q`I?YYb--i z?=OEY0#Qnu@PtrE`FURWWBIzeZO+jE6-^SilvFFT#fOr)6-QzufgcaisUSYaF|rVt zNEmIldC!Jza8Q#Zf{!G^!Z?M-LUfJ8S&^CTWP-w4>oHXX%dniucnx+oKU%gJikIEb zeGx*5+I`9f_v{eH%*wKh)-$Jpg+2g1w}zU)z^%s@-PcDF^@9nzCMH*RJV;9q6nY!o zQ>`zpk(NrK7>gJ-yvwI3orPG8`@2eh$sd5Mx8CY=Lj_OtyWR5#<;`0$Cpa=>i)8(9 zPg{Sl*_M}r7yr{XQtYu)z2H91*y_eEC5Dh2b-ezpJ*b|xneRpRa9oVA{;BQm>KfsT zsTv(g*Yt78qCjm)*u_Q~OVtFq5!&NUZf+vV5#+m8Tyky1b+-sEOBpQ%@6468`)WU3 z)Ure*$R-K?%9-&A;_*>%tdI6?4wPce=>`bTQB2!%aheYDr+6xcUt5VyKJUAL=Z6;Q zxfR!HU+89jv-V8EisR50uFjlr&?^9Qcj58>fD+ji6~@4y*Rx?>^kIS=qvfiHJSmA$|{y*6Bp zpdJ3Y+l7VUs5cGS&}j-lw6}Z63MV9am%h6<5|Uq~w3CjCqUBEC=_h>M^;Ry^zJ*JR zQwe5wKU8-vC!Cb5_PEqO*P&#DQ{IaF#Gpj647XT@vb_$|qE6oGiAGJ_m-?#~s2hY= zrE7J%Nte(`jU7>DMS_^FE0|5Yoe!KGG@)a1OD}3$wi)aYA(2 zkjWe7;Ea)jom(wRY;@vrTK)sDI6>55h5?zk-s^Sdl|f`nCOzW;O54{)U+R;LjE16f4xa_nm{nv3PCf9E&V?V~ zBhbDBMgab2J2pgYCg6?oho&Sd+~r>QfyJ=N?jUd+QYv)ks|%N zUb8Lh^ExNJAHN>h~SYAo>dncz?`7h5ZkO;|UszeyIufOv8{i-&lO7OrrMi0M& zzb!oJ8BSY;dJ5Lb$(KtaF~>z*ghiweCMuqY&^_1V-*{)#upH1af?{NGa4{zE&W4|z z?siq^0?XKW+FoAm_g{gyYQvsyX7FWhmg>%GBL7v#{Op7!0?n%$L!BXVK=1wK_(kw| zli>G9&t;rp9T{yV8<_p_puR}wkv5Xh65{Y)&YOQVD~I2PPIlY=jN7F9>a}JkEfyzV z%E5-or=6|nG({UIFfkV_D)u^BXmgelj%K$KnE-B7U$jE3U2^?yKyU8}Zg8Z+7+i^G zD!Mv7As3wClB{HJBKaKr*c;l&pTlBOgUld)m#K^{5Qt+QzzhMTWjip6TDsfRU~OulbjhN zevd4l^8f667UrYvU(O86HhE1hK6YK;F)NeAc2qL>f7C`0L94(Yb8_+BL`}aULUIT|ZlRg4o!b zd}Ys6oWUZmRh8p@(8cGWep0^ehdPZBTGY!%~J zjZ*J9hHx#mAg)SdO!zJjMmYSwy*`!Gwj|G(vfP=Qo67=qj*Nw(^4awLM@L~yxDOJD zs!jCZW*_XqjEU_$(odA2`_P#ciQ^8L7HX+v;p?t5%6wohjnW4$>ioB5Jdg|E`bkpL z$McdVZKl#o{==RK{f49j)bvUWDE$S9NBK6y$ct;zF&eL!m18U9=b#w#i>k(PM~d* z*{~K137h=5FPbqAl`amOjm4T?#V2_)5jPRe(?upFD|m@R$~Kc`Vgt%&7G4aLp@e^# z#6NPcNFsQeY^4nBc=f#F_9xuEa_lGN1?@Mc!l$`uMI4yuqo* zo9wp;yg6vDB+H+`4`jK?rS?zQGf_HdX`XGE!T}+jo^wVmk1<<5evXU?uX8B+^dvzd8fa*yAU<0L|ott(jx;2F6gmy7hw!i;D&A4yj&uVR@kr#_Ln9 z(6E_X_w~UTku@0l7La6U;sckLm>IjypX>m-)@{2^&8@2fA$U&aNW?i+u9FxYEmACt z`yQ)=;@jYa1TBoyU3Sl`_7ooyAaG544;T3k5zw9u1a4gKmTvtqk$sspQ6~}bNt!_N z(=@(ap9&Sos++|esBb1qwA<)Mui3T~|0B(wFyDg1;E$ASN(q`oNlgYitj(nhiaBDM zMXd^H-S{V6&PWff_$)s;ntpo%r8sGJ4R zCO_MwEp*AA!Asl;k~hR{394&uZ()C}sb_mDEP`T4upwu)G}8AxtObhK#K~(!{iiYZ zzwJ6CLS4DHopI#?S_q=H@4Z>w$t!!oW?dAH@5A@jcwch-gED{ zr|0{%f9|!{p0#G?nVIK_*a>l}DI}OJH~^+5w;}cMo5N(c8D;!G0*lj*JCAZh<`vDs zq$!i}nXlp7FH8#ZR3(BK&FDtY@x=&YYyMvc%@P(ld0J`|9^*AJG+zn;soM=q61rt( zcn*+D5FO71Q@FPhV2NS^aW)L?s8KPyggNg(h-(-;KLKbSQc0A>>c)Gz>K@{6`@Xm zrHWsc%QKiFOPf=D7?Xo*w=f(x`QPl@Uq*_exK*i~rT+|N`uQYQxevH-ltRN$T>KsP z$T1i%u2A5cG%H8OXCH2PnQlocV)n#GML!xhIQDhS8-0Z33EKZw`e3_Y6KP=lCmR`A;1f5c&bg40&Qgm&U=HNqoPwIr>pC4cV z8BY44#uD)U7~OADnwb21ON{!N*{4lPpT@~+y;!RRKwJaMHsmG@y>dRxu`aNXdSTQ@ zJRQh4`^D|Ad_L4OIgx6tlIL0)zO43gU`n*6xQM(&wEuBc^tE`1OxOzYInx*#FOBK; zg`}0T>bTCI;cC-iG3@*S<-WZuK#2L5nU%jAM`JBp>lQk zbjdjozTibGS!GT-xbT__!bqT515vbf(E64>kaiHYj3(a&ZE4>liogBl#A)~T&>U+V z1*n5&Ud~t&B^f>?7&)4k-RttMAKoL2N1x*pwD&fLeb0;@(`OX%k_yisQ)pvcpLNqK ze+dUggeJJq)lKSz**3gfN)tG0R^29}`$^7&H`DTME8htITpM8G0E@;^+vIk3U4VZC zmfEJjRTx}UNTUDMm*gAMSY1;*+-ndfC4kKHYY;GwGV;K?_AyGihmYJ$w6bcD0|gpP z?$LO4x=LF zBg`#~^j0N!3MSp}#!}UQfJut2umAc-|fuiUfRTxIXMSK6UheVT2;q;U=885KfL6 z6-v!{v^r)o&PKwd);%zF`*J16iUp}Ja|@6EGca!|G3KR2h3ep5g^zA5RiEDb?Q!24 z`Sm)rJbV~ql$As0dN&-+LN0v&GsF9>QRJKm(ON!I^x*6{Usj1W(q}YE->2dbpfmCR zJVYeGV%_@lV;%=X0eDHBHN-ukmp*OHqY3nV>)kW+tJ?H6hDrc3-qxn>T0Ki>O#L6c^ zlKvFWaht&{oO?-RBav9@FEGU@fFMA|7U@?^O!4t3f}rPlAwJ z`6rtvo`O*4MR6<>WIZhfg{NEySf*T3)FomF!2Fl-85wG%Dlr0b292x>S^|60tdASPUsP|my|t3$zS<{DRER=4Q2aE8bM%UY>!-CTSl$m? zqhr%m7+%;Pt_r?Ac$`2krj3)y#Aik{^;xPL`BDU%$tygt4NeSBOQ7H>neVpw(}gFS zSxi*Ca6{QA&nXG^jUrYY_2ZYY;JOJ{RaRa?M*4-$f>C%sg?2icp6WP%X7y7|PsfQN zvpLrz`NR-3^CLW}^@@098l%TUw*-l>nR5&D0CLKD(*_T%rzdd@=~+Xyq|zBj{haWh z;!|-x_Epo96^{X(52j5nUdk>}y-N5eRAC<&S@he&Wb+t*m)seI-`2w}bg;LkRoO#^tsENgOCIOaQ-ta1xDX1hJ z{ICSk(j}qx>asYO`HBH8cRM#$I$5%aM+pmq1XG+~pS)+Xpk`z=f$zzGYP%M5nt}Y1 z!nj^+J}~%tVWg;mmS0VM<8!fzyFwF*!zcXX=3ZCdqavR7En~aVA zaDvYw4wPTQg&JoVX#muj5JItpkf?R1$hH7JEk0Q%OFz2(B4z#_Ffei|?B{MN1xwNJ zwH1Bz_g@*yDK)Pm$cMcr?*T*sakO_Rs4a44)iziIz~{ZW4Z=d-^La?T7vFY3~ml%w=& zrHg_TZI=(l6E4RBGm;O9C65Vq4?}~eeU;W!J=5P?V>B!av1KRAejJKrMNd0STo@Yt zj;??8 zT))Q5Vxq#V#*)<4=qPcj6?@{RXmf8RLW{O6&53sq7exP=U-Bk!@#o={Romxs;H>Yw zF(-gU&K;xS`Hi&+qZ-Ufm8#YQCBrDA08yqC_^z4(;Pkv5B>RlnE4FJ3sRPKE1AZDT zKR92Dg3x!GS7Dvx*2vZ6xlOVhEZku_f)h+Sfjdt~Rf<@yBkW-Rp2%aa!m4ukMgpgQ zJmZclEfZAEb|e?hZZ=Iz>n!86WBQF2?CoFdTJeAn@$J|nw#oXeVR>qF;Ds{!Hqgwo#>p?)mxh(1Am{0caNrhj@Sccb_IwSPqjCs2=5JEPxg)Q!(9)kw6bH(ulv z@A#T~0LQGRp^-Ah^+`SlY-vc6?ULeXEfUWHHl!bY!C=??zmj4B4z3ZLEQUWl2(D66(=iKUj8wLn!9cQUisLA76ocP;_7>zpLpn`iBWvn@A_q*U+r166`bos)AT-Iy(H z`bv?4gTA{a+ao%vU*GSvm5LvNVCw}4e#(||9=0|;HKfY(fCc4Ki=Izp$ZJ^qimw-d zY@7}U>xaA#iO5WtyQIp^Lc4RAwHrXx2b+mZjtP>#U@iWNQ7c*lVXZUn>f)dS_^G6t2gEDOUrTPPU9B z)^7O6$QP$WmLX=gOg7I8AmYeX46D*o-!THi!{?Z2u3aIYRk4omSER|(tg*qI6hm5$ zGiQNJI-0~&tI=;~I=e9f+n++XZmprNLew?fCj+R#L)lk(qa_Znxc&5U> z)DuD8h=ur2Y4nvC0v5;gV>1>9%`(p@z2mWsv(}PBR=j1i3*0C3*G6y?^AWsdO*|W? z1(spetXZ^zg7HHa4o~&%BtFr>kuSKk*2GD_7l>sBqKu&F>E!!Q_(qAXC&CLW$0A?@ zYj+0+rRI2&FQ}d6r|ON>;#!heSk(QK@QVl@)fZ+7TX2A5GlydQ6UVC>X{|rqqjl%* z4!v>dQtWyYrA5T4CT_>4RY*0a^XTriWzHn6o^}sg9Ls1YnzNr@ND{be6O^BNxgvM%p`+FG8jkUAx@CCO>;(ms~WP;G2JF05O z{IXP#@biCudy1-?Mz~fz(0K1mLFG>08ety)25Kirk|lgA`3M|X03qpzE!l~;W6@gQmKBdfNkUf! zKaHPEdSVCRXqn^RQn*&|UtqCX=v&8c3V!&{*nJ2Sfb_@^+DQ$35~(Jn}u)5nh3)+x0GddCEl( z?mfi#lW#$d8TFMvfzG~R#)fz|CzIXH7x+U44sinfpI^Q;18uK& z9r6Akv%itS6*oO_@lq)doA&SUUN*>o=!Ta!+n0u{LODNc(66=gy~wrUtm#QY2a^U4 z24Ovg%Gad22NGj_h6{z+w0~+aG_ya(A991Khp=lm9G`KxeH&VOtYZ2-XM06_ve&o zI>tC)9=FXVEOKAMrhORizirtQqR%o5bxahkBdw+Y;rfnv@TotX3fymlqJlF?`eu7O zbCvI~&MK^qS-^2X_usS{AY2c`nm2v-Y3_#~exE0Qv+YDSUL>Ek1bMxM=N1IHDMrQ_ zY_XR2D%QZrrOes7h{)9Cch1Bq zXkj8CdsX~Zta&Nj;?WoIyxp_!utRfmYH~pV!9w;Bv6@@+nmv9>wjpYda}NIMz*V4^ zM1I&a`zFuqemt>u`D9V#H@0RsEXYrSMisGt!VxD_fBAE=AFI&#lAM2mI+-J# zw^G0uJ3^WH(S|_7$s3*DmSaoBMq(eAdnktDz|cpEy%zd&SX`Pmw+Cxwyc<9l*XT%c+{pgITy$IZc*Pa@XAh5Hz;12kNl8Y>41k6xU8EM!-c)>a z-t>(gro!mM00v?gTFBT^*8T@p7EP3h?{`~Ij z!*(%7+{_ruDw56+D$Q9(R@X239W5|0+uAr7dHET*o4y-po0C>MX+ z7=q^wcvFKP9xuEpWhAQuLdFsd+`OB*c~d?1)6Z_!!o1b~VVMH8t;wU}bfR0>NkF)P z!`>9s0j1XsZk$e-JybyalUy$_!;Pj?+kesg2ouiVAd5BRU**Oe0bclRDz5rWj>^PR zOOFA|0sgp6WuLXZ@VIupd2Oj4f;dU*#0Me2QW2OCLHzHYU=~63SBJg}q~mB(rBcPh zs&DkPKSmbI?%vr9KBVm-r@`*~is`}0YYn^uQftiirrL*qTG;T@hj-#bVG?5o4d5^M z4Wz%4Vi8w-Z^2%6y4{F{%}i|85w*IHcGLC_H^U2z`1D+ z^i9Zw6mAPkWwRUjb%TBBr7&|V0)NP5+?!aBCLYH@{ZZL7#x9v^wgU*5k%EXF!V}82 zFVVGce*_iG*e<9lk2&aDc)?*$)$IYYErLN4+@&=hXFu?5W4HruAECwX25*V zF{D469Vc#)EP2piwxn_UAnq4>FWDH8f0sjLA;3w7`kQkwtiRoyiRlI$kT>hrN!l-Q z+;l0r2=*~t*^H8QOie0|go^r!()&NQRQg-kLtMS!s0yjq?&d((xND^QW&UT(PmBM# zU#LuYq8ed>xMsS6)Fu6gq#ZSA+3!c&Y0@SkhDI& z>qoLm2Iwd$FGu(6BoxN_?AnA7B4}$vEk-=L6GHu}dn3^xt#WmhomKooOT?dT_qgh`5NmA24b3lJV%9s*4TtIPW z6v@+$2nUVy^U`sDKe3Y!NWCaqiF4``FmBO2T71X`aW_!B@&Y4z;CVKv7U3|jZwi)4 zxUMf!*P->*s9P0;wr>!$l`#PVH&zRMraATwH#r^ML)x#oQzG!gaZtzydV*|R#Y^zR zs$@P|ux$Ral5zwPem1;A(h+7nM_rAfWEC`>jLK29e}DR?>Dut=IpHf(aB7iR|7hclU-JRnn($dsU`$$s|Hlk1K5QtYTcmemIRpFg>8~=Aro!*A znS}ON9(p*Dl)m8#5Xn3<=9*m2%41cImkxi{W*|=D&%I41(~#3~NegRd#wi}7ZX*x7 zl(}s{{MFaF7Dq@A=ga-_BFO}H?ytp#t7n@9R{@O~)jaY^)w?y4ZnKH9fKACsw%tLp zjrO3U!EgQP2?_e54!W)FF3;u*msXGH?XIe2x@CHugRZz37DUBX~}_{O&p+$74&i-q%L5&gk2JA=G|`fas;-?TyL-CkrJRQ2~fSi792{ zgNG}4GCpLr<_X-xkRc}sw!*@KlO0!$aGWqdGtX#G%gmKNzstu-Ys8gOtV+34ukOE5 zskn2ymsFO>Sc~-K17QLEM++bXges9ypS}C^Lf?4o>SPYApqF=j-mGKfYR~5#)4;U|}cxr_*pJv^J(j z%SjvSn+0?vfrSH{_*21r$TTcOW*CW?LzM#3bBHcV1%`P363bJKOfn7o%(d#z8#bCW;Gr9O-eZvTag-8{JOaelFIgNpd|D=1!XBN~Q9Va^D^iD4YOb5AB`lQgZqY(I?}EKlj%`%A6MCTHUA^6 z@!#O`2=IaG!|X!1Wz@%WOZ=Sgz7W3)`{tdwPN+HEOd6Ep#3g2X3>0mgS(X7f=b5gw zZkLklRw-#*MvC8cpau-=xgXK+acnHnn&Wwb?1uRj zmZoN-*rC)=S0ZY(%X#3ncx8NPBjNlzr9i?jDkhnM-!mI7&;Fvx7rDTPh2$FP*JPH2 zB(&kF?%116-OM%W%mM5EC*0c}B_!An9N+@?xLj+xYjcIWarlLD6Tswu_Kq3678_XT zj2Rt++{MTfoN2dnRblc1jQyR(eEYzJatscmMkvnloah$NSOCiggvn7MnWFAZDr7Lq z)o9&l60*X-Rmb;V{&UlSv^#_$yX?wJvn`K0G@k&X@2v@WFSE4?l6~(r>*x!h9-Pi{ z9invObpiP-`0|%!&6EI{)-p|$0< z2@*S9jXgw9l#qIhEL<5};YTgPKrf*@L0I2Lyqy33q)H&=Cj?xDsqEs0;NVQw@HyB6%t;!9d-fSIt(9olbW+Gr&ww_NHA(`-&xDy!HU zQF2^`cqT;If1+-x$PW!yxU;U4aVv9=-GeR;hv^n%WX0sp>u+0czX(Zm=SE? zX#mlT0OVr}pGQKp&~8@M`$@OvkuPQBYJ$t^4B_3X6}5z`h{!M|EH@G>D()Gv{OJt( z75_2CvZT)_x&;rnPMx7n&bCnAd+2saWX6TdT|YU#bxskwL)Q2IpqR8`05j^UMlpLE z+B76veO+lB$QvsQ#-RCvNs=_aqNAEie4S*SS)r!tS(M_ha)~Twj?Fsnlq*KIAj)YT zh7Q3jZ-S751sx9xF0CLr*5Mq18Dg2N;N@ehm3-8d3G3nxbuty?mDt>~s&_LLEaQTh z_ku4KZhgePQB5+{i%LRrwm0hWwm-@lVA(QBx@5M`?_P%!)?j$k6*!!xXJ)anW8hpe z4O9Y!>}l+c)XKjv&u}!_)X!{ifO0Gg5lAx5Oy?ID241J$!Oqa;1J+&Iuf5#w0a6FO zmUd){nE?1$;OdniES?PppPuBlTyU;i@T-%KGyImBodcV8#|--AI#zTU=9fgCq0{8b zyTO5>q(@+u#f5^K;*2KAexsD>3+Aa9xGnXuY6wl3JK*|7FSpD?V<=m@G2Vv!*0x{V z0XQ@}(i|pw=g3E||1sw^GUG+{z8~*+nn?DRXUXs-YK>8dSpJU}XZC>wENb*dX8vm5 z{ILHKsV|=M6GV#zg+m55BLDL}u!#t09?_~Z^4?*gij-8zN09Vd{7fed^n-VP%R+DRU{_~pBKJc9-#Ir?MP3F1kmOTt%uJxc8Ps(tsY|}pDiaD zn$-L*t3^o+j!O(IzuMf*o8In}Q5bf9-YRiVZmd^sW&pXKgWC~m2!V;WCLEQwgwKoc z!6dv5m26bJMECzM|7wN>6zTBqE_F91(47e4aMU+643W&A2g%(C$%`n_I8^TGlLbA& zx{;niPM<=h3bY6d#J?aWZ0*B|bXEu^2{h%ki+x&30-*agrCjE$U+Y=q35IwNTl=eb zHLf9&lvk{i=NY)v6_!JKl*4({J5GS%-*Zci$!XRZO%&1Ul&?c2<_ zOJ5*XZ6-bW{+H#03=t2!s_TPL#nDiKWR9isk|b=W`;K`tTenJZVc)tm9i0rxz)t#O zW+KBQ-eLTHSlvNj(vx4l(s{nT6RWNu9mqex#z*;Wx^|JmwtyoOUZH&;&a!l19|o$L zJ>gxv{IMQIC@@WCjs1yjyo0ygYQKQIxNrFe>DX}zeu3#N>45P2fx@0nadMy|{^2K; zG`Zehs}AK73$7C;tGZSQpsPhTM>OZ27S&f9YxJnA_4U#Trvm7qK zYMD&D7f3wd+MIRxx#GlWE=Jhc($<*Dsr4lB2jF4he4PqJt>;0CIkv*9q`OoL;@L-W zOT31y*QH+sS0z>q8O)RUS8*=9|9tIa3oHqho9p-A;N8%Mzq?A#==jlZy)C)*@o?n5 zp*!+A5y)NzMCJv}sq+s9VA&Q>twch53*1=TTybU;7%w+eo+JXK#ZFBM^X=wR92S7P z_pSC|la18zPa&vpmt=gV42|*Wr95rpDKi|Cua&ksg+-6JMx|$!sRSCwS{OYqn1rrsAS8}9ze5b+w^k&o=6yetys&Dj^O`L`%$$mOXXM2#4OkpR&Rohkij;phW=Zmf zmLAay%xOd@glX&B7x=1$6-;}s?xxqv^JRQ3$;p(Z%nypj;qmTZV z;Ln(FmnznBbcn{oT<|xeD^zR3lZ<;u+)ubj&*4;r?WpGoG0Q|v%mXW4TKm~yFF)&m z70?u@mNslJYF+1xN|L9#G&5&YrEl7$ks9T4mIRUocmgW}|7~LbD^+Fv?%ymmX^E*E zFLTW?820>RWXyTjk8ZAI`fNNrakzMbZYiP<(aieOww7DaoNaO!yBd&YFaA0xS+&1AlB+$hGScO6{29mhq6jWzuPsk4P9(@5vMYLcE8${cpeA zOj+LFV_(2++qp<)(v>#-)xk-ehf`K=1Amz3 zHC1~blarV_c!(S`#cTYJ{sIE5Y99?f5^t;7?4p1bCImg%JqX<0nsMI!=BFU+FhKkl7~{!oL6}le4{YzYX6&J?LHiY{8xl`; zZK>nw-jbKWSCHY|8dk2*`*E=2SH_fe$zJ4dGSeP4hMd3I(4*vhdRu8}kt>3-6Dm5Hj>dz!e^QQ|{=Eit0EzEa<@g}} zaEseJotzG8`({R`oWTRy)WdM+8?&$Zz^S~9mJIxZPYb_y?;sOhd3h(B(=^k>OyY|P zKmP^qjq=SvG_l5CNg5pZCt}M<%lf8HJ@I9EGu{zPZ+YaP9rfC%s7ntg>|T^qi{J01 z^=oOMrXa@f{%OvGY0Vtr4zJ;S=rd}8sd#E zCWM1nD>=Y>;4^95hG5`mEk>G%zPw}@T`Re-Q;(vo2zgkesf#QAq7b?0J#Y%C()I%5 z2qnzp&^|wzHn|W}j3EIoW$=yW=$`i~qa$}`Y9YhJ-0>Uj2p6ME z_+Gke@0JW&y8P)m|Cj|4Fw3>;67HW+JfbUtJSPD@9kvWdNQD5Aa$3_O3m{Y7owa7*+trgRWCp9 z8--M3YndgP1Hle2aAEW`P6O&ds!S%oXkOUO4A zq`P*u{{9-J#+hf&9GU`5v8X8o;6nDVuRU-4IkC(6Z%C$bxotI-Kj%7dt{4V&Mo;mr z=S#<*_Lb9aj4IHrqItOCCW~?Ftut3jms^ve^S7*isaY*u)v&H~{FFW8j4S3H{TU*6 z4HLK6hdJETgz8G2e!D9Xqw6vjyMI+EmHc_Fj>g>|zX%%D!5fcdAuwOaqM7$kQYT9b zF7H+xB!$@PG0xN3_r_$s$cStt``jDj3<2|Z#Bq%&f%=|o$DyQK;PzJ@pfJF&;kPB|lUkD7YwRzW-XFPSt&e6W;syxBt(3PZKrbCO$6qpJ=u(n0YtdLm7=*N{+sF#Tr<{Rhr z16dZ&U6NKzC+ga3n0M=JQysHAsQ?X;q9_nX@5nX-pb#Ex;0exBAzPa#M$asJ*SXHv zmdF}|Y@w2-|EDYWcceeVFuPa4J`O+WdOghybuu7IB%u2DVK9cC#)7+;#@cMx4+YmELQ^NuVQ+)hsMnQ{NqF z!W%AgFZ-P^>U2lvUye2cU<-L7^~<9ZO-0RtC}wI$WRmb){>Yd-$(uRkOIKhAf%S4g;YezWH%1uZFz^rtjMMcjKb(i$+lk9%`(R0v-PzS-t{SCnN` zk%K7rT7VziQQX!0-z^P7PY7XSf}5B4`eMsZ1A0sHg_6GGnV=kUB9m!3`|&w)N?xi3 zv%G;X@4=V*j9lw6T)-?SBd6V7+*Hq<-@PxEJzi zXKHn%)*i$=7$(y=2&qb#1S}(OO{GiZBwnpBn%=oQrw&;AC<}1tZ4vx2EN1X=SpOcb zsubaDmT!n;t)<>~uEt4j}ZM2y&&ZVcT6*Kzz{$WlIXpfo>nkh~`#)!7^}VE?53?O!G7WUnNOv zN{KSVYgxBQL%fOee8a9*eR44R@0>vj805|-@5g!h>!Y+U6$j-q<5H4mt*T8&uBMGM z-bIE-rSo?>6smrh&T7Xr@I`zAL9QI*3J~Cofmd^7zw#{$ayz=c?ijG$oB57iMLG$0 zo=%%NXd5+6B}K{y(Xw-RbUz^@QIlZYA9Kw`RUwd)m+4FI;C?ZB_A)Jag03-F^ejX} zKY)%Mtc}daM(SMKlms=3C$;`fNec=Jmz3y=Y{hNYDTOD^@#5~(Gq8QQ#?&B7t#YO` zje5Y=YNP8R*81*)gq`SyuHi1;U-TAO&MElbM#~LKH+RzMci}o|2fZn~l_>N^wFlB; z=|OKZ41sqDY^qz6-i9cg`385xMUe3)mjhyfGDpNZMXgvsFLX1fEroRUNkUtcJlr1I zkD)h}$HV^Z5?$7jp;RTnUTK!(hV?FsDv=~b11HPU8$aneLyU~JY1QGyeO2Wf38W)?S@ej%(^ltv7Ax5CMH9 z>*%Dky9q*oEbqs3q}k6y8i5_6ne&omAseG-JRT2CizoI(!><`sySNu3ug=L-QIX>9 z(P_4S~s?G&a6>I`X`(v&iI?u+!;YCQN&e>EnhTd^jmRAw7T#V$!!^Z?P3hpC9 z*E{V<5_OPk+A(%$p(yLOQ{fDOizCir6|c97Gfcc?d;e@V*z{oNo< z6c>LatF9Nk4LCc@m)khRYwgy82qBR+h^vyX34t+EfIHp4VjBzmeW0Qsu;pViQ}N7m zu!?v>TTed7NdU2|LHf-bP>u*c$kghetGmDa+v$LG zQOOl$ATra1oPCPsWG7|a$-p-|1F*|QNfMV4Y1{2+=NiMuoXT$%>N4SOL{aJDLzsFZ zPFs=Z0Li5N|I&)KV21>Fe@}-zoeSPO7vjxuRx_Wso9p%(u&sjTj`054g?-~y-Fz`0 ztjL}ZmG-)PI;6*i*JdPbg_LI20JKBVI{TN(vP!gnIniW>I(kL>^R{tFnkmmW7ZJbW zwmF`r6t63%MQm_(h!R*=dY9k#_k#nz2^eE%{l0|06hMo zHj4MI3qcv0b5Z}HRzr_MUHpmcTrHeLzyZGUkCVH~t;69gO-?Ri_kT~ttU~{PeV)KhboW%UahFsO~1eM^Uo3?Ob)nB^Pl24_dMW28P_^$o{5t zO^`s3w$|~vUK2hP)yZ9>RzDrS3Qi&)Tk#W2@Q{!yUQp8WKdjpkUBAf`mjin}eJwKW zkE0$KsltGoS01TS9fd~2MZqDneEWKjSZyWt)|8Cs2U;3U>aD6f6m4Xi!5Pq&a5bPlHqgD8QC5E z+!y>>cURT|CfAz}0w6B?+zoG^9?6U~jACOJvqju6hod8kPZ7v5#WxbM8D6O~Pm#Q` z2yw>W;QI*M1kLmL535IbS%8cZCV8gQ0POR?%W;xfXFZ71Xg8$ND8L}sr;Kz4*oRHR zY;=&Xl_dl}DzFu{EoMrq*1^DZN`8#ji&(-yXVcd$p(Ckl$C{@c5k3X_8+y2qPOD*m zxA#_17`i8bi^cL}&hjQESY-IE@{P~z1aZ8UGYaNb96};4g1x!@YBF^FUA#hm`1SL* zdfc`19X&HKWrY{CG3s07BKdyr_lcj4L@>bvj3|ur46CH;P0M^`{*=hmd=Xps3~F+t zEI>O9=@QDINMxsy1!NbO%{I;plhz+fq0N5Bq$n3vw;IQ~n0iw-!d)I4n1(OxgBn&@ zNp3fcJ+rVtU$fdtKaCX_bytDrL1C&%HB_rk@o0^M!-VS=A>rCInbvd)Kvp|Mbzg5z z*Yl1ivv8rLCp+vN9m`4eES>}eaQ@fS<|5LP!KGPk2EIyh(Jz)Jbkxfx=AGcT+`erS)%zfJ^=9kds|#a+)7NzF?8D(a*wC4#rg*A>}@ z4TLzw7USic=r(&airHUv-dX$z^NYD9A2{<<&CHi3c0owv(=s|j zMG;RW-NmUYhbIQxoR>0gpI82#n_tFNLPDWW_b#Yi=A*}a&;Cuf@Q<&Gy23eL`ET$} zJ*+Rpp!=7}-wpu5aB-JUzgd`aO+{cDd&NuN<3P|)*}4I?Xu@x`ky0?W*Pl;gG11Z~hc8zMZdBFmo9=+-$NJIzlK}u9eb!b1QBq^Y}@A0da4tUzn z;9&GYaS>oE4=?4KY}qTS_b$MqLU&63J%9_V{opp(XFlEdR`f-X zk#nlx?YZ_lvX(g5^EwrUJ~oc_`uHF9!yrUf@bM z`Xqvi>{d>nPZC;(4TYmlKDm;!oRc{t{T3wWJCd0byNQlufZBY6I$D#O(x5q%W*jxW z&<8TF+b1pG4nekb(^0dNTN;Bi80f64f=9+|8m07Ob*BH1m2g_Gm3dZ>IyIo@7ZYN?6s^DNNQ#cqIj1#<%|kZU1q*%#^S= zU?~m}+{lOm=;Sl-k~&t>KsJ?V>|ZsW1fk{@&YN7G;t)^ocvl#EUh0&-ol6_L6o_@J zC?3{Rs-P!<&fmQ>|4B~(_iG@M&(6(FN}f+@Ez6z z$GgSa4`!KnspHY?liM1e%G`%e)PZ^>Ho=HA=m%1yBboogd|Oap@iL#PNZg0-Xz3z6 zEWdw6mSv9`l61vB`mS~G_PHK19U|FP0^Gby;YLo8|2(Qa@I6#?2VnWWsAL3k-L5XC z5kCl3_5hy{!T(<*H-gHH(4<}wXRx6&eWqM7fj`aFM8{U~C0P9Fm@NLKG&isnR8<-- zdPIf~+957OSiX~!&B1RAv~y>U9Z?l%?*6eBrNPOyd~#~H!K8Ue>6hO?O}9BKG_wFA zKghuSN#QrqYizlB-Q={!Y34x7$Nt}(q6-pb$jp|*>C4mhar@37H{Pv4u-VhjG{EMB z?FCv#hh|#7Cv!%8tBl64L6Z}n^{;mpPZdi>Er|i@AJ}ai!nvn>JY+g?7N<3?b$Vs6 zLMLC&(wyj-+hKpOwsgtc#g5~UOwRCfP7J{Q5{KMrtBRr9pLex#5^7@PrY6O`gm2E9 zFKPav#~7ZF&^-ADd(ciY$csoZ&ITukhK_B9vsrrVd?$T_abK%_y?oZq`mp+W<#Eh zjmtK*jR|Gd)*)G-Q?-dw* zylw^H3j`kDaKBXPh)bVdGZw6de8q(G%Bp(u5!XjPaaNrakcigz2zodB)F=%)Gl6u#jU)r60;bPZ)D24;4ks@LblLi zEB1d1wyZ&P>3`7a{u2R30Gv@A>H_B2w(olsv4xR!kFdl`Psd6UNd>`G4w-F!AV>AH zc%|ODAcVX!{cx>^jY<=MNd5lP_B6vBJ*rTrT^b#!?Z^PpMWt$2=+CYgd_wiN49>@e zd62E_Dsy8M(tagqb6BBbf9y>D&Zd;0lh{!D^u zQm*M+Q@I7Yp=BK2G9a0(hw@)VJuyhtTZ2!gGQQ>1Tkm=G6}(%WP=)w}B=b>1J0R+| zb5tE`LM z@$+j?X-OltG=Yo3txQP|V$upWlvf${NsaqERxdE=2enTj0~N>7(S%web(HC#vS{Ea zxCY}SAo(a~HLaV9;O}<;=Y+ZlFW|__;K{cw_7-@)@BXDa2Uvye@?w3cG#=q>t|ON0 zbue6?UmvQ4mO>s-HU}#CO`pKwADDZQ0g+Qoi^xM8gS+SjC0dc?Z{%_Cq=~U2zY@E~ z+4q0_>mPng#rWiW`$Kww3vS-#0wFDTB?RE#@!0=aeOb(8QXW{kHHu#Eh zf!(jkL`gX^!3`vNP{)h0<%lY#DniU2AnS!#k{v;hZUNI~W?NrqBFoes5a?i@3Yb`; z8>a^O0zybPk9{ShM%Hy*KEp$|u>Yg!8^h~-zOI`Sois*cv$1V7Mq}Hy?WD17H4Pfu zKCx}vNs~tJ>F@tOAMbB--7~Xi@3q%j`}yUHq>z^p9ogqO+{nh-zjk6gB#jXh)_<=) zh;7mAvr$}skgE5O7sl6IZ11AFIBJQkNG!spykiYEee##^fu`ZS`kemcBQo#`*`OgL zINe4eKVQVR^}Sd8H3peZOXme!1Y3g%1c8R#KOV02)4)u0=|xlZlIy(e}v5XVqqv2~YJ zmJ)BS+%t!R;_Y|qa&tYHmZ?fH92Sc#>k`D51$r`2<8xneHQFHflcnkdcIpVE zBn$W&{KZSi`7^R!eU>Lr(EVrvDl;QSqPAh3ux%QKz`Tz8t<}4-5~(kjjsyQXK{8%B z*$@(`)=r5=+!pOy9%49!t)vZTAPbn~ps7Qs4&s$s|85nCVl0Q9`M(2fFobfv>09>e zlA@W{!hYu}HnnmJ3lTh|Qby*qGBlUp4K3lzns&vlCzRFl5vzG}yt&nMhuuwSTnvrdi~}?t=V_V+JyXOQb)Zs&e9CaUSmxDY|9_lV3MS zB37#_HBlh05TC2h2&kr(g5G}hnc^jPQ-Ty-FI5q%xoc$LCT=%)mrk6uxL|J`Nv5orj`-O+Kc4{H7Eh8x} zBVz?UhH$leb9%xuyyJg|5*MOGTE8d_p%?>r4S4eK6C7u#1~Bsjpvue|OyhU_A!FV- zFqV%f9`*&L!{}%c2GG)sm3N$_=W@m7`_-?d;N&>&SK;<_^I3LLE8V4AE|U?o!p15Y z?Uq(FrSqX*0&*X)auniG(; zr9y*#73Z)`7nnv#pSJwVM8P14%p8)!2?)_)PI&+Mjjf2XG0t6zTgBE*v_)?e)eud| zisJc)TVgo+DMP`2%<|7C8+-B^bbXRp&6AAiQ7js^oWC51FNPz~aJ$aWpp z(4dsS8PcxBZ|%MBO_WR|!lDeMf2(`+Cm3b0 zqKgXQ#M*CCa-hht0!)F^Fuj57zauU;)RG&cu^{;sneDqb1{WprK&rEEEzbpI1G}6= zObjZibJv{RPa~?k587AsP2^2T7Pvg7{%+E30e47FX zHmMVJ8?{vDo`r(dbIMQBR#QLcg}GisZx6~j(R|cBb&C$FzXx;~40W5}64X?J5i%Ab zN^QFM!ytYX>wLK389BV8$nh3Rt4j0)7~NdH0ct*D6IPGtL0TjIaBMtTm2d%wKHGq3 z?_d#+A@@i1D>hCoRraeKXGnm+7`2$e#>F31ifOioN8%>Rz)}|5rwZ|WDYFyi@Kh9w zS+_G#gTr&@yiFEnVDj(QUCM$ot1kYgi>Z{VxB$DK7o0W()i=S@e~*|aXgfSXuPEab zrIfv}u*nl8#-f~uui;uK1YouW=HeXpt*mNT!DN4dBv^OwCSMXgW`N`*2)vgHwId}| zs2&FvmGNSDV0@NF30;(x&ChbtMUgedCg+jJM^(-Z{<*yKD_AxQYIO;2*M@5UyM(Xn zD?vgACOB}$w`8JP2mrLz1Swehi~1nr@Z~MvyV@g#?9(Lg56E7S1U`OY>vd~}q%ydi zCR(3u+Dcfo=ud!-nO>GXwu+2iijZB;_mG|jNz?r+yU&CraO~2#07H9cp>4i$x&#Ne zb_F?ZLb`QH7ywn}cd5smqzxe!>%Ci%7G0 zl4fZBp^vTmaH}Oc`t?8hRW}weKse{ynLoF>-H3C!0KQK9+>B+hxVZ8J_7`)~p5TYI z4Ogw*oNq{T#3hN+qTjH13=wOEY34fMNo#`hd@}U4Ar7(k64F1(Wjr;(0yli5zPz^y zFZk?V$zcb@b}pmgIzu%u`lnOWg)Wz|<2rW+I_BE%$XBJ(?aTSCpW*tg|3QQ9%DBZ^;3cJMHz=gNvmSmL9ot^{!8KGh-+*eK% zXAQE^Spk$Vsj1vWfvRMpk0$F9)y4y@VpDaUJ2|c*ZaA5PBjwAX2_TiXuH z#DH0L!D2p!^d>#8LE}v4iF&1;lUyS!4+sD3o4`yFqO*7#?B%<`sp_ZeQ#74=BbV9 zl2L3Dmx?_)JM=4(!3flebkAk$PuIoLo)}&6G5qkX7@ZN7x8meqEsj|(KVlF?&2ajG zU)g3E#R|vIh+^nK_*-={f$S48I}@Eafw(|AE{E*%)Tl&C&v5sS)?%-XtxH3!)8lr1wuItH)6k zf{44>A>M5U?#mzaXKdS|&oPLL(sG+N^h$)Nu>J`0cdo~|zX_zt-FlXhjn3`SG#N(( z!oKe;rTwu^VL?AG)s1vSWP8O?`*^fp&9-Gj@!VkgfSCi%O0HU1lOnppo@ayB$xwz+ zA7`mo?G4@uG()-n#pW2&ZKQ4cDrqgB{Xhyu;FiZeP694&^(^FXlRul^Tx^!KlOwz9 z2q|&SqDp8N0Km->hYG>q1}X~t&P|_eSalSlyPydsLEM)txDSrVi4fO&5>AhQ1-`*4YK>LC zB?pQ*r(a*Uf+xOu|JH5ySNl>hb)3cTa7v6UIg*8BW3^1m+O{c6q8Y(Q7JapKna?rV zRoRH`)IT)nXTCy4>BRX8t^|CAWejxV84X*y1I86ndAd-V{>UfGw7FIZxt%cZAU2^a zU$k0B?Gw7c4w+6+K}-eD-J6HcwSxZFb~i?t$imrLn1P6Ys>6<=MSoStofmUz@Htd= z-wD_5?iW?OzcF}@3Gaqh*rU>9eO!9r?l<00j2|c_|{qEGMP=?_= zF=Ia_9@I*v(slzc>o4|DA%10y zv+*c8C5_D2;@A6b(r5eO)n!Fw-%>yDMQ@XQZ|0Zp$e|Zphlj>on^(j1a2@9A51b>M zKoDs_^KtBB;uC1n_IuFbO6aR1mytp~mn8P>(!qYlp~6Wdfx%z8Ug?DoT704&yLb9p|`uF^{=WBH=kKwgj_PNlEHZ zvK4-9bX&^t=fI&m0?R6DMWVW>(qxE^eZr|*<1G8_kAE;ZDbVJ1*s6BygeuhrlkQ4^ zkTLh8AMrXm;X`Zf?{YmUHxv2m#uKlrB)|7LISn#? zn;RCJ00n;zYD3on75>TmHw!%*O1|AswU3YfJfjYx-eAW*6QK>wr$CF}jZ@oJ6@r!= zEleoe$h{JG)Zabn-QfOcwvFdP7k2#UwY+oZwT06O9q7$UBe6*0CIxab7p_+XV65j9R(ExV9be&%u2Tp7X`=af8IWB zvdAqD6!FZ8WLt&-y7DI=P6+tez(cj^_}!>&J=)eTW@zlI8Oy%1wiZ|_CxIuHLn`!O z-37)PCk{sq+Ur3ldg;rHiIaVX!-S$jsR+c#r%enp1vN;%p=lJQkh77=YT`2-6`VpV)9snRR)|(Qg!q~ zh^Wdq;E6IPN>M12<>e4TN~d#Wgd&^zuTg( zSRhw=esj5M>XYYF$6EC(I9@e{Wq#_jj%8W@P5{wbsXZNzPG%7PX@uzwwIVsU!^YyP zCqsuxhZPm)3{U(KF`#@s%I`&4-x53Reh^K)E#+0=c7S>2rt%h?5jq{OYy$pxsvEv3 z8>yYWUYP5`9a)oZ-?Zh@@;UBsBuy=xGYLcRrF;-H+?{66`z3 zqRWI|HBpn#jr@N0OtgltNGUj7S`G^e7y1?KVT*r`%X#rLlnnKfe5lr{rpouL4Od12 zxkK?YH#5K)0piw;8R0k9f|A8bAzB=Es}YZl~QMwK8t;X>vLF`UN5@GlEyd&MP1Ct#|Ray(t3Rvu8vv&4YM ztJ`7pihMj@djlsxG4Y+cwTkB2rqEg0%3ZqRwa#%VbO zY6y|!p%OPhBCTnP{Te*B>E{8*a(3yF|F3@Tie6rvGe22F3e;Y{=^mUw{*W;VyFt;R zpyz?gg5#}#4J)6`$@-V1(f}xa`yi9C$n6rBT^+|!EtVqgJvSt*1B_l$e)T7_OUE_g zcAq}gGAexbAJ-&ZDg?_AJS@HvIfRa;^G396=LKQK2d5Ntho0IbC1Qz|d$J2~E;n%& zIK68Bf(xE_Ai@0Ow%O{SCn1-W#~nHMRgx(|hqS-tjuvf26wI;{<7^H@@i%U@2>Zf+ z2bD46$>3zD>FroV@-BOUW-DGCw{$q1#v!;GW66oM#0;>F?kV*q`m%3Vtwn0fVwp10 z@DO<v%$2D%UnX_ zy@0g+UoMR+n!mrKTrNjkfipo@&eNgrv>vEi;PYzS zT1>ew+Uof2ajZ>J*QCifk-3|;hUi}R2;A(2HS>M0r8@0~Oh8ZocjxWt?XA_oVWB>L z-|&l=*W1wR*`;O!wHI*M;bA0=qnRc}0s(-bUBJ zgfToYEDj5q+hZg*iX0H~F;LRFxj_K8FX^hlH=0KGZSr^evc`pE5xGE`Z)&_%Y5U;*MmPA#B;rnQgY@Z!b;+?t1gB^yct5p#ziz{?Sp9>S0}n|9_%+Ofx%@W{ z5+yPQI4}~|uC;YdTq_knTQ!d$?9i^_O?qH*MzipK?w#A0u&w0HdMN0$?NINUy5M)lXW>?hLX}ac#mK93Lw@Y zcSW*S1tyW15ddn{7o(mk@P6g05BEJ69?8*0v0!+;#7N{Zq+YOK^m(HKa??T6uC?!1 z$eG`U_hP9CbZyRb`1GGh8@CV`xf3V2e&jobp<18;a73%ZxCb~f`(}S zupty$1JhNvBbJ)?q;RcTHG{&mlt;M+>6%{>;9T1EdsFoVH$Nu~rAhu`PjHVxjkjh= z$}h5n%&AXcuxOCfmK9cu;7jv9uZjKUDY!qv$d+)d(;$LhL7_Y*Cy_*aG%hn8dB|>? z=e&B_wrQi96|;sN*d%0NOOOBuQm<#P`rY4QWT`oFfgjk^byJI^F~aD()@!toCdlyk zG(x=_Z|^txKB1y1`1zO7Mu5iR_&+vMIuw9G&g;S^tGz$)WxcN(L0EMYLZOD%!0u-4 zc85RHo?X)Q1{P;;U5R0?+LUto!s}qwc0;Wmj`U|SH3#}z{49)cV(t4+wDKmaG7yEX zeDzbaMQl7(?>uFPG;Y^K+gtuU>KJ)yJ5PRfNE<#S(4YbI=ErGFdub_{nj6B z-TTd8PLPOR9N&d!3>x;5+M*~gqv`F;llc*5b6Z7fCMOk+rItzAXf0qlOfI5vw&U#) zWh_=u$wP#oQOx{4k<*#g$3J8F%#*3=l1e|(Edbe8SkuF?~+)PRbNKMY0fzwCp zdy)DpvA70IXMVFSIvt;xwh?9Eim7uZ8dRbP)|B}#Yt}N8T@D-0ajgI~yU@XQ%&PB4kL9H0LQ(xDZcc6L+$0x_D(5v|%K(e@v2;Hgkhmbo49hb-ad}?G+)>p;ZgTH2fvur7Rd7cM59{l76Y%HU! zXQBRtgdoi+=B8vts57Uu$1{PI!(YY44;`t!2oq+qVCf_#J)`lM&$qNhnCr@H7!mV{ zR5_C5IwY;)@VsJTUvZ^dPzxCVi@jtc??rscWeK(W z+a5!d@%I-Kx1D6bUEAQRuX}JQr+rvLEMgQr(4%g;PP3-t_t@2!cHiTT-Hfew9RFCm z+TCR1(Qlbbi+pxATI=29_a4eHhr-x_tv`fLrdA=9A%ifpWk3yv33 z`Q@^TFcqj4iJD&bB-}Ww$49rjyaqpJ6Uo$>19M4jJeRqn12D9G89tWIQLypl@!?>3 z!+Qz;D%s#R*f`ZCRaXY5#eIm;`#P2Lo_@G~r2n;6_-@B^b{-qANy-vweGQqTNcdsd zDcm|EZ%1=VY1+j61%8LwoCr|upul5ZAzPfp@sTiPDt!pk>0=r6p*=3}lM2<5HF29s z?=8*lTptZwo?(v3&#II=SLQ2mZG?k?MKixv~qJg#h3ZrlcYJ#FOhfO z*m~kPBg}FnQNykTo7^;o50S*-HbB%smXfO7A=(6tW<+>=F3#kjLo(+3%zByZ??KA_Jb$CTHIhU8D-$BxI|Ul4~C9z(olmxDJ{og(s=bLQdQ# zj-VVHN;ol0z8pYt`fM0mm*HW`_u(whGBTYN*L!4&?jlM{Xh zp4(41ud4naK_7*yc|JU{X+NOkLG$GqBR9lTKI_5K)=ARuGtp;kdF(E#_K zQP{twuBDZ8wSu{?0`E+Mis|<1<#o+5(LXmp2RX__*8He;X>3hrpI_{MfQilp7dqB2 zE(c&y1lFvxBcbX3$FU@&tjVIZPUJlr4rQT?8LF9y5LGrygC{a2#-IuF0dW#ylFyuu zS%c`TUy^tx+l)!SO`3%$X@dEzS5jmovXcp`tz2;x2m9jU-^hJSpF8}ChZ)gNJ3?nu z{0JD`MU*T~$|kNpXZ)GfXYCc#cQ?dUaXr_6CycJ2)xRxO^YSpHzA{|-ze^0BHNPu| zH0hJ4;vf7T|JJ8nfMz zAX$r<5#pjflwF((}~E4%?+X2vGVHhVuS`RQpjOI1#I z39tZIqd(aqREp zsBVu4$6Ok+?T_V{CZwLik1oUP5!=O#KU~0mvR&+L7ZqsJJJLA&D#%xPSQ4|}h-131 zXWDDBCmr&k7nQg1h5Jx7C|;+?>Zyt!ZE{%%r9c?+N#9jm>Z>mf;2%?TzrBM+bD+?W+&o>)jz`d8?MA*7- z#PG9HV6tY*yybkXfnmAvJ8?*aswF=&Ft5o zA4vOT%+SnesD@M&pjM+AHI!T7U@ga^%4-<3L`lP*3p6?Oy%Q<~w7j3WGmiA|Av4yW zu?U}}jO9(hoZx9AAofY+vI8=8_9|;aMMr4(`C4w&kt!=-E+l}N@4O{&x(#ZX+n%xb z#-F(7!BK*uss54k?&-_Mr=oa$;)Xw(b|mVLVsH8^YpW+<>BEIfR!d)OCo z25OkLpaz#D{!B#DB%(#J?a-$oidrlvGsskwWT?y;b&*eA?~PRo^$?!qJFeN{m@Ppb zHOsRh4Rmb{SqzA2^Z{;}mh@*S-zA`a6((-c{e8UQjAdxT!er~AM-F>KtU*>KVbVvi z>8XNPzGzF?e*WOjzuX4DpTZ12=mCs-k5n}Y>&!E8Cc^GezTfTr`J6vlsRc`V7eI$` zj0v>zRWst?yg8v<@mHZqJHL6`FS~a(R)ENZw;QsE!cBIygrABim*`ZKUK&3R%TRgncV zhVv9_n8=d_c$-W)Z!)HKU2Kygh*Q!oZaym9?B?0gdkWEg#$efBYyDVB`!+?$o5I=n zt<}1j;2SIgpcqYcjX-JxHD|+EW1+TS`AFFxT3G zDSYywe20L?`TsL5h0e%oZheQ-;?1>~LpwHi2AU(t{5%4WSDZ~3l8EVDc*~$+FE%E} zjv843%0B`M%ySy~(s4G~yYk^KboLqg>YuDM=oz4J^k9Nc2ankJs|b5$5)^{iko%qP zZ1C-axsPB+i=dUZzy=6{{Q`gWmbJE~v1I&8XV5A07CL9PA;9W9Fv&xU$T}adQ+jUR zf37Kk)uCTc#hPcmH&2U;(Z!vf+YT&$2y6`u2#K7oGe&miTiJdkbevuipjiBPBe{7c zvO~ei0*3r+NzalM_3;*PGR7*)d-r$vx?}y`eEMUVmquj*1(q{-WLH@!!*x$7OlBQT z-TVME4-rL{@)~ooHUsq_oooh-e(~aC29G4oNJyQ2hW^=2&xsRJ7AqXCJb|QyZBFc8 zu4%VWw1+NQ!QfF{EhDuMQ`WFyL=Fk@t~`qiyL4&V+&3ULxoG|fHjMrC%!%$%Aq{xrQ)QoSWAp7d`V!`-1=1?^ zD5gSt#h2x|9Qi3GD9gS$WOSmsP2);y!7dkL&oYY0P%GN~pd2e?EimP@#_KoG%rX_M#0LHDqG(RNi@8=|4jq)F109I7AjvcIz0PY zS3apZCNm4*P5PcE6e~bsNzCD<14$;M!2S7p}9zh$cU8{tK8S1Y~R0P z8#%K1WylwxDKQfSJEGM;Qnt5c0w=jlz8+q)IWwsHTpuClOc5bavUCVLx1{91QTT;h zA;Rso26@3)+gE)dSEh~puZ9&4^o&3-!&50oH)yS`A60G#sz8VHUc3W&4YYqcyDI^g z;GqC+@6mS7^tj_6l(q)K-LpXosF#slC}1|FubAL=#l6Yq9-YSY9Pt|iZG2xfZ+g`I zzCH?m*a$_@tLgGRGmE2Bbts<9Q-q%cY#8E>~~`_%?z zNBm~zua~IY>wvlCz)dg5f54qIRB}P+j$Tn+!S`Un9HRDQE@*$@wWKbs(xi{b()w{E z;_kp+kKJy>l9&haH{yW@ie>%}8{*Vuo2!ke<>W%!aoHYsZXg1De-)=Ggwa+I zZT)L-2yv?;sYJ(I?}8sqAqf9BW}7PPI4*7jY=M`Wz8wwL94UF7$`EgfPRnD-3HC&S z$cw=UC{HQ7{sh7R4d(`!E;1MMWOBsXphyxU*VrZ_M_~H>{VX23 zQ*klZbV|HF0vvaRy0(6PC9+4WUE+x7oZ^?W*o<8H)=cJ8q}|lGSLZnq-IGlAX@Z4q zdJ;4pEntJR{;8FMxKlG-NyjXOqxg)0`krl^PP{hb+SPh3UunjK?}iIw3BDZcNVPnV zj`TW7Ggl2>ldaPM=G4^_f2wp|lT?lUY)xxH_5988L!t9Sp7rQZ9G`ad#m7Gy|3B_p zBkD2&mTQrNLi5(MIouCPt}JCEcO{MjpzPnmz5e>1edl6d-jXws{MdrJjsyU8vzVIhl$ z68WFi$;3C}F1I3&?I5Cv_c8*`F;4eB9BjFhu!zn!KNzz^YMekfAtAK|Au{P^yaBEf znK!6qQT-H-M1LB=3JVn_;1$}fEQ^UB#KnWXtfviH=hXQBA)IP@-G(i!#*u1zB)|Vz z{RRkmnW$90FMdQU*>!oV?aus}hbQId~hiNhi674=R$JuxhZI)gMyer3F`|!4`iN9`KU{zRn#eAvt zy&Fuk2cU~!&E>hnM%+N2wB}}0VsIpk-spodhyJxw*ETLNK|YEezpwq`H(6d!GU@k? z5K$?tiA{QnP}HvU+r@vrME1m~9Y|RSX**=``b2S5am*W&)N(JiY|&|k5WZO}$RR7N zeA%lc7`L0NL^6KR)u+|0YpVw%r@d~iO13TX_S2lpVPl6BtN>@iz~u_O0beX1eFVPn ztqu+aYx{2fW8n5cB>3N-qYC9G{({0SURGQAT=+_zu3tH3Ru|VhvBBc(vY%##P=w7~ z!+GA`MTN^%L5159X7V@XGa2z%zh%FNp#ga(`Zp{#P!o699O}RtFIVCw?C`Ih;hevm z^&$*FxQzwIk12q~dt|#i_OCjXofmnw-s0s2hBve9-%8Zia$y;>QPXLlgv1~n zViY&@WN^6=8(a~mas4O14^2hTX`e-TQe$G>nc~oAoQIVRPcq$oulYh&<+fo-ocgyZ zcR~On>!jeHr-!S1Hb8i-><4vvc(@6a6L8w@34`Nq8(FC3O>gJSByTci6Bn##2GVQR zKDW*oRy=O8{?Mf`W%w+kgE{NXC#{Cl-qwKu-CXEDn0#h-p>4A>-avMJ?OZ5W5_O3l zi!!Tw!^?9qmN61S_Tiy&-ez=KH6oIegRY4=27a)DdlqMAeyy5o{bVUdR96{R;e~r8 z)H$2q`~vLQ6y#`XH*0Bc4{{1l%C_w+)ho`AB!0(z-M<86@Y)`f7R;N}{0azUTd){@9(ozdeDvmdrWSe)khaM8C5oOq-m%hhImoEZ@=PveCbis2t* zP6&D;4e*I%kI)q;9na2p@@7sh;LX8#&rLsyoSo)0O(Yu@Zn7WpJnlk*tjyo$y zP4~E-j?q1`WW=tVvLwH3pC%I!5VJJc8>aLDr}uNK3^zk z?U}vEv1xl5_mK}!w%IBM9dF@V^IPrwXAi*1<>wa$M(f-~QuJfXP;WbhRJpPr`zvCk zQHTP?ksLg2Vx{h_nin1_y%ENEoU%lKXCU_;6wh|mo9ZqA#obZdg>Fm@?y{1_OP#RW zM&6|8+=Q+Z+R-k;D}yEb9co%KAmDyZt`- z$fSH#Ot2Jz|7UmGvRW#2{2Bb%FY{szh|%cEAgUa6pd?o?okD3vWv%ZP#c_W*Kg#sZ zt*izERhJN+Rtsz?L_t0VOv8L~^PJL}oB{E`mo8`L$XMfLI`Oy8+|Qwry6)9k#DJVk z?QB9_%1dlNQN9eD3I0Ce35rcDT^b&T{eL3&1=OBqyPu2A%}$mznhbKp?JSrw8w8P0 zlFsK(T0dTS&u;eFp9&uz5Mwr`(c!XTo{L@CA{rbBkm$EuMTS1jQ!}tGH>7?Hr{kgW+EuG-}>GE zw#VW2-YEl2`bt#4yCT-9-e@m+*H39~w#s0iuJST9J86qxn+sR=CX1&U_w0mT5p4eq z;VcY0 zInJ?3ph<_>^2XYMmJ9wVL{JD8`PNmKTBp^48oN*<2kX;_Luh#-eqSE?d3z2#Ts_Dl zZZE$_tKurtqEd0Q3OjBX9##!IXec%Gi42HX%bXo%2sK|e?tp7<`Y55V$NjWU{J$@t zEh(#56OLm8v|V62N7QFE>Qv=RT`e*i@ms^-$rD1=G1Vt?By0h4Z&K~}_wAum%Fhem zl--{AgCblu#oJAHve@B^z&erGOXBJndkpP5R&%V*q-Ny~)(*+1yBk6?#4hI>v1`}a zR!i;I^Hz_`B!2U!B8~Qzvb$K&{jgcbDkXk#YWymxvk%3-bm8P+@%)d11oCfq9qk(G zBP}6kX>Id@P(eA~^dSjs`@M}6NTcwq@f0&ZKNfENF1}a4ez6Kyy~8yp0Gx;^k?+(m zV4a3ow+l_&ks%XmS}>j^RB>a&%VGy2jGU*BCf}2_TilkS!n87Q_UChub|<4zAM;() zW>D~26=O)U`(nWg`&CfPEqc;P9g?7tw9% zbP%(waItYI;G>;ede9N(v!i?}KkZ%Wx7RXezo`9e`4g{pmY1?w3sC_3Zfh|1?dGl0 z@>HsxK>7!sASeJk=h<_gb^z#oedB&gfA5?{V?AE4b z+q5|I&c$!L*lD$Dl8%ovaWPoctc+UA;dvYkac0`u^2m(sPs?9wG)t2&GKI^xwhxJF_xrVd9 z&>DWCW~~om$U>3g^{>RMx!qPplw^z!hh@v~&f~0w0^6_!m}k3E7E~kBRk1>;dK<63 zuSgc45Z;bf&7^6*tXzTV7-e`8q19tZz(y-F>M@ownUS)atWD6>@jevc&y(ci)yEbe zBW9Z__ea+s1RVqs$35yDp$wdiB@?It{H$!#k6QXF%4USPQ$GSi|#IaS^CtBeS(@vZEz|_1ng2OsIBIZuV@And>?2B z&WxeJ?h^JrE+?98Of0p;jM&MNQ8r<_k9+ND4db|a-ASn#iLfTC45&B^OH61Jl>Sps z0EUxYhL%a%@_i8*GNIy@P*2iPX*f^@!I?rn&3A7w^T+STlgFo}~Z>0YnKN{zU%C&&k(- z+Opsp!H!Wb^1Nb+Qj(VthY)m7G6WknafO-y} zOEvV;;XFoEdW4m^AA)u*{6fAskIJye z`ET7?1c5ieV0qd>_t&R0LAkJh-YcIe{DUuc73D0q#JcaU=X?nfPP-1+!jygg(S|#+ zJ7%l)1zDue56PE)Jjmv2-~3vxq|Tba|BdjVoK>3ms{wJ5>Avf-Cr3^v;!r=byHa~)4pqHkLRc%cI6+t{FF2p)hD)2d3BvR6qzI)Q_-x>O^ z5+6qZeAjQi^GiC-(5FnEuO1i*ZSL=1yqp)6dN=`_j>wH{YE1#Zu;_ijDpHYw0h3|) z7qHu9-_~#n|I9Wx#*FYP2{($fiv;xeL+sS)og=+`B>v9G__%BLFf+5ubxkEhIsV*v zdGQks1A6r!yObLng%E0sDXDpmW43H%d>B(GV0~7T^To8zYvv55Q;~Sd2~5~QR&aYS zELT8upI!(%_-Cg4PofG(9B{SfNFO08$W`=~<fLDikJEGY>#2`rfr?_zwSx6L)1kX(#8ydR)~^3dV*AKbZq(m zb8QT6f!pTGy2_RXVnh*ljfZpv@#{Z6QHP&qIwm4O`Sx~Rf#^(!V?(TIJMIAUDtN3q_5zud5`|Fo!!VjnMxtxef1LnODtN=VaIW&0ZRvats=f(z~Eo6 z=4(x>xe||-dRbauIF@Kc+CL&V-2J$Iux5AAz;q3e{uq_v>5}529-_OaxVAE-uG}So zFUbb*ILm;-ShdZGEe;iA(t!y!lPwneJcA%bIsJtpgDNQii$Ft3Q_7=?-ND-xsxo)~ zIFUzJm2?%OI%}kn2$c$?&c>0wj5MhTZELtA4KB37rz5$5qO&0CZ~)ZQBeiJUCp;EC zn8@4fbcKEJuwldA`c)9RgV_yvAW45P?qcoc@=3K>-G?0)*rCS2i8m^nZ{DwJbK{)v zu-TS7VT*Qte1X>BIc>GL)9~6Lhaq~qS^#}JFs6REAu3#yf&JqsjMX413(AoS&6bjM z4Q*?(w4R-&Y#1$QsDNoV`&1&pCY$&5d3tpoo*~u=+%HhS7E+du2I+ZWo8g!hI+@qB zrDhDteEb`iA(U88*Z%7&_k`Qc47{1>eFUho00_WmC#cpHxt?uk!LrxFZHN^pWEX$# zBw~PIVSdopVPs|~z>fgpk#doEXiSK&0)4)6T={sy(?yI$N04fb_u9+v zV$)yM>P!Cg2wZX7b=^gH4GO@%#C@g_1f7WFQ$3%{jV>3snxG*5s7qnDDfE^L{C(Gn zcBz;KwTiJSpOvU%lVn+%#^PCmIGkUJLo!XN;`^suIF=cWI7C=UGG4Al4(+0?$(j=c z9m4hjbaFmg6;4z2{#Ic&5oq(LuAFkktU-ierDh}P91if`GVet`X&GJjOnR5-du|SC zjy8d3=G|sArP2xl?bEaNTUAfX92s*5JGUAKZ73ApV^NMlg5k#eka%trpl9x=HA4z! zPGiNS=)njk5KuGnSw+FyoQ~@|?h)=d*K(e^=*2}|6lR460#;tejTy+12udXE0!DW! z1560Pr3{2=zCe3$KsOjt7WS>@|C@RFE#Enm1;~AVTI(^xj(Bn^j|K%HEQEpr<%1Fs z%+*qRM%$tlo@>7{`3*?nl?Js@Jwlj&|hI=k~qB?v(F&U+=vl z6~FO}1*(uS1-!vtrc%#QfRDhwPmuDf%tWpJs(p(a`F)k2sT&QE)^_G1xzo<}ko4-T z8D_#)ot$3Q@YHjHvdWl+27!>$47UbKIp_LAdHp)GR7|Y{6Z1c)l17eSxosL3Ki!1p z0$}H*hsj93n9eS!?eSOvFLr}5Q4@hvQ$xv^nHP^qo=+%c;@UtL!D4cK@Xv+{l!rnw zW}2{k8tm&zrY+9x~#B1*H3>2_gSAA9i+Paxn^4&nKPaW zChyj&P2mOiz}_Cg;Y(yQ*9TGcwRmr%F~*rF$Bulo0>(ToY=3mg?^qN1puIEsoB=Uf zi2iF(ZEAyP&?$%VQ|s=^>)o#DE)mg0fN05)BN7-DQ138ni5jwYxyk&GUb{|=U*(}; zUi-CX^HRR&%>*HQ^@L+PeV0oi#s7Dtg$#(%k$CTu7i?~t7G!T2&z}dhd$I*%27@?- zogEpn!e)z0TE@&|{@91$d3tPGJjWoN3S_ya(#Fh|-F#MDQQUoh7f5~s<=^h53UnBr zwoC{Ov7!RSb{$QJ$9red!^(Zlv|6^Ty)aipR18=W1sh@9J)Y&}Q)&81zXxJ!!GqXB z*B24a2w)2^r*Ls8RzCl`Yc<}$mM9?X@m4L82)yq&DA}LrbCd{6q6c3S(a^zL+hFUH zV0~zc&NGDS@Uvg%?(54?n6p{8nxlP>wX{zrfd0Po{+-2mmT>R4RUSLpZeTt9zj1FI(d(cL6 z*&A{$j`RD5F6t1yblNW?>b+q)Bz%TKhN8>Sf#THBd6>l}4wKi$K1m;Xqr(RJe>V_D z?9UuR}U88*U`Ey2RF)wNww zta0z`O+)Z@{A(o6Q~c0E;N#Mb${$jRjZzy&=)si9z#Dw5zcasCvECFbWUuhnX9+W# zrpf#91~XMzbr9<8!t)VAFOMz8I#itzn)(GgqHs3!_J4yzG7!8CfdNNBHv3N(tCO03 zAX#lma=T`<#eUX|TW+B8`s;QmWP9wvNs|na4>(i?jTml`T5>DpToWalWg+Uv+7IR?+@{vS{8;81!0MGL11lWm_gd9tm^ zHQBap+coKATa&G+CZCLx?a9X5@45HBf5Q2kz1MeR?X^Ny=2cX$hbmX??@`}G=4d#C z01p)Zp))*GKfLe~%RDkFC+=;}4Atq-4o`hbj~|DswhKi878W^WH-roNu6C)b>Vnl; zS2EqZ$P$D*xd=O`6XIF_Bv~-KA_&D~Mw3{3!#hqeXV6hCod^L*<}uvd5xZpN9<4ba zrF_45Q5&o`M=3r?ri1)Qj&qy<0iBznG7thc=*ae?fQ}8mS7D+ewG>Y&YiN!C4QnoS zPAI5oG=$p**`p?+s_VT*2H`)5E+vKZT<+uhBeGo47b^Vk znJ!}k(zg2WLYcnfeGb2;6NJ<_iLZgz2V;)W6hR^>?aK#K%)`mYjep2}U3K~`o7=8% z_fpD^@`o{dW_{&kL$t#YN@xTx?Vw*yy09vA4op<-LMj|!dDKnlc%m;%3W4AZzwHmW znk6uXdfGN*)e`-`p8^v^0^f&Gm$w16w6L&0v=`&kWIS~CXJlOc_@-^c;g9ImLzY>A za8!+1OrE&Vi%75m)IUPF!^3mzM!DWMD)3a_;E2~HM)exz$ZA&!T^Siv8OaC^GMX3Ljb#_un zvND66o&4v@x^gelHys)H|KaozZ}5Q2RJAKdDJ8rmIdU>K=XK*Ym}JD;_-$5oU} z65qt1?8;HXa^e;ngk@T`lYFb50t?a3ed)OmmyzLk(dEA zM>9E|&5mqcAb);e^S!xy`dT}-;`krV^@Q*s^LF@}2AmFhSqh~n4e3hJ%lUq^je`ME zH6dhN7qWrI@texnTG_ZJtK@#p^7BuO0&?Aie9mGsZbbW?b@`>`TmLSi=N}?70;XN< zq@tvV#mUZk{s4@P-1H~BJN)Yy-*|n9d~d%odHrAX^h4`g=wdTHQD@Y`s0ve zV6kB6=MJL(puK-un{gn=9j$D z<7H;am;Iip>8=nnnI0b4I*WuKJ^46HunTN2D~K<2hWO4~$!MsYxAg@jgAzFn(yE82 zc59e%N@e5f|0~-dMGOzoAn=L*!U8F+VObE6jDvlcXfxe5%q3B$iCzv5syUj_|AJW- z|7f?oswCbD1vB1Tq~TgHf#nQ}D%sTjiON(uO$*MJ>yTF4uRTtl%*f{Pf6Fh&#^Hpy52(F1Qi#W=vkk&P`W;q937b z5uNfg(7u@7T@U8D)wX>)5=pp2LMAV)>`UxdG6hS7+;&%o?WM2kt4jZrXXLQUv`9PH z_O7+ZUi`i|{6qVT{0zY%iyUX6(;&o>ZFRfi`BlC?-!Ch;;I4W-Ms`e@iz4~^7JV^S z56s4r>HP^w{9M*Yx@L+GRw!}-LzZ7$VbMnnV>O3AYAnMr*<|p`|H~-VScpY9_l;1$ zgA=5qiovoILmA{3U9>$kdmN#L+`6@;x>@hqSHLf?;;F&)ef92M9q=&~NM79OpTBR7 z@`v{M*q|bqG4a8>7tkpaI%mEow92lrg+Li#J^9HD;gVsM7qI+P+^yfFw;={}bbOuy zuDr#i=q@R_yMG8uSX(?W-|jWv{TTYT&siP({pnH&6L3{S_6?LlPiVJ2_HKw@!I3d< zso@G3_1>f{bhqi#fa555Kwg#BsPwB_*)ZSJ{J&9k3xC^bzp}YxVqCUchy~W zYz&(>z13FZWsQ5OO+VP#ysJWU7xgHLIvG!nA=FK$322 zoz_8q2|vM(Fu?A94z~%fpf}@6fMEaCve0Bs!MX@o1{)&-xO&snw|lOji>(^fn6PSK zD6myd$PB-AYbl2%N@hwklt#7?JD=xetq>Dx`j&UFW)OOcDV_!+C-U5*8-9kp=e2|Cm8@xdXCT#F7Z%e(eTsg%$%{d~@^u;zFAt%v zT2{HOp#PUd4itf>rOZ6{`P(|MGha3%pFgU!?h)oOOg=DkJN!_yL^Sp1sm)BnM=`E(~>ZuZ`ZxLKxh z(NqO3TzkMBeo-6K8C*iJmYv;3{z!UgMwxW&=`+qx!Cw~g!G%u-ITskAJZWFPhSY31h#B%sP^@%9q_i#I>?zWsKEeKF>s$%cw2j_6gz zp9FR0(I(*TbGlC^+ln4g(U<7%5g+lm3p#!%vBd*TNxG-nd5DS`yrEz07A8E1ppJ|v zju{t7-LxU|3k@)&SnkKUzBofgJm}9>{*`%So&~OX^uCQd2@%sL>5Q9wPBixol+ONN z;=ZE8Q;(cqU)F2~7WnsO5O`*;UN1Qb4z9wYT+Sc*$Ds#a5I* zPjnW`r2$*LTHQsS+6*hQ}`hd*kOscIfP1Kis^};kwLjq5@@5`yf>%pk{ zIHWdml@QJe%BL;@EoV6{FZtdEKB>*VRQZ4^5~O<26;*t zX8Y8*?3>{(Yv}oOCQ)@3iT80l;xgXSvJ$7X6p{`4V~E^}2$4J8f3+)UmVuS$KO%6L zFO-wWe>0DC_x`r--X5IC)rGGA>ZY$J+dgSNio31VBtihnuO)~ z!8hgklA0Ta2?p3y=Mq18>dfI*Lr^7jdXXOI2;RuU0Q0wEmheTDF+C6zOWHwUK5QGpR2j+-oW;P<4%!) zyT3O*&r$1N&Q`G_PzRp|Uxd>D79lEx&1Q6rJO3%;Wpw9@m%Z$QC z4w@0VOp8z4)t>AWnA}lfYR`VaYZr#~E8UEJ(0Se$|Im54Uq!9jCcMp}~%;2n%9*a&p#`zB;gwc*f^J_+r#;mEA7l-bm@AQe@Y?aV+;(b^+OnM6vLRwl(gwXch%oC5M zVN&rQZfAPg*7xzHYJ0ZBrD?CFLFIhU$X{B2655B=ooFarw}uNhYle5WPjSF=BpRyX ze!RVB+fkr0;(|u2yL|TVj^`CM}vjilGkg$BUj>YEBj12y1}w0dF6Km&wL{uNah{_%N1GLvz?wI9kn3xA+QQ}IsEzxD z62OQyeG%~&ek6C5|J4xpC{UVY%jv?q-vVfGYF;O4m9?~``3*lruh;g6{*+0rTqv|G z^t_y#)`8d3{TAFdW0sQ!+xq7?%gVNLKU`AjbDtt}!vHQW@E}xI)k+c%cROH-tRXr+ zJOjtvy!fnhLZZOUBnZysN}Nnh$fr z1fRYO#K(Wf^K^$SkjZs2Ka%r+Krq&BEVGSPYEm=gVCFi*jmX5|OT$~I`MQ8$^J+2{B;UST|HIf?tx@V|C zCvGk+)2O%EHn1(Z?*wg={o(QlO0oK(&&?0AT0lKvmDQcqzG|4Yt0J;BzELkeW0I*h zru{`0`G`z)_v)48LLTk!x-r;QsL?tKZaL%)+ER&VsjD^!AfyPpty-(4X*#m9_!oxikFemVS+$nUHGi=qsIzvWN#-8qTG@M@3SRrqY zzqrC3cJ%3pB&^$$G5j z0JO2L<9ITEqs5gT;FExl0YKyO{N4(U%F}adIt~jV*u>IZ`0`|D?_Eb|Jv^^@lM>?I zo<0hf7j<-J(jsM;R=}G!@Lx|L#|TBd-QZ_|zX#|2*`tZ%29y>h+jYZR*&%C69c;n1 z32~+2VE)^4ztrZ_ajryY3jS_tFoUZy(vf%5Ddqp$frpNTA1XXNJ+-2txHQ8YW2IAp zZkD&R%W&w>oH2P;#lC2;G^3x>81$3g1;vt4rhE`&S*Bpnq7t1!Xke7c$;i1@J#=6$ ztlcTVXIoutl)L>-ZPF-%c71OwVatW*J9hC~0GF|9z}O)mO(vdV+Eq9aKGv-?6aMG=w zFi3G<=Ev6_6Zu&N?^q7+g^?Z=9`8MBmLwM+tVV0K!_mJV{&nAzTfJZ`P2^OgwcB0# zi8-3xLS2TRl#7!E=D%UvFtyk^Igy2JaG#KsskK#Ch4`jKs`%K>ao|TX;L@#XPT*w8 zR9?RhzS6?;gb|!#@cSt3kWI|2;P17L>ezD5P_(e4UU?@+7V(d423kqM0}`}9U!W>O z7Ky;cdMPz+4ba(E4az2;ZQ51O;raljX{T7M`q;>~YK6a)=<3H&-Gm zS1R3qq$N7Ss-)r_FCCgR3dimz2<+2@`a-@~G<|=i?Bb7e3aFmpP=1%@dS#87W9Zvq%7e}JXKRnwMV3m^c)p&~R)q_!n(9$Q zeaEFeqHhOr-jVUFX^zq&vo#+#9hO?bfY;|9x^fxnKvpVh^)cESKLZ51kc67eE^ zEMuVql@P1X)7l{%(R&Ko%M-WT=^8AH24jP%NeLzRS&aA)G4Y9WJ9*Y~30GBjMW+uL zKB-Vw>qlCcn^Zq3J}x9cJ{(S}bPnAwdkH?+D|24V{BPAsm`A7T7cKU5?dZV|>}`T6 z0cWqXX$Ywfgdspo}+`nvw&XbmIoN$T%f1FK{`>)7oG?UnD1Xw849 zNgdSK-OIO2y~6WWB9xJKf2(DBm(0xSvgOs@{u=M`MlQzVoIQYXg(lfGxYr7T%{d^M z|CMrPqYP9vaHv;t5FpRC1SIYMvE#myb4lg8rEq%q1rdf#m}M zerU*U=zQ4vXlgD3qvXxNdG_>m#VR{SwI(4$<&8Ld*kv`C)RBS+tv!N(q0=3KgNCE zh7jS?Tc@>e&IafFd4P!~yG2x4K=(6#Wbq=_D(lT@AO&N=F`NviPMnyJP|DeD%Ro!Y zRbuLBdMa1Em!~^x^B7g)VH!QZL@D2kDG78-C_gG|4dmbW|MR6pf?UfK(73jT5Ouuj zO=IeA+Gc-`Y;XJBOK(l%U)O2+@7IFdwqnkYtJaEZeRcK3o9m30p;!Of=8POC0tCxPcny2bo%gAjVx=+YR09 zkIK}lv0N>iEVpp$K{6A2Uj1+Q=9FekN-$1TNu`X<_^@j&1qHC}Di`-4lyRl3vpa*D zm)-2}uL8C7#^Vx+4f9C06&C#B-T&PF=oP~otaLI0M)b)csb%DY9;W}{;o zrqp$qFy`SwSmG~H7qp=v@MoV#w-0DWHCFd&OCv-Lo7}EF|7ZKNYy*F(zT_~q9r=b; z4do5T1S)G}SVZvi_21Ax*O;C#pyQ4JEjgUB>z?K{_b7WM3SKnlWW{U->wiG}d0S_} z4;t^fLJN>&BQYUdp*=Gpm5289z}6D3@`UHz^L_j+(BRAt5u8LrN&90Ql9!|VR$IgY ztF)VKlB#fcr}sUiFWE@)Td{+gWpZ=k#8hth{q~GlmZ6d?fpv$P36Ddu2>5SNrK`JF z-t3Q*A#B4&J@LR_6n;vB5w1MF1#=8JSXXzv(utW^TgsKu9L!*l*Q_vmc7%s1PrSor`{znSiQ(HToMlx&O%XqeWBWy$@ow(c@^-k5$1&p> zkL1em`_SHL8g2?RFzeJ|?4+W{E+WUH-tN@=flyMeE4M1}(iE$q$B20KElFV-{@c?| zze?%b3t3blLdZ4*bg>({c7K0wxOYEUx22~HJ{V>2B2zrW9VD|1$ifI))c6RM`F-p+ zsQ42NJLEMPKHcRDcI=SsK=)n6)l}XOdPXmQH$AJac1xwsrkk`wU&~pMX^K^w$ktdv zCQ4UN{0;Hw$J<7&IX8C`8=%_J`>}t$*k#B~w5%(*LN~v>+qFqGYS~+Y3p|UHrdTURX*H^RzjKIof0rdxS2i4 z@=@X@Ad;nDi>aI=Y8h%jvmoUroc&*Jv2%jsz9S^33!h$3+{{_CQ5b*Kwr0{UK15uk zY1ki`k6uE$t_IJ82`Eq`2m3jvYDc9~h>XPomav$a69jH!32f{Mkm+#F}f zx($`uK&%LbipIo{_@>KtLdOfO$D5gM$E!?43wB?o6<2iT_MqK+$t{bIbygM2S<46o zpSw8UG+$EJqu4RFd273FKjh9?Eo}^2`8OLKsg@O`d+-y(YA*&}X35&s$_>TsWAqnGHab6WP%(wIW>bqk;k(X!KP}3V~eVQO#?2r(Sk#|qu z#*h1vYw!#xMSj2V#jsdnHGcsUM+m}sT6(7~i@ZQIjslh`KZN19D*9gY;q7c8&5;4B zfEi$Hr6GXKs0hRQSH;2w6?+wEFLU1#*e}eC1yli`;m^Z5R7RkrzJC6hqajHc zRzE~B;{Z@Sk6%6AAd}GXgOXOgfnAPQg?UOkq(SKk`X->e^6lw7?M7!B^>woE{jYi& z_}*1^F_@8}()enQm>AC1)e%(FVv6}vUv9im%n$!JeS&~#c~+MaCK7^yZ3r5^b0lMg zVxOoB@`#^H$H92Or_tFz7xisQ=))cR~&8m6#jLff3A9y|1dEX@45!0KVr9bC=`jzzLDHQ z{Q8iI*6p(8-^z!3A!tJA=KjJYbdokwE%4&Dv;m28)9gR^n{d%4o1Xbp*Ke^ddGCb+ zf^>w@&5q#O0YiT*gKD!LIjnhBX%>b|EKicu`1AHrqC96Cz8jgh(L;R zWCLtjpa|~Mmm~{{*n8Q1=1kxcd$`o4dHBvW$Ks*1{!UFaDIGe;vZ*{}G`%cL4s!kO8kOJ7pUK&2eoPICK_sm|!k0;u_G>H7tC!I!VzB zlaXO6)UZe}0l@z*clMb1I1aZAd>dUjFs*%ZghcEOBuW?0O!fD zMkRpJ{&7@aI_QtCHu&)%OX&c+drU?@0LFC-1D180qMZfjC}~1DOXtU-w(=?Gj)m4} z#A~s$RT)QH6I?j6mDz*rKzT{LtB{p2Cc&^&mTm>_`&;ZG%lra!SZQ3@&NdlZIz(Bc$-#)m{K+Hx6*QFyMH85x#sLKZXU zoqeK(!F3;h^%m$+dJzMlG6gbM@8oS(K!EUk~1&P)OtUkWy#kv-*ZM9S)EYm3hu zdBicSA0zBn2?=fCI&Y&z$O~cz_d-llKDjVrYl>}XtX~VID+nOfT?Oh!p*Xrw{u&XL z;)|#R!=~R4nwnqDGYGE3s0hH5{f)lP7pdVKOUhTtfMxU@AA}ck1A~5Ny_d<1%8g~1WfzI{ zy&|_(xO%+!diwu?fGp^+u@~4!>1uybo~xQ&)B*D`vfDleabh9+f3oK~KCMJ2)24=E z{)`ONoZe}&c&aSvWXA-cJY?hsow}~bY@&FSW!$b9zb0iqE;Pqo*X<1ZI|d%}*8vTL z>b&=p(k1Gimo;E%nI({5guk*~dAbhE>RVNUWUcqVN4jfSDOsRdN+2RI3V0~dpJUI7 zlS9q!-I<`@wS_~6HD!|&M`+tfc%r-JbQAd?fw`Gsh4~!fC&_Vo%d&?oO zNC_BbA7v@g{^o{edA|#^`&IK@)6lp_zHidoJI@}1i%?Xzb-T5s6zIOEKU+f~JrDyw zDIv$mc@D(%m?m0k`vVE(Jw-p182qg!XAv2=JAMON$bSH;W=& zTAAjfTb<-E-nSMRn@foKw2#!al)DVCZ&v-&-;^G0Ix|jn-9>t$xqzbi5paSqghv}5 zrfXo|F2|pQQ#4ncANu=}S3^g4MjeOUqPaYq8y*~Sf3fV&O_mO}#U4;$^OA228U8*y zf39>tH2J)b=W^@gwqgi>b2Pf> ztXWMOm1qOcyuiz2tgL$jvf-j!VK6JQxo(0SCmT0XbMQ=yY@;q!{M1^sYN>t=6;z8) zaMhfX+KuA zg>s=3nG~I_J<|V2S3s5pU-aQxd?80sSw4py%H^-=R zdbr5rlB|=gi@|4YtHtyc6U94@tblElgYs#{RV2wEWFmQOrbQd^6a)j{oxA6#xsi6X zk`z_GA7$wU3FcV|D#5fDuSgRC4_G3(-{@?NfH+LTdr)G@%5_Jey{0-Qi(|oBzlW>j zpG-eon>#_?_st{W)I{%1^zif~P$-RES5VCRHw1c?m^lADZ&^tp%7@Av$;qLIX_?77 z`rumtP3*mvcn+_YKoOWn?5$vB?-9^|i6kvxyChMXOW0nP^$)Sl&e)8MUWm;~a*u^HLqhLDW=QE}|R7fC(4fvV8;w92~!-YQ$t8lKYm2 zyO*=1akYA%lmyvhC+9?oK&1BruP|mj*5VS+S2Z|YoD$q-kH;F(J6`bJC!TFKfB29& zDPmaW`W{ZE3)H!5>=W_J2>52)#E!!w@zsnZVpYg~{SYQBGYGoWlioi{`haSGhnaW( zK+6>P82_Y?*x_)>S*R+dnKnmi*!YWHiAqa3Im(D$;@iChQq&OXX>p+e2HWkB+E<~(r%gT;K#*EhG0vkIIVzSy*wze z%lEQPw!pA|PUj5UEr{V4nsM2Kh8z`8wM==XUX|__t{(^(3BBS31mpV zvC7x<*TLo54Di>ZVIDyK zMt+%NA~}ExOH#fRiYmrshS3iMZU1gOketOwxA;CR7P*&s*1Yi?7*<{c?%;#!B#?3G~uKEb&9<>p58MEK30Y2)dvfzXJ%g?l3n2(s5)EfVx6fm4NmjROi9S zwKDc;(D!6Rw*2S{GY&B8LpwyGs3O)uT#&Y|_aJCC=JoV@ja{I2_zaR2#X!QbYKIys z-#oR?C)q%Vz1trY;Z_2_gJQz^9Q2 zm(IC*zP_;<2N;cJ0b4)1L}b>OJrFFwh;5nNj;5sg(;E1uyUZ%bb)Im{tc}=fOskMihb5BBP3yd{{ZGY%!*ewhRd~^~Y_WDgx!R!{h_4|LyEkSy#{U zoUlC~pW37B8#n&YT6PE%-BngX5-eYI%-z=@f$YUt#_Ab!nRc;)InczsW5Y;CM_vd$uJmPf?{n7foQ z(}Q)+M+b)slzrn>TDLo9oWfMdT{l7evpQ z{8&!6DCM|Do0vuzIf`zLh?h~HHPA6c*k$6aTDCdXL7(LZh-q*kNA?|DobW_MDo-ni znE;p&khYGZB;~FQXXHKwRl$+|uv8bF>^?@#dV)r{NQfoik)6vZZOOf}_JkJBraRaX z0h+K}Z^Gi39iY$h#e@ZwMuk^yJ&7T6Oj0$~eQ2;;P+Vxd&FlT3yaHwO3w7@cQ7 z4GWy7r7=81Kmh2uARL2H`oDKYK7*G*7$@=Wd8SFUV4ply8^G8w4F!jdmrY zQ+pI>*0)Q-yQNFBzl7~(dYs(l2y0JmPp3c0{9xYXxR6xaE9BGmo!!{tzIES?Pcg&$ zQMqBQuMAD&_+lT0Id!@BzEwt>jW%mWr_<+B+59u9f{+OJ3tW4`(M&aZPNP-*(fV;d zPoOp=LHWZ0?V)GMyQ7lNlCJ$7F=Q!gEm>wwxCzzFTKyA!bJ;Q^?Hs`$tnz$YbkKj& zR#oKJG!luMoma51LIPlDVNK!31&lv`+WA9YN;0p@z&8w|*bqhqu8CmKyPp z-W(b@TTkqCs=>r3fn4LG@3OeVc_IAUn`pMh-mkIJ7Qp(XdGLU-Jd5kZ%)eX}J0)9k z*4dzR)o^p8J_03~BZLCp@Zj!6=bgH7jPb7KLS!1Om0A_Yu$=m`7y7U+d`!+y44@+}PoHnNMo|Y?&j3X2B-Ejk_->EX%0lj*OD6 z;U!481vxPZJ!<#@wxK)4%sO=yas#T+E4`D{2c}Kq zln$S>>*EWqQ&|QalvlY5Lr#pU?iZdnkHlUSu9

s4&bcDbHgys`Q8|Ssnb#9qa?* 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 ) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index 0b252729a99667..e6511f5e7bd98a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -266,7 +266,7 @@ export function SuggestionPanel({ }} > {i18n.translate('xpack.lens.sugegstion.confirmSuggestionLabel', { - defaultMessage: 'Confirm and reload suggestions', + defaultMessage: 'Reload suggestions', })} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 86a0e5c8a833ac..fc224db743dca1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; - import { ExpressionRendererProps } from '../../../../../../../src/legacy/core_plugins/expressions/public'; import { Visualization, FramePublicAPI, TableSuggestion } from '../../types'; import { @@ -20,6 +19,7 @@ import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { Ast } from '@kbn/interpreter/common'; +import { coreMock } from 'src/core/public/mocks'; const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); @@ -59,10 +59,11 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(1); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -80,10 +81,11 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(1); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -101,10 +103,11 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); - expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(1); + expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -136,6 +139,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -230,6 +234,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -307,6 +312,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -355,6 +361,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -394,6 +401,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -438,6 +446,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -485,6 +494,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); @@ -542,6 +552,7 @@ describe('workspace_panel', () => { visualizationState={{}} dispatch={mockDispatch} ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} /> ); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 314b10796435bc..6d0ab402a29710 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -6,8 +6,9 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiImage, EuiText } from '@elastic/eui'; import { toExpression } from '@kbn/interpreter/common'; +import { CoreStart, CoreSetup } from 'src/core/public'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public'; import { Action } from './state_management'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; @@ -32,6 +33,7 @@ export interface WorkspacePanelProps { framePublicAPI: FramePublicAPI; dispatch: (action: Action) => void; ExpressionRenderer: ExpressionRenderer; + core: CoreStart | CoreSetup; } export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel); @@ -46,8 +48,14 @@ export function InnerWorkspacePanel({ datasourceStates, framePublicAPI, dispatch, + core, ExpressionRenderer: ExpressionRendererComponent, }: WorkspacePanelProps) { + const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); + const emptyStateGraphicURL = IS_DARK_THEME + ? '/plugins/lens/assets/lens_app_graphic_dark_2x.png' + : '/plugins/lens/assets/lens_app_graphic_light_2x.png'; + const dragDropContext = useContext(DragContext); const suggestionForDraggedField = useMemo(() => { @@ -87,12 +95,25 @@ export function InnerWorkspacePanel({ function renderEmptyWorkspace() { return ( -

- +

+ +

+ -

+

+ +

+ ); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss index 4671d779833af7..24ccef4c081aa6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss @@ -23,9 +23,14 @@ flex-grow: 0; } +/** + * 1. Don't cut off the shadow of the field items + */ + .lnsInnerIndexPatternDataPanel__listWrapper { @include euiOverflowShadow; @include euiScrollBar; + margin-left: -$euiSize; /* 1 */ position: relative; flex-grow: 1; overflow: auto; @@ -35,8 +40,8 @@ padding-top: $euiSizeS; position: absolute; top: 0; - left: 0; - right: 0; + left: $euiSize; /* 1 */ + right: $euiSizeXS; /* 1 */ } .lnsInnerIndexPatternDataPanel__filterButton { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss index 9eab50dd139d8a..54f9a3787466d3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss @@ -1,27 +1,61 @@ .lnsFieldItem { @include euiFontSizeS; - background: $euiColorEmptyShade; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); border-radius: $euiBorderRadius; margin-bottom: $euiSizeXS; - transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance; - - &:hover { - @include euiBottomShadowMedium; - z-index: 2; - cursor: grab; - } } -.lnsFieldItem--missing { - background: $euiColorLightestShade; +.lnsFieldItem__popoverAnchor:hover, +.lnsFieldItem__popoverAnchor:focus, +.lnsFieldItem__popoverAnchor:focus-within { + @include euiBottomShadowMedium; + border-radius: $euiBorderRadius; + z-index: 2; } -.lnsFieldItem__name { - margin-left: $euiSizeXS; +.lnsFieldItem--missing { + background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade); + color: $euiColorDarkShade; } .lnsFieldItem__info { + border-radius: $euiBorderRadius - 1px; padding: $euiSizeS; + display: flex; + align-items: flex-start; + transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance, + background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation + + .lnsFieldItem__name { + margin-left: $euiSizeXS; + flex-grow: 1; + } + + .lnsFieldListPanel__fieldIcon, + .lnsFieldItem__infoIcon { + flex-shrink: 0; + } + + .lnsFieldListPanel__fieldIcon { + margin-top: 2px; + } + + .lnsFieldItem__infoIcon { + visibility: hidden; + } + + &:hover, + &:focus { + cursor: grab; + + .lnsFieldItem__infoIcon { + visibility: visible; + } + } +} + +.lnsFieldItem__info-isOpen { + @include euiFocusRing; } .lnsFieldItem__topValue { @@ -45,3 +79,8 @@ min-width: 260px; max-width: 300px; } + +.lnsFieldItem__popoverButtonGroup { + // Enforce lowercase for buttons or else some browsers inherit all caps from popover title + text-transform: none; +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index 85996659620e7d..da42113b4e7b4d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -492,16 +492,20 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ })} {!localState.isLoading && paginatedFields.length === 0 && ( - - {showEmptyFields - ? i18n.translate('xpack.lens.indexPatterns.hiddenFieldsLabel', { - defaultMessage: - 'No fields have data with the current filters. You can show fields without data using the filters above.', - }) - : i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', { - defaultMessage: 'No fields can be visualized from {title}', - values: { title: currentIndexPattern.title }, - })} + +

+ + {showEmptyFields + ? i18n.translate('xpack.lens.indexPatterns.hiddenFieldsLabel', { + defaultMessage: + 'No fields have data with the current filters. You can show fields without data using the filters above.', + }) + : i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', { + defaultMessage: 'No fields in {title} can be visualized.', + values: { title: currentIndexPattern.title }, + })} + +

)}
diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 33567b20f364db..fecf2792da1c7f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -96,7 +96,7 @@ describe('IndexPatternDimensionPanel', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, @@ -280,7 +280,7 @@ describe('IndexPatternDimensionPanel', () => { 'Incompatible' ); - expect(options.find(({ name }) => name === 'Date Histogram')!['data-test-subj']).toContain( + expect(options.find(({ name }) => name === 'Date histogram')!['data-test-subj']).toContain( 'Incompatible' ); }); @@ -822,7 +822,7 @@ describe('IndexPatternDimensionPanel', () => { .find(EuiSideNav) .prop('items')[0] .items.map(({ name }) => name) - ).toEqual(['Unique count', 'Average', 'Count', 'Filter Ratio', 'Maximum', 'Minimum', 'Sum']); + ).toEqual(['Unique count', 'Average', 'Count', 'Filter ratio', 'Maximum', 'Minimum', 'Sum']); }); it('should add a column on selection of a field', () => { @@ -973,7 +973,7 @@ describe('IndexPatternDimensionPanel', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index ec2c153931bd14..494a0ae9fb1e35 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -330,28 +330,21 @@ export function PopoverEditor(props: PopoverEditorProps) { -

- -

-
+ iconType="sortUp" + /> )} {incompatibleSelectedOperationType && !selectedColumn && ( )} {!incompatibleSelectedOperationType && ParamEditor && ( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx index 8fd72a790b38e5..85c1deb0ea7e1a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx @@ -48,7 +48,7 @@ describe('FieldIcon', () => { `); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx index d68ee0b82f28ec..f1e8db04860a7d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx @@ -17,7 +17,7 @@ function getIconForDataType(dataType: string) { const icons: Partial>> = { boolean: 'invert', date: 'calendar', - ip: 'storage', + ip: 'ip', }; return icons[dataType] || ICON_TYPES.find(t => t === dataType) || 'empty'; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 87ef874dece66b..62591bdf1e0815 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -18,6 +18,7 @@ import { EuiButtonGroup, EuiPopoverFooter, EuiPopoverTitle, + EuiIconTip, } from '@elastic/eui'; import { Chart, @@ -144,6 +145,7 @@ export function FieldItem(props: FieldItemProps) { return ( ('.application') || undefined} button={ @@ -157,7 +159,7 @@ export function FieldItem(props: FieldItemProps) { >
{ togglePopover(); @@ -167,12 +169,9 @@ export function FieldItem(props: FieldItemProps) { togglePopover(); } }} - title={i18n.translate('xpack.lens.indexPattern.fieldStatsButton', { - defaultMessage: 'Click or Enter for more information about {fieldName}', - values: { fieldName: field.name }, - })} - aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButton', { - defaultMessage: 'Click or Enter for more information about {fieldName}', + aria-label={i18n.translate('xpack.lens.indexPattern.fieldStatsButtonAriaLabel', { + defaultMessage: + 'Click or press Enter for information about {fieldName}. Or, drag field into visualization.', values: { fieldName: field.name }, })} > @@ -181,6 +180,18 @@ export function FieldItem(props: FieldItemProps) { {wrappableHighlightableFieldName} + +
@@ -260,7 +271,8 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { if (histogram && histogram.buckets.length && topValues && topValues.buckets.length) { title = ( {i18n.translate('xpack.lens.indexPattern.fieldTopValuesLabel', { - defaultMessage: 'Top Values', + defaultMessage: 'Top values', })} ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index 79847223631498..841d59b602ee8c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -1037,7 +1037,7 @@ describe('IndexPattern Data Source suggestions', () => { { columnId: 'col2', operation: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, scale: 'interval', @@ -1113,7 +1113,7 @@ describe('IndexPattern Data Source suggestions', () => { { columnId: 'newCol', operation: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, scale: 'interval', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx index 9558a141ad7a00..ba48a3c2f20325 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx @@ -31,7 +31,7 @@ const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< function ofName(name: string) { return i18n.translate('xpack.lens.indexPattern.dateHistogramOf', { - defaultMessage: 'Date Histogram of {name}', + defaultMessage: 'Date histogram of {name}', values: { name }, }); } @@ -51,7 +51,7 @@ export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternC export const dateHistogramOperation: OperationDefinition = { type: 'date_histogram', displayName: i18n.translate('xpack.lens.indexPattern.dateHistogram', { - defaultMessage: 'Date Histogram', + defaultMessage: 'Date histogram', }), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx index fb12910b7517d2..7c01a34fca2db4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx @@ -44,7 +44,7 @@ describe('filter_ratio', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Filter Ratio', + label: 'Filter ratio', dataType: 'number', isBucketed: false, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx index 63c6398e93997d..a6689210be858b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx @@ -18,7 +18,7 @@ import { OperationDefinition } from '.'; import { BaseIndexPatternColumn } from './column_types'; const filterRatioLabel = i18n.translate('xpack.lens.indexPattern.filterRatio', { - defaultMessage: 'Filter Ratio', + defaultMessage: 'Filter ratio', }); export interface FilterRatioIndexPatternColumn extends BaseIndexPatternColumn { @@ -35,7 +35,7 @@ export const filterRatioOperation: OperationDefinition { return { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx index 31f70eb6fb3bba..b26608086bd690 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx @@ -51,7 +51,7 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn { export const termsOperation: OperationDefinition = { type: 'terms', displayName: i18n.translate('xpack.lens.indexPattern.terms', { - defaultMessage: 'Top Values', + defaultMessage: 'Top values', }), getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { if ( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index 736a6f712d3468..6307a8f0f68d16 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -154,7 +154,7 @@ describe('getOperationTypesForField', () => { columnOrder: ['col1'], columns: { col1: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index 9023173ab95dfe..1b98fa2b300054 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -367,7 +367,7 @@ describe('state_helpers', () => { expect( getColumnOrder({ col1: { - label: 'Top Values of category', + label: 'Top values of category', dataType: 'string', isBucketed: true, @@ -392,7 +392,7 @@ describe('state_helpers', () => { sourceField: 'bytes', }, col3: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, @@ -411,7 +411,7 @@ describe('state_helpers', () => { expect( getColumnOrder({ col1: { - label: 'Top Values of category', + label: 'Top values of category', dataType: 'string', isBucketed: true, @@ -438,7 +438,7 @@ describe('state_helpers', () => { suggestedPriority: 0, }, col3: { - label: 'Date Histogram of timestamp', + label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, From 68f20dcc5bac644a52e53f7cb5724964df0f6ed5 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Wed, 2 Oct 2019 09:39:05 -0400 Subject: [PATCH 37/53] [Maps] More compressed forms (#47043) * Adjusting vector size selectors * Degrees * Button group for symbol type * i18n and snaps * Input with popoper and icons * Improving layer settings styles * Column layout * Changing step * Fixed up the Go To and Draw popovers * Removing unecessary css * Improving source settings styles * Improving design to avoid panel inside panel * Remove unused translations * Update x-pack/legacy/plugins/maps/public/components/metrics_editor.js Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * Update x-pack/legacy/plugins/maps/public/components/_index.scss Co-Authored-By: Caroline Horn <549577+cchaos@users.noreply.github.com> * Oh snap --- .../geometry_filter_form.test.js.snap | 72 ++++++----- .../maps/public/components/_index.scss | 20 ++++ .../public/components/geometry_filter_form.js | 23 ++-- .../maps/public/components/metric_editor.js | 9 +- .../maps/public/components/metrics_editor.js | 37 +++--- .../layer_panel/_index.scss | 2 +- .../layer_settings/layer_settings.js | 62 ++++++---- .../set_view_control/set_view_control.js | 32 +++-- .../__snapshots__/tools_control.test.js.snap | 8 +- .../tools_control/tools_control.js | 80 ++++++------- .../update_source_editor.js | 2 +- .../vector_style_symbol_editor.test.js.snap | 112 +++++++++--------- .../components/vector/color/_color_stops.scss | 2 +- .../static_orientation_selection.js | 1 + .../vector/size/size_range_selector.js | 7 +- .../vector/size/static_size_selection.js | 5 + .../vector/vector_style_symbol_editor.js | 62 +++++----- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 19 files changed, 303 insertions(+), 237 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 5e9500f65d167f..053863c68775f6 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -2,9 +2,6 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` - - + - Create filter - + + Create filter + + `; exports[`should not show "within" relation when filter geometry is not closed 1`] = ` - - + - Create filter - + + Create filter + + `; exports[`should render relation select when geo field is geo_shape 1`] = ` - - + - Create filter - + + Create filter + + `; diff --git a/x-pack/legacy/plugins/maps/public/components/_index.scss b/x-pack/legacy/plugins/maps/public/components/_index.scss index 3e13cd3755dd33..6f180b840b877c 100644 --- a/x-pack/legacy/plugins/maps/public/components/_index.scss +++ b/x-pack/legacy/plugins/maps/public/components/_index.scss @@ -2,6 +2,26 @@ margin-bottom: $euiSizeS; } +.mapMetricEditorPanel__metricEditor { + padding: $euiSizeM 0; + border-top: $euiBorderThin; + + &:first-child { + padding-top: 0; + border-top: none; + } + + &:last-child { + margin-bottom: $euiSizeM; + border-bottom: 1px solid $euiColorLightShade; + } +} + +.mapMetricEditorPanel__metricRemoveButton { + padding-top: $euiSizeM; + text-align: right; +} + .mapGeometryFilter__geoFieldSuperSelect { height: $euiSizeL * 2; } diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js index 2c518a856f2390..10410f4d79e5b5 100644 --- a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js @@ -16,6 +16,7 @@ import { EuiButton, EuiSelect, EuiSpacer, + EuiTextAlign, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; @@ -154,8 +155,6 @@ export class GeometryFilterForm extends Component { }); return ( - - - {this.props.buttonLabel} - + + + + + {this.props.buttonLabel} + + ); } diff --git a/x-pack/legacy/plugins/maps/public/components/metric_editor.js b/x-pack/legacy/plugins/maps/public/components/metric_editor.js index f2f309d0512132..65f345925fd84d 100644 --- a/x-pack/legacy/plugins/maps/public/components/metric_editor.js +++ b/x-pack/legacy/plugins/maps/public/components/metric_editor.js @@ -43,7 +43,7 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu label={i18n.translate('xpack.maps.metricsEditor.selectFieldLabel', { defaultMessage: 'Field', })} - display="rowCompressed" + display="columnCompressed" > {fieldSelect} - {labelInput} + {removeButton} ); } diff --git a/x-pack/legacy/plugins/maps/public/components/metrics_editor.js b/x-pack/legacy/plugins/maps/public/components/metrics_editor.js index ebb3904a447991..53d078369cf89f 100644 --- a/x-pack/legacy/plugins/maps/public/components/metrics_editor.js +++ b/x-pack/legacy/plugins/maps/public/components/metrics_editor.js @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonIcon, EuiButtonEmpty, EuiPanel, EuiSpacer, EuiTextAlign } from '@elastic/eui'; +import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui'; import { MetricEditor } from './metric_editor'; export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) { @@ -25,23 +25,26 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, let removeButton; if (index > 0) { removeButton = ( - +
+ + + +
); } return ( - +
- +
); }); } @@ -80,7 +83,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, return ( - {renderMetrics()} +
{renderMetrics()}
{renderAddMetricButton()}
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss index 5412a05e7dd7b8..b219f59476ce9f 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss @@ -1,3 +1,3 @@ @import './layer_panel'; @import './filter_editor/filter_editor'; -@import './join_editor/resources/join'; +@import './join_editor/resources/join'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js index c0dbb1ec8fcbab..5be842f949871b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -36,7 +36,9 @@ export function LayerSettings(props) { }; const onAlphaChange = alpha => { - props.updateAlpha(props.layerId, alpha); + const alphaDecimal = alpha / 100; + + props.updateAlpha(props.layerId, alphaDecimal); }; const onApplyGlobalQueryChange = event => { @@ -47,18 +49,21 @@ export function LayerSettings(props) { return ( ); }; @@ -67,9 +72,9 @@ export function LayerSettings(props) { return ( @@ -77,23 +82,28 @@ export function LayerSettings(props) { }; const renderAlphaSlider = () => { + const alphaPercent = Math.round(props.alpha * 100); + return ( ); @@ -103,15 +113,23 @@ export function LayerSettings(props) { const layerSupportsGlobalQuery = props.layer.getIndexPatternIds().length; const applyGlobalQueryCheckbox = ( - + display="columnCompressedSwitch" + > + +
); if (layerSupportsGlobalQuery) { @@ -146,8 +164,6 @@ export function LayerSettings(props) { {renderLabel()} {renderZoomSliders()} {renderAlphaSlider()} - - {renderApplyGlobalQueryCheckbox()} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js index f1d055d357b24f..05ec23d10795a4 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js @@ -13,6 +13,8 @@ import { EuiFieldNumber, EuiButtonIcon, EuiPopover, + EuiTextAlign, + EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -72,7 +74,7 @@ export class SetViewControl extends Component { return { isInvalid, component: ( - + + {latFormRow} {lonFormRow} {zoomFormRow} - - - + + + + + + + ); } @@ -149,6 +158,7 @@ export class SetViewControl extends Component { return ( , "id": 1, - "title": "Draw shape to filter data", + "title": "Draw shape", }, Object { "content": , "id": 2, - "title": "Draw bounds to filter data", + "title": "Draw bounds", }, ] } @@ -166,7 +166,7 @@ exports[`renders 1`] = ` onSubmit={[Function]} />, "id": 1, - "title": "Draw shape to filter data", + "title": "Draw shape", }, Object { "content": , "id": 2, - "title": "Draw bounds to filter data", + "title": "Draw bounds", }, ] } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index 722c615ea43242..ea6ffe3ba14355 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -26,10 +26,17 @@ const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLa defaultMessage: 'Draw bounds to filter data', }); -export class ToolsControl extends Component { +const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', { + defaultMessage: 'Draw shape', +}); + +const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabelShort', { + defaultMessage: 'Draw bounds', +}); +export class ToolsControl extends Component { state = { - isPopoverOpen: false + isPopoverOpen: false, }; _togglePopover = () => { @@ -42,18 +49,18 @@ export class ToolsControl extends Component { this.setState({ isPopoverOpen: false }); }; - _initiateShapeDraw = (options) => { + _initiateShapeDraw = options => { this.props.initiateDraw({ drawType: DRAW_TYPE.POLYGON, - ...options + ...options, }); this._closePopover(); - } + }; - _initiateBoundsDraw = (options) => { + _initiateBoundsDraw = options => { this.props.initiateDraw({ drawType: DRAW_TYPE.BOUNDS, - ...options + ...options, }); this._closePopover(); }; @@ -68,48 +75,50 @@ export class ToolsControl extends Component { items: [ { name: DRAW_SHAPE_LABEL, - panel: 1 + panel: 1, }, { name: DRAW_BOUNDS_LABEL, - panel: 2 - } - ] + panel: 2, + }, + ], }, { id: 1, - title: DRAW_SHAPE_LABEL, + title: DRAW_SHAPE_LABEL_SHORT, content: ( - ) + ), }, { id: 2, - title: DRAW_BOUNDS_LABEL, + title: DRAW_BOUNDS_LABEL_SHORT, content: ( - ) - } + ), + }, ]; } @@ -141,10 +150,7 @@ export class ToolsControl extends Component { withTitle anchorPosition="leftUp" > - + ); @@ -154,15 +160,9 @@ export class ToolsControl extends Component { return ( + {toolsPopoverButton} - {toolsPopoverButton} - - - + - + {this._renderMetricsEditor()} ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap index f83240e3f70d01..97706169d98bf7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/__snapshots__/vector_style_symbol_editor.test.js.snap @@ -1,34 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Should render icon select when symbolized as Icon 1`] = ` - - + + hasEmptyLabelSpace={false} + label="Symbol type" + labelType="label" + > + + @@ -60,37 +60,37 @@ exports[`Should render icon select when symbolized as Icon 1`] = ` } singleSelection={true} /> - + `; exports[`Should render symbol select when symbolized as Circle 1`] = ` - - + - + hasEmptyLabelSpace={false} + label="Symbol type" + labelType="label" + > + + + `; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/color/_color_stops.scss b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/color/_color_stops.scss index 7d6c6ede0d3307..eab7896650772b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/color/_color_stops.scss +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/color/_color_stops.scss @@ -1,6 +1,6 @@ .mapColorStop { position: relative; - padding-right: $euiSizeXL + $euiSizeXS; + padding-right: $euiSizeXL + $euiSizeS; & + & { margin-top: $euiSizeS; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/orientation/static_orientation_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/orientation/static_orientation_selection.js index 8a335822038876..b5529c69874598 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/orientation/static_orientation_selection.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/orientation/static_orientation_selection.js @@ -23,6 +23,7 @@ export function StaticOrientationSelection({ onChange, styleOptions }) { showInput showLabels compressed + append="°" /> ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/size_range_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/size_range_selector.js index fd1ce0ffe89a88..31b9b4f5ad6498 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/size_range_selector.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/size_range_selector.js @@ -8,6 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { ValidatedDualRange } from 'ui/validated_range'; import { DEFAULT_MIN_SIZE, DEFAULT_MAX_SIZE } from '../../../vector_style_defaults'; +import { i18n } from '@kbn/i18n'; export function SizeRangeSelector({ minSize, maxSize, onChange, ...rest }) { const onSizeChange = ([min, max]) => { @@ -23,10 +24,14 @@ export function SizeRangeSelector({ minSize, maxSize, onChange, ...rest }) { max={DEFAULT_MAX_SIZE} step={1} value={[minSize, maxSize]} - showInput + showInput="inputWithPopover" showRange onChange={onSizeChange} allowEmptyRange={false} + append={i18n.translate('xpack.maps.vector.dualSize.unitLabel', { + defaultMessage: 'px', + description: 'Shorthand for pixel', + })} {...rest} /> ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/static_size_selection.js b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/static_size_selection.js index 507f81ade93092..38f8fe53d17488 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/static_size_selection.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/size/static_size_selection.js @@ -8,6 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { staticSizeShape } from '../style_option_shapes'; import { ValidatedRange } from '../../../../../components/validated_range'; +import { i18n } from '@kbn/i18n'; export function StaticSizeSelection({ onChange, styleOptions }) { const onSizeChange = size => { @@ -23,6 +24,10 @@ export function StaticSizeSelection({ onChange, styleOptions }) { showInput showLabels compressed + append={i18n.translate('xpack.maps.vector.size.unitLabel', { + defaultMessage: 'px', + description: 'Shorthand for pixel', + })} /> ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_symbol_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_symbol_editor.js index 0fcbe7653c3deb..268b1f39255b94 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_symbol_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_symbol_editor.js @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiSelect, + EuiButtonGroup, EuiSpacer, EuiComboBox, } from '@elastic/eui'; @@ -21,14 +21,14 @@ import { SymbolIcon } from './legend/symbol_icon'; const SYMBOLIZE_AS_OPTIONS = [ { - value: SYMBOLIZE_AS_CIRCLE, - text: i18n.translate('xpack.maps.vector.symbolAs.circleLabel', { + id: SYMBOLIZE_AS_CIRCLE, + label: i18n.translate('xpack.maps.vector.symbolAs.circleLabel', { defaultMessage: 'circle marker', }), }, { - value: SYMBOLIZE_AS_ICON, - text: i18n.translate('xpack.maps.vector.symbolAs.IconLabel', { + id: SYMBOLIZE_AS_ICON, + label: i18n.translate('xpack.maps.vector.symbolAs.IconLabel', { defaultMessage: 'icon', }), }, @@ -41,26 +41,27 @@ export function VectorStyleSymbolEditor({ isDarkMode, }) { const renderSymbolizeAsSelect = () => { - const selectedOption = SYMBOLIZE_AS_OPTIONS.find(({ value }) => { - return value === styleOptions.symbolizeAs; + const selectedOption = SYMBOLIZE_AS_OPTIONS.find(({ id }) => { + return id === styleOptions.symbolizeAs; }); - const onSymbolizeAsChange = e => { + const onSymbolizeAsChange = optionId => { const styleDescriptor = { options: { ...styleOptions, - symbolizeAs: e.target.value, + symbolizeAs: optionId, }, }; handlePropertyChange('symbol', styleDescriptor); }; return ( - ); }; @@ -113,28 +114,23 @@ export function VectorStyleSymbolEditor({ ); }; - const renderFormRowContent = () => { - if (styleOptions.symbolizeAs === SYMBOLIZE_AS_CIRCLE) { - return renderSymbolizeAsSelect(); - } - - return ( - + return ( + + {renderSymbolizeAsSelect()} - - {renderSymbolSelect()} - - ); - }; + - return ( - - {renderFormRowContent()} - + {styleOptions.symbolizeAs !== SYMBOLIZE_AS_CIRCLE && ( + + + {renderSymbolSelect()} + + )} + ); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 16a338e2039821..a821a66076bae9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5872,9 +5872,7 @@ "xpack.maps.source.wms.attributionText": "属性 URL にはテキストが必要です", "xpack.maps.style.customColorRampLabel": "カスタマカラーランプ", "xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel": "境界", - "xpack.maps.toolbarOverlay.drawBounds.onSubmitButtonLabel": "境界を描く", "xpack.maps.toolbarOverlay.drawShape.initialGeometryLabel": "図形", - "xpack.maps.toolbarOverlay.drawShape.onSubmitButtonLabel": "図形を描く", "xpack.maps.tooltip.geometryFilterForm.createFilterButtonLabel": "フィルターを作成", "xpack.maps.tooltip.pageNumerText": "{total} 個中 {pageNumber} 個の機能", "xpack.maps.tooltip.showGeometryFilterViewLinkLabel": "ジオメトリでフィルタリング", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d9988b34b272d9..1f856731d43e19 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5875,9 +5875,7 @@ "xpack.maps.source.wms.attributionText": "属性 url 必须附带文本", "xpack.maps.style.customColorRampLabel": "定制颜色渐变", "xpack.maps.toolbarOverlay.drawBounds.initialGeometryLabel": "边界", - "xpack.maps.toolbarOverlay.drawBounds.onSubmitButtonLabel": "绘制边界", "xpack.maps.toolbarOverlay.drawShape.initialGeometryLabel": "形状", - "xpack.maps.toolbarOverlay.drawShape.onSubmitButtonLabel": "绘制形状", "xpack.maps.tooltip.geometryFilterForm.createFilterButtonLabel": "创建筛选", "xpack.maps.tooltip.pageNumerText": "第 {pageNumber} 项功能,总计 {total} 项", "xpack.maps.tooltip.showGeometryFilterViewLinkLabel": "按几何筛选", From d3fb7b8d3c6cebda01830e67dd988ffe62648c04 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 2 Oct 2019 10:29:45 -0400 Subject: [PATCH 38/53] [SR] render alert icon in policy table if last snapshot failed (#46960) --- .../public/app/sections/home/_home.scss | 17 ++++++++- .../policy_list/policy_table/policy_table.tsx | 36 ++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss index c714222daa98b5..741ee76985937e 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/_home.scss @@ -28,4 +28,19 @@ .euiToolTipAnchor { display: flex; } -} \ No newline at end of file +} + +/* + * Wraps long snapshot name with ellipsis when it is rendered with an icon + */ +.snapshotRestorePolicyTableSnapshotFailureContainer { + max-width: 200px; + > .euiFlexItem:last-child { + min-width: 0; + .euiText { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } +} diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx index a47b670177ca83..19239a282eb296 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx @@ -14,6 +14,8 @@ import { EuiToolTip, EuiButtonIcon, EuiLoadingSpinner, + EuiText, + EuiIcon, } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; @@ -94,8 +96,40 @@ export const PolicyTable: React.FunctionComponent = ({ name: i18n.translate('xpack.snapshotRestore.policyList.table.snapshotNameColumnTitle', { defaultMessage: 'Snapshot name', }), - truncateText: true, sortable: true, + render: ( + snapshotName: SlmPolicy['snapshotName'], + { lastFailure, lastSuccess }: SlmPolicy + ) => { + // Alert user if last snapshot failed + if (lastSuccess && lastFailure && lastFailure.time > lastSuccess.time) { + return ( + + + + + + + + {snapshotName} + + + ); + } + return snapshotName; + }, }, { field: 'repository', From 58c1f879f7763fb46af6f2644040ca1f39b2976f Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Wed, 2 Oct 2019 11:06:14 -0400 Subject: [PATCH 39/53] [SIEM] Table Styles & Markup Tweaks (#46300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * restore conditional space before AS number * touchup table widths and text * adjust datepicker width * refactor matchMedia; set bp to above mbp rez * timeline table body refactor, first pass * TruncatableText: rm “width”, added “truncated” * cleanup imports * cleanup styles * rm size prop * swap out div? prob need to fix ref * restore truncation in timeline * think i have text overflow and tooltips happy now * light clean up * single overflow scrolling element * use polished for hex in rgba needs * simplify body markup * events table header poc * close button fixes * improve sort indicator position * drag handle updates * fix fields browser positioning * apply aria roles * fix blown out table width * localize sorting onClick to header text * correct key placement * prevent hover and click for unsortable and add btn * rm btn for non aggregatable col headers * change width/height prop names to avoid html attr * fix loading alignment * account for action cell width when one action * clean up trGroup organization * imports cleanup * fix types and skeleton rows/cells poc * new skeleton row comp * fix column heads not dragging * supplement row indentation * move widths out of styled components for perf * inline dynamic width * account for inline styles with dnd * cleanup * tweak in-page events table styling for consistency * cleanup * make compressed for consistency * cleanup * update jest tests + change matchMedia to css mq * fix missing grab cursor in IE11 * fix action td group width in IE11 * fix columns menu positioning in IE11 * fix collapsing notes in timelines table in IE11 * decouple from DroppableWrapper to prevent issues * update snapshots * more specific selector * rm show prop * add truncate to shouldComponentUpdate * bulk up `HeaderPanel` unit tests * correct conditional styles and add some more tests * improve SkeletonRow unit tests * change for loop to map * switch from pure to React.memo * make SkeletonRow cellCount dynamic * rm comments * fix buttons not being draggable for col headers * fix for safari position sticky + overflow auto bug * correct type errors * correct field browser overlap * missing semicolon --- .../drag_and_drop/draggable_wrapper.test.tsx | 9 +- .../drag_and_drop/draggable_wrapper.tsx | 96 +- .../drag_and_drop/droppable_wrapper.tsx | 13 +- .../components/drag_and_drop/helpers.ts | 9 - .../draggables/field_badge/index.tsx | 67 +- .../components/event_details/columns.tsx | 29 +- .../event_details/event_details.tsx | 10 +- .../events_viewer/events_viewer.test.tsx | 2 +- .../events_viewer/events_viewer.tsx | 40 +- .../events_viewer_header.test.tsx | 79 -- .../events_viewer/events_viewer_header.tsx | 51 - .../field_renderers/field_renderers.tsx | 9 +- .../fields_browser/category_columns.tsx | 7 +- .../fields_browser/field_browser.tsx | 33 +- .../components/fields_browser/field_items.tsx | 24 +- .../components/fields_browser/field_name.tsx | 3 +- .../components/fields_browser/index.test.tsx | 2 +- .../components/fields_browser/index.tsx | 39 +- .../components/fields_browser/translations.ts | 2 +- .../filters_global/filters_global.tsx | 4 +- .../public/components/flyout/button/index.tsx | 9 +- .../public/components/formatted_ip/index.tsx | 41 +- .../__snapshots__/header_panel.test.tsx.snap | 13 - .../__snapshots__/index.test.tsx.snap | 9 + .../header_panel/header_panel.test.tsx | 22 - .../components/header_panel/header_panel.tsx | 77 -- .../components/header_panel/index.test.tsx | 160 +++ .../public/components/header_panel/index.tsx | 82 +- .../siem/public/components/loader/index.tsx | 12 +- .../components/ml/tables/translations.ts | 2 +- .../components/notes/note_cards/index.tsx | 32 +- .../components/open_timeline/index.test.tsx | 2 +- .../open_timeline/timelines_table/index.tsx | 12 +- .../open_timeline/title_row/index.test.tsx | 2 +- .../components/page/add_to_kql/index.tsx | 1 - .../page/hosts/hosts_table/columns.tsx | 1 + .../page/hosts/hosts_table/index.test.tsx | 2 +- .../page/hosts/hosts_table/translations.ts | 2 +- .../uncommon_process_table/index.test.tsx | 10 +- .../hosts/uncommon_process_table/index.tsx | 6 +- .../uncommon_process_table/translations.ts | 8 +- .../page/network/domains_table/index.test.tsx | 2 +- .../network/domains_table/translations.ts | 2 +- .../network_top_n_flow_table/columns.tsx | 19 +- .../page/network/users_table/index.test.tsx | 2 +- .../page/network/users_table/translations.ts | 2 +- .../siem/public/components/pin/index.tsx | 29 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../components/resize_handle/index.test.tsx | 65 +- .../public/components/resize_handle/index.tsx | 46 +- .../resize_handle/styled_handles.tsx | 18 - .../__snapshots__/index.test.tsx.snap | 7 + .../components/skeleton_row/index.test.tsx | 58 + .../public/components/skeleton_row/index.tsx | 67 ++ .../siem/public/components/tables/helpers.tsx | 8 +- .../timeline/body/actions/index.tsx | 138 +-- .../__snapshots__/index.test.tsx.snap | 1070 +++++++---------- .../body/column_headers/actions/index.tsx | 87 +- .../body/column_headers/common/styles.tsx | 2 - .../header/__snapshots__/index.test.tsx.snap | 30 +- .../body/column_headers/header/index.test.tsx | 57 +- .../body/column_headers/header/index.tsx | 210 ++-- .../header_tooltip_content/index.tsx | 13 +- .../body/column_headers/index.test.tsx | 4 - .../timeline/body/column_headers/index.tsx | 268 ++--- .../body/data_driven_columns/index.tsx | 108 +- .../body/events/event_column_view.tsx | 92 +- .../components/timeline/body/events/index.tsx | 79 +- .../timeline/body/events/stateful_event.tsx | 88 +- .../body/events/stateful_event_child.tsx | 58 +- .../components/timeline/body/helpers.ts | 3 +- .../components/timeline/body/index.test.tsx | 7 +- .../public/components/timeline/body/index.tsx | 93 +- .../get_row_renderer.test.tsx.snap | 10 +- .../plain_row_renderer.test.tsx.snap | 10 +- .../generic_row_renderer.test.tsx.snap | 508 ++++---- .../renderers/auditd/generic_row_renderer.tsx | 11 +- .../body/renderers/column_renderer.ts | 8 +- .../body/renderers/empty_column_renderer.tsx | 18 +- .../body/renderers/formatted_field.test.tsx | 26 +- .../body/renderers/formatted_field.tsx | 49 +- .../timeline/body/renderers/helpers.tsx | 9 - .../netflow_row_renderer.test.tsx.snap | 346 +++--- .../netflow/netflow_row_renderer.tsx | 22 +- .../body/renderers/plain_column_renderer.tsx | 69 +- .../body/renderers/plain_row_renderer.tsx | 4 +- .../timeline/body/renderers/row_renderer.tsx | 12 +- .../suricata_row_renderer.test.tsx.snap | 926 +++++++------- .../suricata/suricata_row_renderer.tsx | 6 +- .../generic_row_renderer.test.tsx.snap | 380 +++--- .../renderers/system/generic_row_renderer.tsx | 11 +- .../zeek_row_renderer.test.tsx.snap | 926 +++++++------- .../body/renderers/zeek/zeek_row_renderer.tsx | 5 +- .../timeline/data_providers/index.tsx | 4 +- .../provider_item_and_drag_drop.tsx | 4 +- .../timeline/properties/helpers.tsx | 50 +- .../components/timeline/properties/styles.tsx | 16 - .../public/components/timeline/styles.tsx | 325 +++++ .../components/timeline/timeline.test.tsx | 4 +- .../public/components/timeline/timeline.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 12 +- .../truncatable_text/index.test.tsx | 24 +- .../components/truncatable_text/index.tsx | 21 +- .../siem/public/pages/network/network.tsx | 59 +- 104 files changed, 3715 insertions(+), 3930 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.test.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/header_panel/__snapshots__/header_panel.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/components/header_panel/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/siem/public/components/header_panel/header_panel.test.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/header_panel/header_panel.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/header_panel/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 60c466459794e2..d9b78836b450e4 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -12,7 +12,6 @@ import { MockedProvider } from 'react-apollo/test-utils'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; - import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { DraggableWrapper } from './draggable_wrapper'; @@ -50,14 +49,12 @@ describe('DraggableWrapper', () => { }); describe('text truncation styling', () => { - test('it applies text truncation styling when a width IS specified (implicit: and the user is not dragging)', () => { - const width = '100px'; - + test('it applies text truncation styling when truncate IS specified (implicit: and the user is not dragging)', () => { const wrapper = mount( - message} /> + message} truncate /> @@ -68,7 +65,7 @@ describe('DraggableWrapper', () => { ); }); - test('it does NOT apply text truncation styling when a width is NOT specified', () => { + test('it does NOT apply text truncation styling when truncate is NOT specified', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 9ddfc82dd88a5a..0755ef0e5592cf 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -20,7 +20,6 @@ import { dragAndDropActions } from '../../store/drag_and_drop'; import { DataProvider } from '../timeline/data_providers/data_provider'; import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../timeline/helpers'; import { TruncatableText } from '../truncatable_text'; - import { getDraggableId, getDroppableId } from './helpers'; // As right now, we do not know what we want there, we will keep it as a placeholder @@ -29,17 +28,14 @@ export const DragEffects = styled.div``; DragEffects.displayName = 'DragEffects'; const Wrapper = styled.div` - .euiPageBody & { - display: inline-block; - } + display: inline-block; + max-width: 100%; `; Wrapper.displayName = 'Wrapper'; const ProviderContainer = styled.div<{ isDragging: boolean }>` ${({ theme, isDragging }) => css` - // ALL DRAGGABLES - &, &::before, &::after { @@ -47,23 +43,13 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>` color ${theme.eui.euiAnimSpeedFast} ease; } - .euiBadge, - .euiBadge__text { - cursor: grab; - } - - // PAGE DRAGGABLES - ${!isDragging && ` - .euiPageBody & { - z-index: ${theme.eui.euiZLevel0} !important; - } - - { + & { border-radius: 2px; padding: 0 4px 0 8px; position: relative; + z-index: ${theme.eui.euiZLevel0} !important; &::before { background-image: linear-gradient( @@ -86,6 +72,14 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>` } } + &:hover { + &, + & .euiBadge, + & .euiBadge__text { + cursor: move; //Fallback for IE11 + cursor: grab; + } + } .${STATEFUL_EVENT_CSS_CLASS_NAME}:hover &, tr:hover & { @@ -112,7 +106,8 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>` background-color: ${theme.eui.euiColorPrimary}; &, - & a { + & a, + & a:hover { color: ${theme.eui.euiColorEmptyShade}; } @@ -131,9 +126,10 @@ const ProviderContainer = styled.div<{ isDragging: boolean }>` ${isDragging && ` - .euiPageBody & { + & { z-index: ${theme.eui.euiZLevel9} !important; - `} + } + `} `} `; @@ -147,7 +143,7 @@ interface OwnProps { provided: DraggableProvided, state: DraggableStateSnapshot ) => React.ReactNode; - width?: string; + truncate?: boolean; } interface DispatchProps { @@ -166,10 +162,10 @@ type Props = OwnProps & DispatchProps; * data provider associated with the item being dropped */ class DraggableWrapperComponent extends React.Component { - public shouldComponentUpdate = ({ dataProvider, render, width }: Props) => + public shouldComponentUpdate = ({ dataProvider, render, truncate }: Props) => isEqual(dataProvider, this.props.dataProvider) && render !== this.props.render && - width === this.props.width + truncate === this.props.truncate ? false : true; @@ -186,7 +182,7 @@ class DraggableWrapperComponent extends React.Component { } public render() { - const { dataProvider, render, width } = this.props; + const { dataProvider, render, truncate } = this.props; return ( @@ -198,34 +194,28 @@ class DraggableWrapperComponent extends React.Component { index={0} key={getDraggableId(dataProvider.id)} > - {(provided, snapshot) => { - return ( - - {width != null && !snapshot.isDragging ? ( - - {render(dataProvider, provided, snapshot)} - - ) : ( - - {render(dataProvider, provided, snapshot)} - - )} - - ); - }} + {(provided, snapshot) => ( + + {truncate && !snapshot.isDragging ? ( + + {render(dataProvider, provided, snapshot)} + + ) : ( + + {render(dataProvider, provided, snapshot)} + + )} + + )} {droppableProvided.placeholder} diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index af28e74df0f5f1..c0ab5a939bc4da 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { rgba } from 'polished'; import * as React from 'react'; import { Droppable } from 'react-beautiful-dnd'; import { pure } from 'recompose'; import styled from 'styled-components'; -import { THIRTY_PERCENT_ALPHA_HEX_SUFFIX, TWENTY_PERCENT_ALPHA_HEX_SUFFIX } from './helpers'; - interface Props { children?: React.ReactNode; droppableId: string; @@ -34,22 +33,22 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string ? ` .drop-and-provider-timeline { &:hover { - background-color: ${props.theme.eui.euiColorSuccess}${THIRTY_PERCENT_ALPHA_HEX_SUFFIX}; + background-color: ${rgba(props.theme.eui.euiColorSuccess, 0.3)}; } } .drop-and-provider-timeline:hover { - background-color: ${props.theme.eui.euiColorSuccess}${THIRTY_PERCENT_ALPHA_HEX_SUFFIX}; + background-color: ${rgba(props.theme.eui.euiColorSuccess, 0.3)}; } > div.timeline-drop-area-empty { color: ${props.theme.eui.euiColorSuccess} - background-color: ${props.theme.eui.euiColorSuccess}${TWENTY_PERCENT_ALPHA_HEX_SUFFIX}; + background-color: ${rgba(props.theme.eui.euiColorSuccess, 0.2)}; & .euiTextColor--subdued { color: ${props.theme.eui.euiColorSuccess}; } } > div.timeline-drop-area { - background-color: ${props.theme.eui.euiColorSuccess}${TWENTY_PERCENT_ALPHA_HEX_SUFFIX}; + background-color: ${rgba(props.theme.eui.euiColorSuccess, 0.2)}; .provider-item-filter-container div:first-child{ // Override dragNdrop beautiful so we do not have our droppable moving around for no good reason transform: none !important; @@ -86,7 +85,6 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string } } `; - ReactDndDropTarget.displayName = 'ReactDndDropTarget'; export const DroppableWrapper = pure( @@ -118,5 +116,4 @@ export const DroppableWrapper = pure( ) ); - DroppableWrapper.displayName = 'DroppableWrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts index dce7b84a2128bd..415970474db4c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts @@ -224,12 +224,3 @@ export const DRAG_TYPE_FIELD = 'drag-type-field'; /** This class is added to the document body while dragging */ export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; - -/** A hex alpha channel suffix representing 10% for the `AA` in `#RRGGBBAA` */ -export const TEN_PERCENT_ALPHA_HEX_SUFFIX = '1A'; - -/** A hex alpha channel suffix representing 20% for the `AA` in `#RRGGBBAA` */ -export const TWENTY_PERCENT_ALPHA_HEX_SUFFIX = '33'; - -/** A hex alpha channel suffix representing 30% for the `AA` in `#RRGGBBAA` */ -export const THIRTY_PERCENT_ALPHA_HEX_SUFFIX = '4d'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx index f80afea9a98e95..faf65338b43378 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx @@ -4,54 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore - EuiHighlight, -} from '@elastic/eui'; +import { rgba } from 'polished'; import * as React from 'react'; import { pure } from 'recompose'; -import styled from 'styled-components'; - -const FieldBadgeFlexGroup = styled(EuiFlexGroup)` - height: 38px; -`; - -FieldBadgeFlexGroup.displayName = 'FieldBadgeFlexGroup'; - -const FieldBadgeFlexItem = styled(EuiFlexItem)` - font-weight: bold; -`; - -FieldBadgeFlexItem.displayName = 'FieldBadgeFlexItem'; - -/** - * The name of a (draggable) field - */ -export const FieldNameContainer = styled.div` - padding: 5px; - &:hover { - transition: background-color 0.7s ease; - background-color: #000; - color: #fff; - } +import styled, { css } from 'styled-components'; + +const Field = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border: ${theme.eui.euiBorderThin}; + box-shadow: 0 2px 2px -1px ${rgba(theme.eui.euiColorMediumShade, 0.3)}, + 0 1px 5px -2px ${rgba(theme.eui.euiColorMediumShade, 0.3)}; + font-size: ${theme.eui.euiFontSizeXS}; + font-weight: ${theme.eui.euiFontWeightSemiBold}; + line-height: ${theme.eui.euiLineHeight}; + padding: ${theme.eui.paddingSizes.xs}; + `} `; - -FieldNameContainer.displayName = 'FieldNameContainer'; +Field.displayName = 'Field'; /** * Renders a field (e.g. `event.action`) as a draggable badge */ -export const DraggableFieldBadge = pure<{ fieldId: string }>(({ fieldId }) => ( - - - - {fieldId} - - - -)); +// Passing the styles directly to the component because the width is +// being calculated and is recommended by Styled Components for performance +// https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 +export const DraggableFieldBadge = pure<{ fieldId: string; fieldWidth?: string }>( + ({ fieldId, fieldWidth }) => ( + + {fieldId} + + ) +); DraggableFieldBadge.displayName = 'DraggableFieldBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index 0013e40afa9c43..d835d2c6219312 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Draggable } from 'react-beautiful-dnd'; import { EuiCheckbox, EuiFlexGroup, @@ -15,30 +14,30 @@ import { EuiToolTip, } from '@elastic/eui'; import * as React from 'react'; +import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; -import { ColumnHeader } from '../timeline/body/column_headers/column_header'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; -import { DragEffects } from '../drag_and_drop/draggable_wrapper'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; -import { DefaultDraggable } from '../draggables'; import { ToStringArray } from '../../graphql/types'; +import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; +import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; +import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; +import { DefaultDraggable } from '../draggables'; import { DraggableFieldBadge } from '../draggables/field_badge'; -import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; +import { EVENT_DURATION_FIELD_NAME } from '../duration'; import { FieldName } from '../fields_browser/field_name'; -import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; -import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; -import { OnUpdateColumns } from '../timeline/events'; import { SelectableText } from '../selectable_text'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; -import { WithHoverActions } from '../with_hover_actions'; - -import * as i18n from './translations'; import { OverflowField } from '../tables/helpers'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; import { DATE_FIELD_TYPE, MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; -import { EVENT_DURATION_FIELD_NAME } from '../duration'; +import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; +import { OnUpdateColumns } from '../timeline/events'; +import { WithHoverActions } from '../with_hover_actions'; +import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; +import * as i18n from './translations'; import { EventFieldsData } from './types'; const HoverActionsContainer = styled(EuiPanel)` diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx index 821ec2048d5ad0..f77c703f064f6c 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx @@ -9,14 +9,12 @@ import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; -import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { DetailItem } from '../../graphql/types'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { OnUpdateColumns } from '../timeline/events'; - import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; -import { useTimelineWidthContext } from '../timeline/timeline_context'; export type View = 'table-view' | 'json-view'; @@ -32,9 +30,8 @@ interface Props { toggleColumn: (column: ColumnHeader) => void; } -const Details = styled.div<{ width: number }>` +const Details = styled.div` user-select: none; - width: ${({ width }) => `${width}px`}; `; Details.displayName = 'Details'; @@ -51,7 +48,6 @@ export const EventDetails = React.memo( timelineId, toggleColumn, }) => { - const width = useTimelineWidthContext(); const tabs: EuiTabbedContentTab[] = [ { id: 'table-view', @@ -76,7 +72,7 @@ export const EventDetails = React.memo( ]; return ( -
+
{ expect( wrapper - .find(`[data-test-subj="subtitle"]`) + .find(`[data-test-subj="header-panel-subtitle"]`) .first() .text() ).toEqual('Showing: 12 events'); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx index 62e77aac15fd62..4299657e36dabc 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { getOr, isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { AutoSizer } from '../auto_sizer'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; import { KqlMode } from '../../store/timeline/model'; +import { AutoSizer } from '../auto_sizer'; +import { HeaderPanel } from '../header_panel'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { Sort } from '../timeline/body/sort'; @@ -23,29 +24,18 @@ import { DataProvider } from '../timeline/data_providers/data_provider'; import { OnChangeItemsPerPage } from '../timeline/events'; import { Footer, footerHeight } from '../timeline/footer'; import { combineQueries } from '../timeline/helpers'; +import { TimelineRefetch } from '../timeline/refetch_timeline'; import { isCompactFooter } from '../timeline/timeline'; import { ManageTimelineContext } from '../timeline/timeline_context'; - -import { EventsViewerHeader } from './events_viewer_header'; -import { TimelineRefetch } from '../timeline/refetch_timeline'; +import * as i18n from './translations'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; const WrappedByAutoSizer = styled.div` width: 100%; `; // required by AutoSizer - WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; -const EventsViewerContainer = styled(EuiFlexGroup)` - overflow: hidden; - padding: 0 10px 0 12px; - user-select: none; - width: 100%; -`; - -EventsViewerContainer.displayName = 'EventsViewerContainer'; - interface Props { browserFields: BrowserFields; columns: ColumnHeader[]; @@ -103,18 +93,14 @@ export const EventsViewer = React.memo( {({ measureRef, content: { width = 0 } }) => ( - + <>
+ {combinedQueries != null ? ( c.id)} @@ -138,10 +124,13 @@ export const EventsViewer = React.memo( totalCount = 0, }) => ( <> -
( loading={loading} refetch={refetch} /> + ( sort={sort} toggleColumn={toggleColumn} /> +
( )} ) : null} - + )} @@ -211,5 +202,4 @@ export const EventsViewer = React.memo( prevProps.start === nextProps.start && prevProps.sort === nextProps.sort ); - EventsViewer.displayName = 'EventsViewer'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.test.tsx deleted file mode 100644 index 84e7248869b2f4..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 { mount } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock'; -import '../../mock/ui_settings'; - -import { EventsViewerHeader } from './events_viewer_header'; - -jest.mock('../../lib/settings/use_kibana_ui_setting'); - -const totalCount = 30; - -describe('EventsViewerHeader', () => { - test('it renders the expected title', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="panel_headline_title"]') - .first() - .text() - ).toEqual('Events'); - }); - - test('it renders a transparent inspect button when showInspect is false', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="transparent-inspect-container"]`) - .first() - .exists() - ).toBe(true); - }); - - test('it renders an opaque inspect button when showInspect is true', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="opaque-inspect-container"]`) - .first() - .exists() - ).toBe(true); - }); - - test('it renders the expected totalCount', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="subtitle"]`) - .first() - .text() - ).toEqual(`Showing: ${totalCount} events`); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.tsx deleted file mode 100644 index 048b92e91efd23..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer_header.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; - -import { HeaderPanel } from '../header_panel'; -import { InspectButton } from '../inspect'; - -import * as i18n from './translations'; - -interface Props { - id: string; - showInspect: boolean; - totalCount: number; -} - -export const EventsViewerHeader = React.memo(({ id, showInspect, totalCount }) => { - return ( - - - - - - - - - - ); -}); - -EventsViewerHeader.displayName = 'EventsViewerHeader'; diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx index 7478cbc055d8f7..c7912777c653b9 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx @@ -5,11 +5,11 @@ */ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getOr } from 'lodash/fp'; import React, { Fragment, useState } from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; import { pure } from 'recompose'; + import { AutonomousSystem, FlowTarget, @@ -17,14 +17,13 @@ import { IpOverviewData, Overview, } from '../../graphql/types'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { DefaultDraggable } from '../draggables'; import { getEmptyTagValue } from '../empty_value'; import { FormattedDate } from '../formatted_date'; import { HostDetailsLink, ReputationLink, VirusTotalLink, WhoIsLink } from '../links'; - -import * as i18n from '../page/network/ip_overview/translations'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { Spacer } from '../page'; +import * as i18n from '../page/network/ip_overview/translations'; export const IpOverviewId = 'ip-overview'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx index 8fab6ec14688a8..2581fba75da1e5 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx @@ -18,13 +18,12 @@ import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { getColumnsWithTimestamp } from '../event_details/helpers'; +import { CountBadge } from '../page'; import { OnUpdateColumns } from '../timeline/events'; +import { TimelineContext } from '../timeline/timeline_context'; import { WithHoverActions } from '../with_hover_actions'; - -import * as i18n from './translations'; -import { CountBadge } from '../page'; import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount } from './helpers'; -import { TimelineContext } from '../timeline/timeline_context'; +import * as i18n from './translations'; const CategoryName = styled.span<{ bold: boolean }>` font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx index f356c250eae5c9..17785ff582a3c2 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; import { EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import * as React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; - +import { CategoriesPane } from './categories_pane'; +import { FieldsPane } from './fields_pane'; +import { Header } from './header'; import { CATEGORY_PANE_WIDTH, FIELDS_PANE_WIDTH, @@ -21,26 +23,25 @@ import { PANES_FLEX_GROUP_WIDTH, } from './helpers'; import { FieldBrowserProps, OnFieldSelected, OnHideFieldBrowser } from './types'; -import { Header } from './header'; -import { CategoriesPane } from './categories_pane'; -import { FieldsPane } from './fields_pane'; + const FieldsBrowserContainer = styled.div<{ width: number }>` - background-color: ${props => props.theme.eui.euiColorLightestShade}; - border: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; - border-radius: 4px; - padding: 8px 8px 16px 8px; - position: absolute; - top: 25px; - ${({ width }) => `width: ${width}px`}; - z-index: 9990; + ${({ theme, width }) => css` + background-color: ${theme.eui.euiColorLightestShade}; + border: ${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiColorMediumShade}; + border-radius: ${theme.eui.euiBorderRadius}; + left: 0; + padding: ${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.s} ${theme.eui.paddingSizes.m}; + position: absolute; + top: calc(100% + ${theme.eui.euiSize}); + width: ${width}px; + z-index: 9990; + `} `; - FieldsBrowserContainer.displayName = 'FieldsBrowserContainer'; const PanesFlexGroup = styled(EuiFlexGroup)` width: ${PANES_FLEX_GROUP_WIDTH}px; `; - PanesFlexGroup.displayName = 'PanesFlexGroup'; type Props = Pick< diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx index 5cb96246de7d17..e909a983deedb1 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx @@ -11,22 +11,20 @@ import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; import { BrowserField, BrowserFields } from '../../containers/source'; -import { DraggableFieldBadge } from '../draggables/field_badge'; import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; -import { getColumnsWithTimestamp, getExampleText, getIconFromType } from '../event_details/helpers'; import { getDraggableFieldId, getDroppableId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; +import { DraggableFieldBadge } from '../draggables/field_badge'; import { getEmptyValue } from '../empty_value'; -import { OnUpdateColumns } from '../timeline/events'; +import { getColumnsWithTimestamp, getExampleText, getIconFromType } from '../event_details/helpers'; import { SelectableText } from '../selectable_text'; +import { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; +import { OnUpdateColumns } from '../timeline/events'; import { TruncatableText } from '../truncatable_text'; - import { FieldName } from './field_name'; - import * as i18n from './translations'; -import { ColumnHeader } from '../timeline/body/column_headers/column_header'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; const TypeIcon = styled(EuiIcon)` margin-left: 5px; @@ -179,11 +177,11 @@ export const getFieldColumns = () => [ field: 'description', name: i18n.DESCRIPTION, render: (description: string) => ( - - - {description} - - + + + <>{description} + + ), sortable: true, truncateText: true, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx index a998c057661eab..9c2cf2cb0e0b28 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx @@ -21,11 +21,10 @@ import styled, { css } from 'styled-components'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { OnUpdateColumns } from '../timeline/events'; +import { TimelineContext } from '../timeline/timeline_context'; import { WithHoverActions } from '../with_hover_actions'; - import { LoadingSpinner } from './helpers'; import * as i18n from './translations'; -import { TimelineContext } from '../timeline/timeline_context'; /** * The name of a (draggable) field diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index bcff95dc8358e2..4c9c1fc4147ab7 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -38,7 +38,7 @@ describe('StatefulFieldsBrowser', () => { .find('[data-test-subj="show-field-browser"]') .first() .text() - ).toEqual('Fields'); + ).toEqual('Columns'); }); describe('toggleShow', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx index 29c2c4a2c4e0b1..69720c76cab803 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx @@ -4,24 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionCreator } from 'typescript-fsa'; -import { connect } from 'react-redux'; -import { EuiButton, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import * as React from 'react'; +import { connect } from 'react-redux'; import styled from 'styled-components'; +import { ActionCreator } from 'typescript-fsa'; import { BrowserFields } from '../../containers/source'; +import { timelineActions } from '../../store/actions'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; import { OnUpdateColumns } from '../timeline/events'; - import { FieldsBrowser } from './field_browser'; -import { FieldBrowserProps } from './types'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; - import * as i18n from './translations'; -import { timelineActions } from '../../store/actions'; +import { FieldBrowserProps } from './types'; const fieldsButtonClassName = 'fields-button'; @@ -41,17 +39,8 @@ interface State { show: boolean; } -const FieldsBrowserButtonContainer = styled.div<{ show: boolean }>` - ${({ show }) => (show ? 'position: absolute;' : '')} - - .${fieldsButtonClassName} { - border-color: ${({ theme }) => theme.eui.euiColorLightShade}; - color: ${({ theme }) => theme.eui.euiColorDarkestShade}; - font-size: 14px; - margin: 1px 5px 2px 0; - ${({ show }) => (show ? 'position: absolute;' : '')} - ${({ show }) => (show ? 'top: -15px;' : '')} - } +const FieldsBrowserButtonContainer = styled.div` + position: relative; `; FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer'; @@ -124,28 +113,26 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent< return ( <> - + {isEventViewer ? ( ) : ( - {i18n.FIELDS} - + )} diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts b/x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts index 9446fe1b61bda7..5365fd05b9f750 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts @@ -37,7 +37,7 @@ export const FIELD = i18n.translate('xpack.siem.fieldBrowser.fieldLabel', { }); export const FIELDS = i18n.translate('xpack.siem.fieldBrowser.fieldsTitle', { - defaultMessage: 'Fields', + defaultMessage: 'Columns', }); export const FIELDS_COUNT = (totalCount: number) => diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx index 2283c8c84137f5..0e052fd4196116 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx @@ -64,9 +64,9 @@ export const FiltersGlobal = pure(({ children }) => ( {({ style, isSticky }) => (
)); - StarIcon.displayName = 'StarIcon'; export const Description = pure<{ @@ -100,7 +89,6 @@ export const Description = pure<{ )); - Description.displayName = 'Description'; export const Name = pure<{ timelineId: string; title: string; updateTitle: UpdateTitle }>( @@ -117,7 +105,6 @@ export const Name = pure<{ timelineId: string; title: string; updateTitle: Updat ) ); - Name.displayName = 'Name'; export const NewTimeline = pure<{ @@ -138,7 +125,6 @@ export const NewTimeline = pure<{ {i18n.NEW_TIMELINE} )); - NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { @@ -156,25 +142,6 @@ interface NotesButtonProps { const getNewNoteId = (): string => uuid.v4(); -const NotesButtonIcon = styled(EuiButtonIcon)` - svg { - height: 19px; - width: 19px; - } -`; - -const NotesIcon = pure<{ count: number }>(({ count }) => ( - 0 ? 'primary' : 'subdued'} - data-test-subj="timeline-notes-icon" - size="m" - iconType="editorComment" - /> -)); - -NotesIcon.displayName = 'NotesIcon'; - const LargeNotesButton = pure<{ noteIds: string[]; text?: string; toggleShowNotes: () => void }>( ({ noteIds, text, toggleShowNotes }) => ( ) ); - LargeNotesButton.displayName = 'LargeNotesButton'; const SmallNotesButton = pure<{ noteIds: string[]; toggleShowNotes: () => void }>( ({ noteIds, toggleShowNotes }) => ( - toggleShowNotes()} - role="button" - > - - + /> ) ); - SmallNotesButton.displayName = 'SmallNotesButton'; /** @@ -254,7 +218,6 @@ const NotesButtonComponent = pure( ) ); - NotesButtonComponent.displayName = 'NotesButtonComponent'; export const NotesButton = pure( @@ -298,5 +261,4 @@ export const NotesButton = pure( ) ); - NotesButton.displayName = 'NotesButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx index 81060a10b0d49d..3444875282ae75 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx @@ -19,7 +19,6 @@ export const TimelineProperties = styled.div` justify-content: space-between; user-select: none; `; - TimelineProperties.displayName = 'TimelineProperties'; export const DatePicker = styled(EuiFlexItem)<{ width: number }>` @@ -29,14 +28,12 @@ export const DatePicker = styled(EuiFlexItem)<{ width: number }>` width: auto; } `; - DatePicker.displayName = 'DatePicker'; export const NameField = styled(EuiFieldText)` width: 150px; margin-right: 5px; `; - NameField.displayName = 'NameField'; export const DescriptionContainer = styled.div` @@ -44,33 +41,22 @@ export const DescriptionContainer = styled.div` margin-right: 5px; min-width: 150px; `; - DescriptionContainer.displayName = 'DescriptionContainer'; -export const SmallNotesButtonContainer = styled.div` - cursor: pointer; - width: 35px; -`; - -SmallNotesButtonContainer.displayName = 'SmallNotesButtonContainer'; - export const ButtonContainer = styled.div<{ animate: boolean }>` animation: ${fadeInEffect} ${({ animate }) => (animate ? '0.3s' : '0s')}; `; - ButtonContainer.displayName = 'ButtonContainer'; export const LabelText = styled.div` margin-left: 10px; `; - LabelText.displayName = 'LabelText'; export const StyledStar = styled(EuiIcon)` margin-right: 5px; cursor: pointer; `; - StyledStar.displayName = 'StyledStar'; export const Facet = styled.div` @@ -88,11 +74,9 @@ export const Facet = styled.div` padding-right: 8px; user-select: none; `; - Facet.displayName = 'Facet'; export const LockIconContainer = styled(EuiFlexItem)` margin-right: 2px; `; - LockIconContainer.displayName = 'LockIconContainer'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx new file mode 100644 index 00000000000000..86c470ef4d3a56 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx @@ -0,0 +1,325 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { rgba } from 'polished'; +import styled, { css } from 'styled-components'; + +/** + * OFFSET PIXEL VALUES + */ + +export const OFFSET_SCROLLBAR = 17; + +/** + * TIMELINE BODY + */ + +export const TimelineBody = styled.div.attrs({ + className: 'siemTimeline__body', +})<{ bodyHeight: number }>` + ${({ bodyHeight, theme }) => css` + height: ${bodyHeight + 'px'}; + overflow: auto; + scrollbar-width: thin; + + &::-webkit-scrollbar { + height: ${theme.eui.euiScrollBar}; + width: ${theme.eui.euiScrollBar}; + } + + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${theme.eui.euiScrollBarCorner} solid transparent; + } + + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } + `} +`; +TimelineBody.displayName = 'TimelineBody'; + +/** + * EVENTS TABLE + */ + +export const EventsTable = styled.div.attrs({ + className: 'siemEventsTable', + role: 'table', +})``; +EventsTable.displayName = 'EventsTable'; + +/* EVENTS HEAD */ + +export const EventsThead = styled.div.attrs({ + className: 'siemEventsTable__thead', + role: 'rowgroup', +})` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border-bottom: ${theme.eui.euiBorderWidthThick} solid ${theme.eui.euiColorLightShade}; + position: sticky; + top: 0; + z-index: ${theme.eui.euiZLevel1}; + `} +`; +EventsThead.displayName = 'EventsThead'; + +export const EventsTrHeader = styled.div.attrs({ + className: 'siemEventsTable__trHeader', + role: 'row', +})` + display: flex; +`; +EventsTrHeader.displayName = 'EventsTrHeader'; + +export const EventsThGroupActions = styled.div.attrs({ + className: 'siemEventsTable__thGroupActions', +})<{ actionsColumnWidth: number }>` + display: flex; + flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'}; + justify-content: space-between; + min-width: 0; +`; +EventsThGroupActions.displayName = 'EventsThGroupActions'; + +export const EventsThGroupData = styled.div.attrs({ + className: 'siemEventsTable__thGroupData', +})` + display: flex; +`; +EventsThGroupData.displayName = 'EventsThGroupData'; + +export const EventsTh = styled.div.attrs({ + className: 'siemEventsTable__th', + role: 'columnheader', +})<{ isDragging?: boolean; position?: string }>` + align-items: center; + display: flex; + flex-shrink: 0; + min-width: 0; + position: ${({ position }) => position}; + + .siemEventsTable__thGroupActions &:first-child:last-child { + flex: 1; + } + + .siemEventsTable__thGroupData &:hover { + background-color: ${({ theme }) => theme.eui.euiTableHoverColor}; + cursor: move; //Fallback for IE11 + cursor: grab; + } +`; +EventsTh.displayName = 'EventsTh'; + +export const EventsThContent = styled.div.attrs({ + className: 'siemEventsTable__thContent', +})<{ textAlign?: string }>` + ${({ textAlign, theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + font-weight: ${theme.eui.euiFontWeightSemiBold}; + line-height: ${theme.eui.euiLineHeight}; + min-width: 0; + padding: ${theme.eui.paddingSizes.xs}; + text-align: ${textAlign}; + width: 100%; //Using width: 100% instead of flex: 1 and max-width: 100% for IE11 + `} +`; +EventsThContent.displayName = 'EventsThContent'; + +/* EVENTS BODY */ + +export const EventsTbody = styled.div.attrs({ + className: 'siemEventsTable__tbody', + role: 'rowgroup', +})` + overflow-x: hidden; +`; +EventsTbody.displayName = 'EventsTbody'; + +export const EventsTrGroup = styled.div.attrs({ + className: 'siemEventsTable__trGroup', +})<{ className?: string }>` + ${({ theme }) => css` + border-bottom: ${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiColorLightShade}; + + &:hover { + background-color: ${theme.eui.euiTableHoverColor}; + } + `} +`; +EventsTrGroup.displayName = 'EventsTrGroup'; + +export const EventsTrData = styled.div.attrs({ + className: 'siemEventsTable__trData', + role: 'row', +})` + display: flex; +`; +EventsTrData.displayName = 'EventsTrData'; + +export const EventsTrSupplement = styled.div.attrs({ + className: 'siemEventsTable__trSupplement', +})<{ className: string }>` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + padding: 0 ${theme.eui.paddingSizes.xs} 0 ${theme.eui.paddingSizes.xl}; + `} +`; +EventsTrSupplement.displayName = 'EventsTrSupplement'; + +export const EventsTdGroupActions = styled.div.attrs({ + className: 'siemEventsTable__tdGroupActions', +})<{ actionsColumnWidth: number }>` + display: flex; + justify-content: space-between; + flex: 0 0 ${({ actionsColumnWidth }) => actionsColumnWidth + 'px'}; + min-width: 0; +`; +EventsTdGroupActions.displayName = 'EventsTdGroupActions'; + +export const EventsTdGroupData = styled.div.attrs({ + className: 'siemEventsTable__tdGroupData', +})` + display: flex; +`; +EventsTdGroupData.displayName = 'EventsTdGroupData'; + +export const EventsTd = styled.div.attrs({ + className: 'siemEventsTable__td', + role: 'cell', +})` + align-items: center; + display: flex; + flex-shrink: 0; + min-width: 0; + + .siemEventsTable__tdGroupActions &:first-child:last-child { + flex: 1; + } +`; +EventsTd.displayName = 'EventsTd'; + +export const EventsTdContent = styled.div.attrs({ + className: 'siemEventsTable__tdContent', +})<{ textAlign?: string }>` + ${({ textAlign, theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + min-width: 0; + padding: ${theme.eui.paddingSizes.xs}; + text-align: ${textAlign}; + width: 100%; //Using width: 100% instead of flex: 1 and max-width: 100% for IE11 + `} +`; +EventsTdContent.displayName = 'EventsTdContent'; + +/** + * EVENTS HEADING + */ + +export const EventsHeading = styled.div.attrs({ + className: 'siemEventsHeading', +})<{ isLoading: boolean }>` + align-items: center; + display: flex; + + &:hover { + cursor: ${({ isLoading }) => (isLoading ? 'wait' : 'grab')}; + } +`; +EventsHeading.displayName = 'EventsHeading'; + +export const EventsHeadingTitleButton = styled.button.attrs({ + className: 'siemEventsHeading__title siemEventsHeading__title--aggregatable', + type: 'button', +})` + ${({ theme }) => css` + align-items: center; + display: flex; + font-weight: inherit; + min-width: 0; + + &:hover, + &:focus { + color: ${theme.eui.euiColorPrimary}; + text-decoration: underline; + } + + &:hover { + cursor: pointer; + } + + & > * + * { + margin-left: ${theme.eui.euiSizeXS}; + } + `} +`; +EventsHeadingTitleButton.displayName = 'EventsHeadingTitleButton'; + +export const EventsHeadingTitleSpan = styled.span.attrs({ + className: 'siemEventsHeading__title siemEventsHeading__title--notAggregatable', +})` + min-width: 0; +`; +EventsHeadingTitleSpan.displayName = 'EventsHeadingTitleSpan'; + +export const EventsHeadingExtra = styled.div.attrs({ + className: 'siemEventsHeading__extra', +})<{ className?: string }>` + ${({ theme }) => css` + margin-left: auto; + + &.siemEventsHeading__extra--close { + opacity: 0; + transition: all ${theme.eui.euiAnimSpeedNormal} ease; + visibility: hidden; + + .siemEventsTable__th:hover & { + opacity: 1; + visibility: visible; + } + } + `} +`; +EventsHeadingExtra.displayName = 'EventsHeadingExtra'; + +export const EventsHeadingHandle = styled.div.attrs({ + className: 'siemEventsHeading__handle', +})` + ${({ theme }) => css` + background-color: ${theme.eui.euiBorderColor}; + height: 100%; + opacity: 0; + transition: all ${theme.eui.euiAnimSpeedNormal} ease; + visibility: hidden; + width: ${theme.eui.euiBorderWidthThick}; + + .siemEventsTable__thead:hover & { + opacity: 1; + visibility: visible; + } + + &:hover { + background-color: ${theme.eui.euiColorPrimary}; + cursor: col-resize; + } + `} +`; +EventsHeadingHandle.displayName = 'EventsHeadingHandle'; + +/** + * EVENTS LOADING + */ + +export const EventsLoading = styled(EuiLoadingSpinner)` + margin: ${({ theme }) => theme.eui.euiSizeXS}; + vertical-align: top; +`; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index 2617f9a957dd24..85986d2ed471c1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -113,7 +113,7 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="timelineHeader"]').exists()).toEqual(true); }); - test('it renders the timeline body', () => { + test('it renders the timeline table', () => { const wrapper = mount( @@ -148,7 +148,7 @@ describe('Timeline', () => { ); - expect(wrapper.find('[data-test-subj="horizontal-scroll"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); }); test('it does NOT render the paging footer when you do NOT have any data providers', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index ded0209cef35d0..5101d557923698 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -15,7 +15,6 @@ import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; import { KqlMode } from '../../store/timeline/model'; import { AutoSizer } from '../auto_sizer'; - import { ColumnHeader } from './body/column_headers/column_header'; import { defaultHeaders } from './body/column_headers/default_headers'; import { Sort } from './body/sort'; @@ -30,12 +29,12 @@ import { OnToggleDataProviderEnabled, OnToggleDataProviderExcluded, } from './events'; +import { TimelineKqlFetch } from './fetch_kql_timeline'; import { Footer, footerHeight } from './footer'; import { TimelineHeader } from './header'; import { calculateBodyHeight, combineQueries } from './helpers'; import { TimelineRefetch } from './refetch_timeline'; import { ManageTimelineContext } from './timeline_context'; -import { TimelineKqlFetch } from './fetch_kql_timeline'; const WrappedByAutoSizer = styled.div` width: 100%; diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap index d17982cd26ae24..23b930c7a114b6 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap @@ -1,17 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TruncatableText renders correctly against snapshot 1`] = ` -.c0 { +.c0, +.c0 * { + display: inline-block; + max-width: 100%; overflow: hidden; text-overflow: ellipsis; + vertical-align: top; white-space: nowrap; - width: 50px; } - Hiding in plain sight - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx index c1bc485e21f2ab..51ffdb43fd4677 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx @@ -12,42 +12,26 @@ import * as React from 'react'; import { TruncatableText } from '.'; describe('TruncatableText', () => { - const width = '50px'; - test('renders correctly against snapshot', () => { - const wrapper = shallow( - {'Hiding in plain sight'} - ); + const wrapper = shallow({'Hiding in plain sight'}); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it adds the hidden overflow style', () => { - const wrapper = mount( - {'Hiding in plain sight'} - ); + const wrapper = mount({'Hiding in plain sight'}); expect(wrapper).toHaveStyleRule('overflow', 'hidden'); }); test('it adds the ellipsis text-overflow style', () => { - const wrapper = mount({'Dramatic pause'}); + const wrapper = mount({'Dramatic pause'}); expect(wrapper).toHaveStyleRule('text-overflow', 'ellipsis'); }); test('it adds the nowrap white-space style', () => { - const wrapper = mount( - {'Who stopped the beats?'} - ); + const wrapper = mount({'Who stopped the beats?'}); expect(wrapper).toHaveStyleRule('white-space', 'nowrap'); }); - - test('it forwards the width prop as a style', () => { - const wrapper = mount( - {'width or without you'} - ); - - expect(wrapper).toHaveStyleRule('width', width); - }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx index 7cf91ce35cd43b..ff8307666275d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx @@ -4,21 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiText } from '@elastic/eui'; import styled from 'styled-components'; /** * Applies CSS styling to enable text to be truncated with an ellipsis. * Example: "Don't leave me hanging..." * - * Width is required, because CSS will not truncate the text unless a width is - * specified. + * Note: Requires a parent container with a defined width or max-width. */ -export const TruncatableText = styled(EuiText)<{ width: string }>` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: ${({ width }) => width}; -`; +export const TruncatableText = styled.span` + &, + & * { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; + white-space: nowrap; + } +`; TruncatableText.displayName = 'TruncatableText'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx index 3ee0cd23d56437..de75edfb33a4a9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx @@ -5,42 +5,51 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { getOr } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; - +import styled, { css } from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; -import { RouteComponentProps } from 'react-router-dom'; + +import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; +import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table'; +import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { manageQuery } from '../../components/page/manage_query'; import { KpiNetworkComponent, NetworkTopNFlowTable } from '../../components/page/network'; import { NetworkDnsTable } from '../../components/page/network/network_dns_table'; import { GlobalTime } from '../../containers/global_time'; import { KpiNetworkQuery } from '../../containers/kpi_network'; +import { NetworkFilter } from '../../containers/network'; import { NetworkDnsQuery } from '../../containers/network_dns'; import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { FlowTargetNew, LastEventIndexKey } from '../../graphql/types'; import { networkModel, networkSelectors, State } from '../../store'; - -import { NetworkKql } from './kql'; -import { NetworkEmptyPage } from './network_empty_page'; -import * as i18n from './translations'; -import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table'; -import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; -import { EmbeddedMap } from '../../components/embeddables/embedded_map'; -import { NetworkFilter } from '../../containers/network'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { NetworkKql } from './kql'; +import { NetworkEmptyPage } from './network_empty_page'; +import * as i18n from './translations'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); const NetworkDnsTableManage = manageQuery(NetworkDnsTable); const KpiNetworkComponentManage = manageQuery(KpiNetworkComponent); + +const ConditionalFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + @media only screen and (min-width: 1441px) { + flex-direction: row; + } + `} +`; +ConditionalFlexGroup.displayName = 'ConditionalFlexGroup'; + interface NetworkComponentReduxProps { filterQuery: string; queryExpression: string; @@ -52,27 +61,6 @@ interface NetworkComponentReduxProps { } type NetworkComponentProps = NetworkComponentReduxProps & Partial>; -const mediaMatch = window.matchMedia( - 'screen and (min-width: ' + euiLightVars.euiBreakpoints.xl + ')' -); -const getFlexDirectionByMediaMatch = (): 'row' | 'column' => { - const { matches } = mediaMatch; - return matches ? 'row' : 'column'; -}; -export const getFlexDirection = () => { - const [display, setDisplay] = useState(getFlexDirectionByMediaMatch()); - - useEffect(() => { - const setFromEvent = () => setDisplay(getFlexDirectionByMediaMatch()); - window.addEventListener('resize', setFromEvent); - - return () => { - window.removeEventListener('resize', setFromEvent); - }; - }, []); - - return display; -}; const NetworkComponent = React.memo( ({ filterQuery, queryExpression, setAbsoluteRangeDatePicker }) => ( @@ -108,6 +96,7 @@ const NetworkComponent = React.memo( /> )} + ( - + ( )} - + From 51d734e9b8eea586be9e80dac3e5cd74323e3873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 2 Oct 2019 17:28:57 +0200 Subject: [PATCH 40/53] Changing status code colors on trace summary (#47114) --- .../HttpInfoSummaryItem.test.tsx | 29 +++++++++++++------ .../Summary/HttpInfoSummaryItem/index.tsx | 22 +++++++------- 2 files changed, 32 insertions(+), 19 deletions(-) rename x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/{ => __test__}/HttpInfoSummaryItem.test.tsx (75%) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx similarity index 75% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx index e87c16b9fd100a..7edc7eab3b3851 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/HttpInfoSummaryItem.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx @@ -6,16 +6,16 @@ import React from 'react'; import { shallow, mount } from 'enzyme'; -import { palettes } from '@elastic/eui'; -import { HttpInfoSummaryItem } from './'; -import * as exampleTransactions from '../__fixtures__/transactions'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { HttpInfoSummaryItem } from '../'; +import * as exampleTransactions from '../../__fixtures__/transactions'; describe('HttpInfoSummaryItem', () => { describe('render', () => { const transaction = exampleTransactions.httpOk; const url = 'https://example.com'; const method = 'get'; - const props = { transaction, url, method, status: 200 }; + const props = { transaction, url, method, status: 100 }; it('renders', () => { expect(() => @@ -23,12 +23,23 @@ describe('HttpInfoSummaryItem', () => { ).not.toThrowError(); }); - describe('with status code 200', () => { + describe('with status code 100', () => { it('shows a success color', () => { const wrapper = mount(); expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual( - palettes.euiPaletteForStatus.colors[0] + theme.euiColorDarkShade + ); + }); + }); + + describe('with status code 200', () => { + it('shows a success color', () => { + const p = { ...props, status: 200 }; + const wrapper = mount(); + + expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual( + theme.euiColorSecondary ); }); }); @@ -40,7 +51,7 @@ describe('HttpInfoSummaryItem', () => { const wrapper = mount(); expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual( - palettes.euiPaletteForStatus.colors[4] + theme.euiColorDarkShade ); }); }); @@ -52,7 +63,7 @@ describe('HttpInfoSummaryItem', () => { const wrapper = mount(); expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual( - palettes.euiPaletteForStatus.colors[9] + theme.euiColorWarning ); }); }); @@ -64,7 +75,7 @@ describe('HttpInfoSummaryItem', () => { const wrapper = mount(); expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual( - palettes.euiPaletteForStatus.colors[9] + theme.euiColorDanger ); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx index d433dae75a1aee..5396b57fc27a6f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -6,24 +6,26 @@ import React from 'react'; import { EuiToolTip, EuiBadge } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { palettes } from '@elastic/eui'; import { units, px, truncate, unit } from '../../../../style/variables'; import { statusCodes } from './statusCodes'; -const statusColors = { - success: palettes.euiPaletteForStatus.colors[0], - warning: palettes.euiPaletteForStatus.colors[4], - error: palettes.euiPaletteForStatus.colors[9] -}; +const { + euiColorDarkShade, + euiColorSecondary, + euiColorWarning, + euiColorDanger +} = theme; function getStatusColor(status: number) { const colors: { [key: string]: string } = { - 2: statusColors.success, - 3: statusColors.warning, - 4: statusColors.error, - 5: statusColors.error + 1: euiColorDarkShade, + 2: euiColorSecondary, + 3: euiColorDarkShade, + 4: euiColorWarning, + 5: euiColorDanger }; return colors[status.toString().substr(0, 1)] || 'default'; From 0bfa7ca5c6412d277d165abb69fb8f211ff9329e Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 2 Oct 2019 12:05:02 -0400 Subject: [PATCH 41/53] Support space-specific default routes (#44678) --- config/kibana.yml | 4 - docs/setup/settings.asciidoc | 4 - .../advanced_settings.test.js.snap | 30 ++ .../field/__snapshots__/field.test.js.snap | 419 ++++++++++++++++++ .../settings/components/field/field.js | 15 +- .../settings/components/field/field.test.js | 30 ++ .../lib/__tests__/to_editable_config.test.js | 18 +- .../settings/lib/to_editable_config.js | 4 + .../kibana/ui_setting_defaults.js | 18 + src/legacy/server/config/schema.js | 2 +- .../server/config/transform_deprecations.js | 1 + .../config/transform_deprecations.test.js | 48 +- src/legacy/server/http/index.js | 9 +- .../default_route_provider.test.ts | 87 ++++ .../http/setup_default_route_provider.ts | 74 ++++ src/legacy/server/kbn_server.d.ts | 1 + .../privilege_space_table.tsx | 2 +- .../space_selector.tsx | 2 +- .../legacy/plugins/spaces/common/constants.ts | 5 + x-pack/legacy/plugins/spaces/common/index.ts | 2 +- .../spaces_url_parser.test.ts.snap | 0 .../lib/spaces_url_parser.test.ts | 2 +- .../lib/spaces_url_parser.ts | 2 +- x-pack/legacy/plugins/spaces/index.ts | 6 +- .../spaces/public/components/space_avatar.tsx | 4 +- .../legacy/plugins/spaces/public/lib/index.ts | 1 + .../lib}/space_attributes.test.ts | 0 .../lib}/space_attributes.ts | 4 +- .../spaces/public/lib/spaces_manager.ts | 35 +- .../customize_space_avatar.tsx | 2 +- .../spaces/public/views/management/index.tsx | 4 +- .../public/views/management/page_routes.tsx | 12 +- .../public/views/nav_control/nav_control.tsx | 4 +- .../public/views/space_selector/index.tsx | 4 +- .../spaces/server/lib/get_active_space.ts | 2 +- .../on_post_auth_interceptor.test.ts | 13 +- .../on_post_auth_interceptor.ts | 12 +- .../on_request_interceptor.ts | 2 +- .../spaces/server/new_platform/plugin.ts | 8 - .../spaces_service/spaces_service.test.ts | 2 +- .../spaces_service/spaces_service.ts | 2 +- .../api/__fixtures__/create_test_handler.ts | 6 +- .../spaces/server/routes/api/v1/index.ts | 36 -- .../server/routes/api/v1/spaces.test.ts | 93 ---- .../spaces/server/routes/api/v1/spaces.ts | 51 --- .../spaces/server/routes/views/enter_space.ts | 24 + .../spaces/server/routes/views/index.ts | 1 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../functional/apps/spaces/enter_space.ts | 60 +++ x-pack/test/functional/apps/spaces/index.ts | 1 + .../es_archives/spaces/enter_space/data.json | 83 ++++ .../spaces/enter_space/mappings.json | 287 ++++++++++++ .../page_objects/space_selector_page.js | 10 +- .../common/suites/select.ts | 125 ------ .../security_and_spaces/apis/index.ts | 1 - .../security_and_spaces/apis/select.ts | 341 -------------- .../spaces_only/apis/index.ts | 1 - .../spaces_only/apis/select.ts | 74 ---- 59 files changed, 1251 insertions(+), 843 deletions(-) create mode 100644 src/legacy/server/http/integration_tests/default_route_provider.test.ts create mode 100644 src/legacy/server/http/setup_default_route_provider.ts rename x-pack/legacy/plugins/spaces/{server => common}/lib/__snapshots__/spaces_url_parser.test.ts.snap (100%) rename x-pack/legacy/plugins/spaces/{server => common}/lib/spaces_url_parser.test.ts (97%) rename x-pack/legacy/plugins/spaces/{server => common}/lib/spaces_url_parser.ts (95%) rename x-pack/legacy/plugins/spaces/{common => public/lib}/space_attributes.test.ts (100%) rename x-pack/legacy/plugins/spaces/{common => public/lib}/space_attributes.ts (94%) delete mode 100644 x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts delete mode 100644 x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts delete mode 100644 x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts create mode 100644 x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts create mode 100644 x-pack/test/functional/apps/spaces/enter_space.ts create mode 100644 x-pack/test/functional/es_archives/spaces/enter_space/data.json create mode 100644 x-pack/test/functional/es_archives/spaces/enter_space/mappings.json delete mode 100644 x-pack/test/spaces_api_integration/common/suites/select.ts delete mode 100644 x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts delete mode 100644 x-pack/test/spaces_api_integration/spaces_only/apis/select.ts diff --git a/config/kibana.yml b/config/kibana.yml index 7d49fb37e03208..9525a6423d90a0 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -18,10 +18,6 @@ # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false -# Specifies the default route when opening Kibana. You can use this setting to modify -# the landing page when opening Kibana. -#server.defaultRoute: /app/kibana - # The maximum payload size in bytes for incoming server requests. #server.maxPayloadBytes: 1048576 diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7f9034c48e232f..5b3db22a39ea64 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -256,10 +256,6 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). `server.customResponseHeaders:`:: *Default: `{}`* Header names and values to send on all responses to the client from the Kibana server. -[[server-default]]`server.defaultRoute:`:: *Default: "/app/kibana"* This setting -specifies the default route when opening Kibana. You can use this setting to -modify the landing page when opening Kibana. Supported on {ece}. - `server.host:`:: *Default: "localhost"* This setting specifies the host of the back end server. diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap index a3af9fcc884e5c..8c1db0c33e0b02 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/__snapshots__/advanced_settings.test.js.snap @@ -70,6 +70,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "array", + "validation": undefined, "value": undefined, }, Object { @@ -88,6 +89,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "boolean", + "validation": undefined, "value": undefined, }, ], @@ -108,6 +110,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -126,6 +129,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "image", + "validation": undefined, "value": undefined, }, Object { @@ -146,6 +150,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -164,6 +169,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -186,6 +192,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -204,6 +211,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -222,6 +230,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -240,6 +249,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "markdown", + "validation": undefined, "value": undefined, }, Object { @@ -258,6 +268,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -280,6 +291,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -298,6 +310,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -342,6 +355,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "array", + "validation": undefined, "value": undefined, }, Object { @@ -360,6 +374,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "boolean", + "validation": undefined, "value": undefined, }, ], @@ -380,6 +395,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -398,6 +414,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "image", + "validation": undefined, "value": undefined, }, Object { @@ -418,6 +435,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -436,6 +454,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -458,6 +477,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -476,6 +496,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, Object { @@ -494,6 +515,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "json", + "validation": undefined, "value": undefined, }, Object { @@ -512,6 +534,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "markdown", + "validation": undefined, "value": undefined, }, Object { @@ -530,6 +553,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "number", + "validation": undefined, "value": undefined, }, Object { @@ -552,6 +576,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "select", + "validation": undefined, "value": undefined, }, Object { @@ -570,6 +595,7 @@ exports[`AdvancedSettings should render normally 1`] = ` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -689,6 +715,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] = "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -731,6 +758,7 @@ exports[`AdvancedSettings should render read-only when saving is disabled 1`] = "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -868,6 +896,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], @@ -910,6 +939,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1` "readonly": false, "requiresPageReload": false, "type": "string", + "validation": undefined, "value": undefined, }, ], diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap index 1a5039bbb96f84..eb8454f64e7ba7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/__snapshots__/field.test.js.snap @@ -3707,3 +3707,422 @@ exports[`Field for string setting should render user value if there is user valu /> `; + +exports[`Field for stringWithValidation setting should render as read only if saving is disabled 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + + + + +`; + +exports[`Field for stringWithValidation setting should render as read only with help text if overridden 1`] = ` + + + +
+ + + + + + foo-default + , + } + } + /> + + + + + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + } + isInvalid={false} + label="string:test-validation:setting" + labelType="label" + > + + + + + + +`; + +exports[`Field for stringWithValidation setting should render custom setting icon if it is custom 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + + } + type="asterisk" + /> +

+ } + titleSize="xs" + > + + + + + + + +`; + +exports[`Field for stringWithValidation setting should render default value if there is no user value set 1`] = ` + + + +
+ + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + + + + +`; + +exports[`Field for stringWithValidation setting should render user value if there is user value is set 1`] = ` + + + +
+ + + + + + foo-default + , + } + } + /> + + + + + } + fullWidth={false} + gutterSize="l" + idAria="string:test-validation:setting-aria" + title={ +

+ String test validation setting + +

+ } + titleSize="xs" + > + + + + + +     + + + } + isInvalid={false} + label="string:test-validation:setting" + labelType="label" + > + + + + + + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js index f431a862fb4c8b..c0b1188950126c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.js @@ -166,7 +166,7 @@ class FieldUI extends PureComponent { onFieldChange = (e) => { const value = e.target.value; - const { type } = this.props.setting; + const { type, validation } = this.props.setting; const { unsavedValue } = this.state; let newUnsavedValue = undefined; @@ -181,8 +181,21 @@ class FieldUI extends PureComponent { default: newUnsavedValue = value; } + + let isInvalid = false; + let error = undefined; + + if (validation && validation.regex) { + if (!validation.regex.test(newUnsavedValue)) { + error = validation.message; + isInvalid = true; + } + } + this.setState({ unsavedValue: newUnsavedValue, + isInvalid, + error }); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js index 81cda09eaf0da5..0a2886d0d0287e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/components/field/field.test.js @@ -143,6 +143,22 @@ const settings = { isOverridden: false, options: null, }, + stringWithValidation: { + name: 'string:test-validation:setting', + ariaName: 'string test validation setting', + displayName: 'String test validation setting', + description: 'Description for String test validation setting', + type: 'string', + validation: { + regex: new RegExp('/^foo'), + message: 'must start with "foo"' + }, + value: undefined, + defVal: 'foo-default', + isCustom: false, + isOverridden: false, + options: null, + } }; const userValues = { array: ['user', 'value'], @@ -153,6 +169,10 @@ const userValues = { number: 10, select: 'banana', string: 'foo', + stringWithValidation: 'fooUserValue' +}; +const invalidUserValues = { + stringWithValidation: 'invalidUserValue' }; const save = jest.fn(() => Promise.resolve()); const clear = jest.fn(() => Promise.resolve()); @@ -392,6 +412,16 @@ describe('Field', () => { const userValue = userValues[type]; const fieldUserValue = type === 'array' ? userValue.join(', ') : userValue; + if (setting.validation) { + const invalidUserValue = invalidUserValues[type]; + it('should display an error when validation fails', async () => { + component.instance().onFieldChange({ target: { value: invalidUserValue } }); + component.update(); + const errorMessage = component.find('.euiFormErrorText').text(); + expect(errorMessage).toEqual(setting.validation.message); + }); + } + it('should be able to change value and cancel', async () => { component.instance().onFieldChange({ target: { value: fieldUserValue } }); component.update(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js index ad1ba30ece4b19..555aab8c2b5ffa 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/__tests__/to_editable_config.test.js @@ -43,7 +43,7 @@ describe('Settings', function () { def = { value: 'the original', description: 'the one and only', - options: 'all the options' + options: 'all the options', }; }); @@ -76,6 +76,18 @@ describe('Settings', function () { expect(invoke({ def }).type).to.equal('array'); }); }); + + describe('that contains a validation object', function () { + it('constructs a validation regex with message', function () { + def.validation = { + regexString: '^foo', + message: 'must start with "foo"' + }; + const result = invoke({ def }); + expect(result.validation.regex).to.be.a(RegExp); + expect(result.validation.message).to.equal('must start with "foo"'); + }); + }); }); describe('when not given a setting definition object', function () { @@ -94,6 +106,10 @@ describe('Settings', function () { it('sets options to undefined', function () { expect(invoke().options).to.be.undefined; }); + + it('sets validation to undefined', function () { + expect(invoke().validation).to.be.undefined; + }); }); }); }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js index b557c880c496b5..4c3b87e5120920 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/to_editable_config.js @@ -43,6 +43,10 @@ export function toEditableConfig({ def, name, value, isCustom, isOverridden }) { defVal: def.value, type: getValType(def, value), description: def.description, + validation: def.validation ? { + regex: new RegExp(def.validation.regexString), + message: def.validation.message + } : undefined, options: def.options, optionLabels: def.optionLabels, requiresPageReload: !!def.requiresPageReload, diff --git a/src/legacy/core_plugins/kibana/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/ui_setting_defaults.js index 191ae7309f46ff..7a152526339560 100644 --- a/src/legacy/core_plugins/kibana/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/ui_setting_defaults.js @@ -55,6 +55,24 @@ export function getUiSettingDefaults() { 'buildNum': { readonly: true }, + 'defaultRoute': { + name: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteTitle', { + defaultMessage: 'Default route', + }), + value: '/app/kibana', + validation: { + regexString: '^\/', + message: i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteValidationMessage', { + defaultMessage: 'The route must start with a slash ("/")', + }), + }, + description: + i18n.translate('kbn.advancedSettings.defaultRoute.defaultRouteText', { + defaultMessage: 'This setting specifies the default route when opening Kibana. ' + + 'You can use this setting to modify the landing page when opening Kibana. ' + + 'The route must start with a slash ("/").', + }), + }, 'query:queryString:options': { name: i18n.translate('kbn.advancedSettings.query.queryStringOptionsTitle', { defaultMessage: 'Query string options', diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 919653bc941f46..2b91eafd45caab 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -78,7 +78,7 @@ export default () => Joi.object({ server: Joi.object({ uuid: Joi.string().guid().default(), name: Joi.string().default(os.hostname()), - defaultRoute: Joi.string().default('/app/kibana').regex(/^\//, `start with a slash`), + defaultRoute: Joi.string().regex(/^\//, `start with a slash`), customResponseHeaders: Joi.object().unknown(true).default({}), xsrf: Joi.object({ disableProtection: Joi.boolean().default(false), diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js index 7cac17a88fe64b..8be880074f9fd3 100644 --- a/src/legacy/server/config/transform_deprecations.js +++ b/src/legacy/server/config/transform_deprecations.js @@ -95,6 +95,7 @@ const cspRules = (settings, log) => { const deprecations = [ //server + rename('server.defaultRoute', 'uiSettings.overrides.defaultRoute'), unused('server.xsrf.token'), unused('uiSettings.enabled'), rename('optimize.lazy', 'optimize.watch'), diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js index 38044357f230df..4094443ac0006a 100644 --- a/src/legacy/server/config/transform_deprecations.test.js +++ b/src/legacy/server/config/transform_deprecations.test.js @@ -62,6 +62,24 @@ describe('server/config', function () { }); }); + describe('server.defaultRoute', () => { + it('renames to uiSettings.overrides.defaultRoute when specified', () => { + const settings = { + server: { + defaultRoute: '/app/foo', + }, + }; + + expect(transformDeprecations(settings)).toEqual({ + uiSettings: { + overrides: { + defaultRoute: '/app/foo' + } + } + }); + }); + }); + describe('csp.rules', () => { describe('with nonce source', () => { it('logs a warning', () => { @@ -74,20 +92,18 @@ describe('server/config', function () { const log = jest.fn(); transformDeprecations(settings, log); expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", - ], - ] - `); + Array [ + Array [ + "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", + ], + ] + `); }); it('replaces a nonce', () => { expect( - transformDeprecations( - { csp: { rules: [`script-src 'nonce-{nonce}'`] } }, - jest.fn() - ).csp.rules + transformDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }, jest.fn()).csp + .rules ).toEqual([`script-src 'self'`]); expect( transformDeprecations( @@ -158,12 +174,12 @@ describe('server/config', function () { const log = jest.fn(); transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, log); expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules must contain the 'self' source. Automatically adding to script-src.", - ], - ] - `); + Array [ + Array [ + "csp.rules must contain the 'self' source. Automatically adding to script-src.", + ], + ] + `); }); it('adds self', () => { diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js index 40ac2baa032d63..f8fbc6c4976ff2 100644 --- a/src/legacy/server/http/index.js +++ b/src/legacy/server/http/index.js @@ -25,6 +25,7 @@ import Boom from 'boom'; import { setupVersionCheck } from './version_check'; import { registerHapiPlugins } from './register_hapi_plugins'; import { setupBasePathProvider } from './setup_base_path_provider'; +import { setupDefaultRouteProvider } from './setup_default_route_provider'; import { setupXsrf } from './xsrf'; export default async function (kbnServer, server, config) { @@ -33,6 +34,8 @@ export default async function (kbnServer, server, config) { setupBasePathProvider(kbnServer); + setupDefaultRouteProvider(server); + await registerHapiPlugins(server); // provide a simple way to expose static directories @@ -86,10 +89,8 @@ export default async function (kbnServer, server, config) { server.route({ path: '/', method: 'GET', - handler(req, h) { - const basePath = req.getBasePath(); - const defaultRoute = config.get('server.defaultRoute'); - return h.redirect(`${basePath}${defaultRoute}`); + async handler(req, h) { + return h.redirect(await req.getDefaultRoute()); } }); diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts new file mode 100644 index 00000000000000..fe8c4649651329 --- /dev/null +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +jest.mock('../../../ui/ui_settings/ui_settings_mixin', () => { + return jest.fn(); +}); + +import * as kbnTestServer from '../../../../test_utils/kbn_server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Root } from '../../../../core/server/root'; + +let mockDefaultRouteSetting: any = ''; + +describe('default route provider', () => { + let root: Root; + beforeAll(async () => { + root = kbnTestServer.createRoot(); + + await root.setup(); + await root.start(); + + const kbnServer = kbnTestServer.getKbnServer(root); + + kbnServer.server.decorate('request', 'getUiSettingsService', function() { + return { + get: (key: string) => { + if (key === 'defaultRoute') { + return Promise.resolve(mockDefaultRouteSetting); + } + throw Error(`unsupported ui setting: ${key}`); + }, + getDefaults: () => { + return Promise.resolve({ + defaultRoute: { + value: '/app/kibana', + }, + }); + }, + }; + }); + }, 30000); + + afterAll(async () => await root.shutdown()); + + it('redirects to the configured default route', async function() { + mockDefaultRouteSetting = '/app/some/default/route'; + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/app/some/default/route', + }); + }); + + const invalidRoutes = [ + 'http://not-your-kibana.com', + '///example.com', + '//example.com', + ' //example.com', + ]; + for (const route of invalidRoutes) { + it(`falls back to /app/kibana when the configured route (${route}) is not a valid relative path`, async function() { + mockDefaultRouteSetting = route; + + const { status, header } = await kbnTestServer.request.get(root, '/'); + expect(status).toEqual(302); + expect(header).toMatchObject({ + location: '/app/kibana', + }); + }); + } +}); diff --git a/src/legacy/server/http/setup_default_route_provider.ts b/src/legacy/server/http/setup_default_route_provider.ts new file mode 100644 index 00000000000000..07ff61015a1875 --- /dev/null +++ b/src/legacy/server/http/setup_default_route_provider.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Legacy } from 'kibana'; +import { parse } from 'url'; + +export function setupDefaultRouteProvider(server: Legacy.Server) { + server.decorate('request', 'getDefaultRoute', async function() { + // @ts-ignore + const request: Legacy.Request = this; + + const serverBasePath: string = server.config().get('server.basePath'); + + const uiSettings = request.getUiSettingsService(); + + const defaultRoute = await uiSettings.get('defaultRoute'); + const qualifiedDefaultRoute = `${request.getBasePath()}${defaultRoute}`; + + if (isRelativePath(qualifiedDefaultRoute, serverBasePath)) { + return qualifiedDefaultRoute; + } else { + server.log( + ['http', 'warn'], + `Ignoring configured default route of '${defaultRoute}', as it is malformed.` + ); + + const fallbackRoute = (await uiSettings.getDefaults()).defaultRoute.value; + + const qualifiedFallbackRoute = `${request.getBasePath()}${fallbackRoute}`; + return qualifiedFallbackRoute; + } + }); + + function isRelativePath(candidatePath: string, basePath = '') { + // validate that `candidatePath` is not attempting a redirect to somewhere + // outside of this Kibana install + const { protocol, hostname, port, pathname } = parse( + candidatePath, + false /* parseQueryString */, + true /* slashesDenoteHost */ + ); + + // We should explicitly compare `protocol`, `port` and `hostname` to null to make sure these are not + // detected in the URL at all. For example `hostname` can be empty string for Node URL parser, but + // browser (because of various bwc reasons) processes URL differently (e.g. `///abc.com` - for browser + // hostname is `abc.com`, but for Node hostname is an empty string i.e. everything between schema (`//`) + // and the first slash that belongs to path. + if (protocol !== null || hostname !== null || port !== null) { + return false; + } + + if (!String(pathname).startsWith(basePath)) { + return false; + } + + return true; + } +} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 406697ab65d8f6..69bf95e57cab9b 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -83,6 +83,7 @@ declare module 'hapi' { interface Request { getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract; getBasePath(): string; + getDefaultRoute(): Promise; getUiSettingsService(): any; getCapabilities(): Promise; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 5a718a34b90057..3c49e5717ba42b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -14,7 +14,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/common'; +import { getSpaceColor } from '../../../../../../../../../spaces/public/lib/space_attributes'; import { Space } from '../../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 9cc9894a0f0513..75211498c57b89 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -14,7 +14,7 @@ import { import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/common/space_attributes'; +import { getSpaceColor } from '../../../../../../../../../spaces/public/lib/space_attributes'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/spaces/common/constants.ts b/x-pack/legacy/plugins/spaces/common/constants.ts index 50423517bc9184..11882ca2f1b3a8 100644 --- a/x-pack/legacy/plugins/spaces/common/constants.ts +++ b/x-pack/legacy/plugins/spaces/common/constants.ts @@ -21,3 +21,8 @@ export const MAX_SPACE_INITIALS = 2; * @type {string} */ export const KIBANA_SPACES_STATS_TYPE = 'spaces'; + +/** + * The path to enter a space. + */ +export const ENTER_SPACE_PATH = '/spaces/enter'; diff --git a/x-pack/legacy/plugins/spaces/common/index.ts b/x-pack/legacy/plugins/spaces/common/index.ts index 0e605562ea3ea4..a0842201e0f083 100644 --- a/x-pack/legacy/plugins/spaces/common/index.ts +++ b/x-pack/legacy/plugins/spaces/common/index.ts @@ -7,4 +7,4 @@ export { isReservedSpace } from './is_reserved_space'; export { MAX_SPACE_INITIALS } from './constants'; -export { getSpaceInitials, getSpaceColor } from './space_attributes'; +export { getSpaceIdFromPath, addSpaceIdToPath } from './lib/spaces_url_parser'; diff --git a/x-pack/legacy/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.ts.snap b/x-pack/legacy/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/spaces/server/lib/__snapshots__/spaces_url_parser.test.ts.snap rename to x-pack/legacy/plugins/spaces/common/lib/__snapshots__/spaces_url_parser.test.ts.snap diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.test.ts b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.test.ts similarity index 97% rename from x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.test.ts rename to x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.test.ts index 5878272c849246..b25d79c0a69076 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.test.ts +++ b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { DEFAULT_SPACE_ID } from '../constants'; import { addSpaceIdToPath, getSpaceIdFromPath } from './spaces_url_parser'; describe('getSpaceIdFromPath', () => { diff --git a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.ts b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.ts similarity index 95% rename from x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.ts rename to x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.ts index 14113cbf9d8070..994ec7c59cb6e0 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/spaces_url_parser.ts +++ b/x-pack/legacy/plugins/spaces/common/lib/spaces_url_parser.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../common/constants'; +import { DEFAULT_SPACE_ID } from '../constants'; export function getSpaceIdFromPath( requestBasePath: string = '/', diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 6f9397233d1d0c..a287aa2fcbb3f7 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -14,12 +14,11 @@ import { AuditLogger } from '../../server/lib/audit_logger'; import mappings from './mappings.json'; import { wrapError } from './server/lib/errors'; import { getActiveSpace } from './server/lib/get_active_space'; -import { getSpaceSelectorUrl } from './server/lib/get_space_selector_url'; import { migrateToKibana660 } from './server/lib/migrations'; import { plugin } from './server/new_platform'; import { SecurityPlugin } from '../security'; import { SpacesServiceSetup } from './server/new_platform/spaces_service/spaces_service'; -import { initSpaceSelectorView } from './server/routes/views'; +import { initSpaceSelectorView, initEnterSpaceView } from './server/routes/views'; export interface SpacesPlugin { getSpaceId: SpacesServiceSetup['getSpaceId']; @@ -88,7 +87,7 @@ export const spaces = (kibana: Record) => return { spaces: [], activeSpace: null, - spaceSelectorURL: getSpaceSelectorUrl(server.config()), + serverBasePath: server.config().get('server.basePath'), }; }, async replaceInjectedVars( @@ -181,6 +180,7 @@ export const spaces = (kibana: Record) => }, }); + initEnterSpaceView(server); initSpaceSelectorView(server); server.expose('getSpaceId', (request: any) => spacesService.getSpaceId(request)); diff --git a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx index ee3755b8df5fa7..0211fe7e82643d 100644 --- a/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/components/space_avatar.tsx @@ -6,9 +6,9 @@ import { EuiAvatar, isValidHex } from '@elastic/eui'; import React, { SFC } from 'react'; -import { getSpaceColor, getSpaceInitials, MAX_SPACE_INITIALS } from '../../common'; +import { MAX_SPACE_INITIALS } from '../../common'; import { Space } from '../../common/model/space'; -import { getSpaceImageUrl } from '../../common/space_attributes'; +import { getSpaceColor, getSpaceInitials, getSpaceImageUrl } from '../lib/space_attributes'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/lib/index.ts b/x-pack/legacy/plugins/spaces/public/lib/index.ts index 538dd77e053f57..56ac7b8ff37f4f 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/index.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/index.ts @@ -5,3 +5,4 @@ */ export { SpacesManager } from './spaces_manager'; +export { getSpaceInitials, getSpaceColor, getSpaceImageUrl } from './space_attributes'; diff --git a/x-pack/legacy/plugins/spaces/common/space_attributes.test.ts b/x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts similarity index 100% rename from x-pack/legacy/plugins/spaces/common/space_attributes.test.ts rename to x-pack/legacy/plugins/spaces/public/lib/space_attributes.test.ts diff --git a/x-pack/legacy/plugins/spaces/common/space_attributes.ts b/x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts similarity index 94% rename from x-pack/legacy/plugins/spaces/common/space_attributes.ts rename to x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts index f943dcf4af105b..dbb1e8fed2d0b6 100644 --- a/x-pack/legacy/plugins/spaces/common/space_attributes.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/space_attributes.ts @@ -5,8 +5,8 @@ */ import { VISUALIZATION_COLORS } from '@elastic/eui'; -import { MAX_SPACE_INITIALS } from './constants'; -import { Space } from './model/space'; +import { Space } from '../../common/model/space'; +import { MAX_SPACE_INITIALS } from '../../common'; // code point for lowercase "a" const FALLBACK_CODE_POINT = 97; diff --git a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts index d39b751e30a8aa..e40e247e405fb9 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/spaces_manager.ts @@ -3,21 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { EventEmitter } from 'events'; import { kfetch } from 'ui/kfetch'; import { SavedObjectsManagementRecord } from 'ui/management/saved_objects_management'; import { Space } from '../../common/model/space'; import { GetSpacePurpose } from '../../common/model/types'; import { CopySavedObjectsToSpaceResponse } from './copy_saved_objects_to_space/types'; +import { ENTER_SPACE_PATH } from '../../common/constants'; +import { addSpaceIdToPath } from '../../common'; export class SpacesManager extends EventEmitter { - private spaceSelectorURL: string; - - constructor(spaceSelectorURL: string) { + constructor(private readonly serverBasePath: string) { super(); - this.spaceSelectorURL = spaceSelectorURL; } public async getSpaces(purpose?: GetSpacePurpose): Promise { @@ -89,36 +86,14 @@ export class SpacesManager extends EventEmitter { } public async changeSelectedSpace(space: Space) { - await kfetch({ - pathname: `/api/spaces/v1/space/${encodeURIComponent(space.id)}/select`, - method: 'POST', - }) - .then(response => { - if (response.location) { - window.location = response.location; - } else { - this._displayError(); - } - }) - .catch(() => this._displayError()); + window.location.href = addSpaceIdToPath(this.serverBasePath, space.id, ENTER_SPACE_PATH); } public redirectToSpaceSelector() { - window.location.href = this.spaceSelectorURL; + window.location.href = `${this.serverBasePath}/spaces/space_selector`; } public async requestRefresh() { this.emit('request_refresh'); } - - public _displayError() { - toastNotifications.addDanger({ - title: i18n.translate('xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle', { - defaultMessage: 'Unable to change your Space', - }), - text: i18n.translate('xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription', { - defaultMessage: 'please try again later', - }), - }); - } } diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx index 2f179083d7b90a..12fa0193b59a4e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/customize_space_avatar.tsx @@ -18,11 +18,11 @@ import { } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { getSpaceColor, getSpaceInitials } from '../../../../lib/space_attributes'; import { encode, imageTypes } from '../../../../../common/lib/dataurl'; import { MAX_SPACE_INITIALS } from '../../../../../common/constants'; import { Space } from '../../../../../common/model/space'; -import { getSpaceColor, getSpaceInitials } from '../../../../../common/space_attributes'; interface Props { space: Partial; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx index 46a718bbc6f35d..179665ed11111c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/index.tsx @@ -24,7 +24,7 @@ const MANAGE_SPACES_KEY = 'spaces'; routes.defaults(/\/management/, { resolve: { - spacesManagementSection(activeSpace: any, spaceSelectorURL: string) { + spacesManagementSection(activeSpace: any, serverBasePath: string) { function getKibanaSection() { return management.getSection('kibana'); } @@ -49,7 +49,7 @@ routes.defaults(/\/management/, { // Customize Saved Objects Management const action = new CopyToSpaceSavedObjectsManagementAction( - new SpacesManager(spaceSelectorURL), + new SpacesManager(serverBasePath), activeSpace.space ); // This route resolve function executes any time the management screen is loaded, and we want to ensure diff --git a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx index d38c5c1998b3a4..66cdb0d276e94b 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/page_routes.tsx @@ -22,11 +22,11 @@ routes.when('/management/spaces/list', { template, k7Breadcrumbs: getListBreadcrumbs, requireUICapability: 'management.kibana.spaces', - controller($scope: any, spacesNavState: SpacesNavState, spaceSelectorURL: string) { + controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( @@ -49,11 +49,11 @@ routes.when('/management/spaces/create', { template, k7Breadcrumbs: getCreateBreadcrumbs, requireUICapability: 'management.kibana.spaces', - controller($scope: any, spacesNavState: SpacesNavState, spaceSelectorURL: string) { + controller($scope: any, spacesNavState: SpacesNavState, serverBasePath: string) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( @@ -85,14 +85,14 @@ routes.when('/management/spaces/edit/:spaceId', { $route: any, chrome: any, spacesNavState: SpacesNavState, - spaceSelectorURL: string + serverBasePath: string ) { $scope.$$postDigest(async () => { const domNode = document.getElementById(reactRootNodeId); const { spaceId } = $route.current.params; - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( diff --git a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx index ad2ae08374708a..bac95bbf22099e 100644 --- a/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/nav_control/nav_control.tsx @@ -54,9 +54,9 @@ chromeHeaderNavControlsRegistry.register((chrome: any, activeSpace: any) => ({ return; } - const spaceSelectorURL = chrome.getInjected('spaceSelectorURL'); + const serverBasePath = chrome.getInjected('serverBasePath'); - spacesManager = new SpacesManager(spaceSelectorURL); + spacesManager = new SpacesManager(serverBasePath); ReactDOM.render( diff --git a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx b/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx index 935e79e73517e3..8c650fa778bdd4 100644 --- a/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/space_selector/index.tsx @@ -21,10 +21,10 @@ import { SpaceSelector } from './space_selector'; const module = uiModules.get('spaces_selector', []); module.controller( 'spacesSelectorController', - ($scope: any, spaces: Space[], spaceSelectorURL: string) => { + ($scope: any, spaces: Space[], serverBasePath: string) => { const domNode = document.getElementById('spaceSelectorRoot'); - const spacesManager = new SpacesManager(spaceSelectorURL); + const spacesManager = new SpacesManager(serverBasePath); render( diff --git a/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts b/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts index 907b7b164b69b3..a77a945239100b 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/get_active_space.ts @@ -7,7 +7,7 @@ import { Space } from '../../common/model/space'; import { wrapError } from './errors'; import { SpacesClient } from './spaces_client'; -import { getSpaceIdFromPath } from './spaces_url_parser'; +import { getSpaceIdFromPath } from '../../common'; export async function getActiveSpace( spacesClient: SpacesClient, diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index dfd4d586554bba..511af53c13ab46 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -428,7 +428,7 @@ describe('onPostAuthInterceptor', () => { ); }, 30000); - it('allows the request to continue when accessing the root of a non-default space', async () => { + it('redirects to the "enter space" endpoint when accessing the root of a non-default space', async () => { const spaces = [ { id: 'default', @@ -449,9 +449,8 @@ describe('onPostAuthInterceptor', () => { const { response, spacesService } = await request('/s/a-space', spaces); - // OSS handles this redirection for us expect(response.status).toEqual(302); - expect(response.header.location).toEqual(`/s/a-space${defaultRoute}`); + expect(response.header.location).toEqual(`/s/a-space/spaces/enter`); expect(spacesService.scopedClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -463,7 +462,7 @@ describe('onPostAuthInterceptor', () => { }, 30000); describe('with a single available space', () => { - it('it redirects to the defaultRoute within the context of the single Space when navigating to Kibana root', async () => { + it('it redirects to the "enter space" endpoint within the context of the single Space when navigating to Kibana root', async () => { const spaces = [ { id: 'a-space', @@ -477,7 +476,7 @@ describe('onPostAuthInterceptor', () => { const { response, spacesService } = await request('/', spaces); expect(response.status).toEqual(302); - expect(response.header.location).toEqual(`/s/a-space${defaultRoute}`); + expect(response.header.location).toEqual(`/s/a-space/spaces/enter`); expect(spacesService.scopedClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -488,7 +487,7 @@ describe('onPostAuthInterceptor', () => { ); }); - it('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { + it('it redirects to the "enter space" endpoint within the context of the Default Space when navigating to Kibana root', async () => { // This is very similar to the test above, but this handles the condition where the only available space is the Default Space, // which does not have a URL Context. In this scenario, the end result is the same as the other test, but the final URL the user // is redirected to does not contain a space identifier (e.g., /s/foo) @@ -506,7 +505,7 @@ describe('onPostAuthInterceptor', () => { const { response, spacesService } = await request('/', spaces); expect(response.status).toEqual(302); - expect(response.header.location).toEqual(defaultRoute); + expect(response.header.location).toEqual('/spaces/enter'); expect(spacesService.scopedClient).toHaveBeenCalledWith( expect.objectContaining({ headers: expect.objectContaining({ diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 7cd3114ced2fab..e02677d94a8da4 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -6,12 +6,12 @@ import { Logger, CoreSetup } from 'src/core/server'; import { Space } from '../../../common/model/space'; import { wrapError } from '../errors'; -import { addSpaceIdToPath } from '../spaces_url_parser'; import { XPackMainPlugin } from '../../../../xpack_main/xpack_main'; import { SpacesServiceSetup } from '../../new_platform/spaces_service/spaces_service'; import { LegacyAPI } from '../../new_platform/plugin'; import { getSpaceSelectorUrl } from '../get_space_selector_url'; -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; +import { addSpaceIdToPath } from '../../../common'; export interface OnPostAuthInterceptorDeps { getLegacyAPI(): LegacyAPI; @@ -28,7 +28,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ log, http, }: OnPostAuthInterceptorDeps) { - const { serverBasePath, serverDefaultRoute } = getLegacyAPI().legacyConfig; + const { serverBasePath } = getLegacyAPI().legacyConfig; http.registerOnPostAuth(async (request, response, toolkit) => { const path = request.url.pathname!; @@ -38,6 +38,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ // The root of kibana is also the root of the defaut space, // since the default space does not have a URL Identifier (i.e., `/s/foo`). const isRequestingKibanaRoot = path === '/' && spaceId === DEFAULT_SPACE_ID; + const isRequestingSpaceRoot = path === '/' && spaceId !== DEFAULT_SPACE_ID; const isRequestingApplication = path.startsWith('/app'); const spacesClient = await spacesService.scopedClient(request); @@ -54,7 +55,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ // No need for an interstitial screen where there is only one possible outcome. const space = spaces[0]; - const destination = addSpaceIdToPath(serverBasePath, space.id, serverDefaultRoute); + const destination = addSpaceIdToPath(serverBasePath, space.id, ENTER_SPACE_PATH); return response.redirected({ headers: { location: destination } }); } @@ -72,6 +73,9 @@ export function initSpacesOnPostAuthRequestInterceptor({ statusCode: wrappedError.output.statusCode, }); } + } else if (isRequestingSpaceRoot) { + const destination = addSpaceIdToPath(serverBasePath, spaceId, ENTER_SPACE_PATH); + return response.redirected({ headers: { location: destination } }); } // This condition should only happen after selecting a space, or when transitioning from one application to another diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 5da9bdbe6543f1..114cc9bf86d467 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -11,9 +11,9 @@ import { } from 'src/core/server'; import { format } from 'url'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { getSpaceIdFromPath } from '../spaces_url_parser'; import { modifyUrl } from '../utils/url'; import { LegacyAPI } from '../../new_platform/plugin'; +import { getSpaceIdFromPath } from '../../../common'; export interface OnRequestInterceptorDeps { getLegacyAPI(): LegacyAPI; diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts index 2bd9edadc52ef0..ed11e6da317fa1 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts @@ -20,7 +20,6 @@ import { checkLicense } from '../lib/check_license'; import { spacesSavedObjectsClientWrapperFactory } from '../lib/saved_objects_client/saved_objects_client_wrapper_factory'; import { SpacesAuditLogger } from '../lib/audit_logger'; import { createSpacesTutorialContextFactory } from '../lib/spaces_tutorial_context_factory'; -import { initInternalApis } from '../routes/api/v1'; import { initExternalSpacesApi } from '../routes/api/external'; import { getSpacesUsageCollector } from '../lib/get_spaces_usage_collector'; import { SpacesService } from './spaces_service'; @@ -178,13 +177,6 @@ export class Plugin { }) ); - initInternalApis({ - legacyRouter: legacyAPI.router, - getLegacyAPI: this.getLegacyAPI, - spacesService, - xpackMain: xpackMainPlugin, - }); - initExternalSpacesApi({ legacyRouter: legacyAPI.router, log: this.log, diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts index 3200c90bca2be0..817474dc0fb3a0 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.test.ts @@ -13,9 +13,9 @@ import { SavedObjectsErrorHelpers, } from 'src/core/server'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; -import { getSpaceIdFromPath } from '../../lib/spaces_url_parser'; import { createOptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { LegacyAPI } from '../plugin'; +import { getSpaceIdFromPath } from '../../../common'; const mockLogger = { trace: jest.fn(), diff --git a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts index 623e6c43b16e8c..08ebc2cb317482 100644 --- a/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts +++ b/x-pack/legacy/plugins/spaces/server/new_platform/spaces_service/spaces_service.ts @@ -12,11 +12,11 @@ import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { SecurityPlugin } from '../../../../security'; import { SpacesClient } from '../../lib/spaces_client'; -import { getSpaceIdFromPath, addSpaceIdToPath } from '../../lib/spaces_url_parser'; import { SpacesConfigType } from '../config'; import { namespaceToSpaceId, spaceIdToNamespace } from '../../lib/utils/namespace'; import { LegacyAPI } from '../plugin'; import { Space } from '../../../common/model/space'; +import { getSpaceIdFromPath, addSpaceIdToPath } from '../../../common'; type RequestFacade = KibanaRequest | Legacy.Request; diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts b/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts index 13667555a9468f..405a3dd34e7fc6 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/api/__fixtures__/create_test_handler.ts @@ -18,7 +18,6 @@ import { createSpaces } from './create_spaces'; import { ExternalRouteDeps } from '../external'; import { SpacesService } from '../../../new_platform/spaces_service'; import { SpacesAuditLogger } from '../../../lib/audit_logger'; -import { InternalRouteDeps } from '../v1'; import { LegacyAPI } from '../../../new_platform/plugin'; interface KibanaServer extends Legacy.Server { @@ -79,9 +78,7 @@ async function readStreamToCompletion(stream: Readable) { return (createPromiseFromStreams([stream, createConcatStream([])]) as unknown) as any[]; } -export function createTestHandler( - initApiFn: (deps: ExternalRouteDeps & InternalRouteDeps) => void -) { +export function createTestHandler(initApiFn: (deps: ExternalRouteDeps) => void) { const teardowns: TeardownFn[] = []; const spaces = createSpaces(); @@ -254,7 +251,6 @@ export function createTestHandler( }); initApiFn({ - getLegacyAPI: () => legacyAPI, routePreCheckLicenseFn: pre, savedObjects: server.savedObjects, spacesService, diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts b/x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts deleted file mode 100644 index ddbca3e8e3d71d..00000000000000 --- a/x-pack/legacy/plugins/spaces/server/routes/api/v1/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 { Legacy } from 'kibana'; -import { XPackMainPlugin } from '../../../../../xpack_main/xpack_main'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { initInternalSpacesApi } from './spaces'; -import { SpacesServiceSetup } from '../../../new_platform/spaces_service/spaces_service'; -import { LegacyAPI } from '../../../new_platform/plugin'; - -type Omit = Pick>; - -interface RouteDeps { - xpackMain: XPackMainPlugin; - spacesService: SpacesServiceSetup; - getLegacyAPI(): LegacyAPI; - legacyRouter: Legacy.Server['route']; -} - -export interface InternalRouteDeps extends Omit { - routePreCheckLicenseFn: any; -} - -export function initInternalApis({ xpackMain, ...rest }: RouteDeps) { - const routePreCheckLicenseFn = routePreCheckLicense({ xpackMain }); - - const deps: InternalRouteDeps = { - ...rest, - routePreCheckLicenseFn, - }; - - initInternalSpacesApi(deps); -} diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts b/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts deleted file mode 100644 index 4d9952f4ab3dcc..00000000000000 --- a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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. - */ - -jest.mock('../../../lib/route_pre_check_license', () => { - return { - routePreCheckLicense: () => (request: any, h: any) => h.continue, - }; -}); - -jest.mock('../../../../../../server/lib/get_client_shield', () => { - return { - getClient: () => { - return { - callWithInternalUser: jest.fn(() => { - return; - }), - }; - }, - }; -}); - -import Boom from 'boom'; -import { createTestHandler, RequestRunner, TeardownFn } from '../__fixtures__'; -import { initInternalSpacesApi } from './spaces'; - -describe('Spaces API', () => { - let request: RequestRunner; - let teardowns: TeardownFn[]; - - beforeEach(() => { - const setup = createTestHandler(initInternalSpacesApi); - - request = setup.request; - teardowns = setup.teardowns; - }); - - afterEach(async () => { - await Promise.all(teardowns.splice(0).map(fn => fn())); - }); - - test('POST space/{id}/select should respond with the new space location', async () => { - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select'); - - const { statusCode, payload } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/s/a-space'); - }); - - test(`returns result of routePreCheckLicense`, async () => { - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - expectSpacesClientCall: false, - }); - - const { statusCode, payload } = response; - - expect(statusCode).toEqual(403); - expect(JSON.parse(payload)).toMatchObject({ - message: 'test forbidden message', - }); - }); - - test('POST space/{id}/select should respond with 404 when the space is not found', async () => { - const { response } = await request('POST', '/api/spaces/v1/space/not-a-space/select'); - - const { statusCode } = response; - - expect(statusCode).toEqual(404); - }); - - test('POST space/{id}/select should respond with the new space location when a server.basePath is in use', async () => { - const testConfig = { - 'server.basePath': '/my/base/path', - }; - - const { response } = await request('POST', '/api/spaces/v1/space/a-space/select', { - testConfig, - }); - - const { statusCode, payload } = response; - - expect(statusCode).toEqual(200); - - const result = JSON.parse(payload); - expect(result.location).toEqual('/my/base/path/s/a-space'); - }); -}); diff --git a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts b/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts deleted file mode 100644 index 3d15044d129e93..00000000000000 --- a/x-pack/legacy/plugins/spaces/server/routes/api/v1/spaces.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 Boom from 'boom'; -import { Space } from '../../../../common/model/space'; -import { wrapError } from '../../../lib/errors'; -import { SpacesClient } from '../../../lib/spaces_client'; -import { addSpaceIdToPath } from '../../../lib/spaces_url_parser'; -import { getSpaceById } from '../../lib'; -import { InternalRouteDeps } from '.'; - -export function initInternalSpacesApi(deps: InternalRouteDeps) { - const { legacyRouter, spacesService, getLegacyAPI, routePreCheckLicenseFn } = deps; - - legacyRouter({ - method: 'POST', - path: '/api/spaces/v1/space/{id}/select', - async handler(request: any) { - const { savedObjects, legacyConfig } = getLegacyAPI(); - - const { SavedObjectsClient } = savedObjects; - const spacesClient: SpacesClient = await spacesService.scopedClient(request); - const id = request.params.id; - - const basePath = legacyConfig.serverBasePath; - const defaultRoute = legacyConfig.serverDefaultRoute; - try { - const existingSpace: Space | null = await getSpaceById( - spacesClient, - id, - SavedObjectsClient.errors - ); - if (!existingSpace) { - return Boom.notFound(); - } - - return { - location: addSpaceIdToPath(basePath, existingSpace.id, defaultRoute), - }; - } catch (error) { - return wrapError(error); - } - }, - options: { - pre: [routePreCheckLicenseFn], - }, - }); -} diff --git a/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts b/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts new file mode 100644 index 00000000000000..e560d4278b4079 --- /dev/null +++ b/x-pack/legacy/plugins/spaces/server/routes/views/enter_space.ts @@ -0,0 +1,24 @@ +/* + * 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 { Legacy } from 'kibana'; +import { ENTER_SPACE_PATH } from '../../../common/constants'; +import { wrapError } from '../../lib/errors'; + +export function initEnterSpaceView(server: Legacy.Server) { + server.route({ + method: 'GET', + path: ENTER_SPACE_PATH, + async handler(request, h) { + try { + return h.redirect(await request.getDefaultRoute()); + } catch (e) { + server.log(['spaces', 'error'], `Error navigating to space: ${e}`); + return wrapError(e); + } + }, + }); +} diff --git a/x-pack/legacy/plugins/spaces/server/routes/views/index.ts b/x-pack/legacy/plugins/spaces/server/routes/views/index.ts index a0f72886940a4a..d7637e299652fe 100644 --- a/x-pack/legacy/plugins/spaces/server/routes/views/index.ts +++ b/x-pack/legacy/plugins/spaces/server/routes/views/index.ts @@ -5,3 +5,4 @@ */ export { initSpaceSelectorView } from './space_selector'; +export { initEnterSpaceView } from './enter_space'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a821a66076bae9..6887dab9425cee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11074,8 +11074,6 @@ "xpack.spaces.spaceSelector.findSpacePlaceholder": "スペースを検索", "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "検索条件に一致するスペースがありません", "xpack.spaces.spaceSelector.selectSpacesTitle": "スペースの選択", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription": "後程再試行してください", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle": "スペースを変更できません", "xpack.spaces.spacesTitle": "スペース", "xpack.spaces.management.copyToSpace.actionDescription": "この保存されたオブジェクトを 1 つまたは複数のスペースにコピーします。", "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1f856731d43e19..649ead90b63561 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11076,8 +11076,6 @@ "xpack.spaces.spaceSelector.findSpacePlaceholder": "查找工作区", "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "没有匹配搜索条件的空间", "xpack.spaces.spaceSelector.selectSpacesTitle": "选择您的空间", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningDescription": "请稍后重试", - "xpack.spaces.spacesManager.unableToChangeSpaceWarningTitle": "无法更改空间", "xpack.spaces.spacesTitle": "工作区", "xpack.spaces.management.copyToSpace.actionDescription": "将此已保存对象复制到一个或多个工作区", "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts new file mode 100644 index 00000000000000..017d252b166cc4 --- /dev/null +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -0,0 +1,60 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function enterSpaceFunctonalTests({ + getService, + getPageObjects, +}: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['security', 'spaceSelector']); + + describe('Enter Space', function() { + this.tags('smoke'); + before(async () => await esArchiver.load('spaces/enter_space')); + after(async () => await esArchiver.unload('spaces/enter_space')); + + afterEach(async () => { + await PageObjects.security.logout(); + }); + + it('allows user to navigate to different spaces, respecting the configured default route', async () => { + const spaceId = 'another-space'; + + await PageObjects.security.login(null, null, { + expectSpaceSelector: true, + }); + + await PageObjects.spaceSelector.clickSpaceCard(spaceId); + + await PageObjects.spaceSelector.expectRoute(spaceId, '/app/kibana/#/dashboard'); + + await PageObjects.spaceSelector.openSpacesNav(); + + // change spaces + + await PageObjects.spaceSelector.clickSpaceAvatar('default'); + + await PageObjects.spaceSelector.expectRoute('default', '/app/canvas'); + }); + + it('falls back to the default home page when the configured default route is malformed', async () => { + await kibanaServer.uiSettings.replace({ defaultRoute: 'http://example.com/evil' }); + + // This test only works with the default space, as other spaces have an enforced relative url of `${serverBasePath}/s/space-id/${defaultRoute}` + const spaceId = 'default'; + + await PageObjects.security.login(null, null, { + expectSpaceSelector: true, + }); + + await PageObjects.spaceSelector.clickSpaceCard(spaceId); + + await PageObjects.spaceSelector.expectHomePage(spaceId); + }); + }); +} diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 7cc704a41becc2..7a876952fad83a 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -12,5 +12,6 @@ export default function spacesApp({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./copy_saved_objects')); loadTestFile(require.resolve('./feature_controls/spaces_security')); loadTestFile(require.resolve('./spaces_selection')); + loadTestFile(require.resolve('./enter_space')); }); } diff --git a/x-pack/test/functional/es_archives/spaces/enter_space/data.json b/x-pack/test/functional/es_archives/spaces/enter_space/data.json new file mode 100644 index 00000000000000..462a2a1ee38fe1 --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/enter_space/data.json @@ -0,0 +1,83 @@ +{ + "type": "doc", + "value": { + "id": "config:6.0.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "/app/canvas" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "id": "another-space:config:6.0.0", + "index": ".kibana", + "source": { + "namespace": "another-space", + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "/app/kibana/#dashboard" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "another-space:index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "namespace": "another-space", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "description": "This is the default space!", + "name": "Default" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:another-space", + "index": ".kibana", + "source": { + "space": { + "description": "This is another space", + "name": "Another Space" + }, + "type": "space" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/spaces/enter_space/mappings.json b/x-pack/test/functional/es_archives/spaces/enter_space/mappings.json new file mode 100644 index 00000000000000..f3793c7ca6780b --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/enter_space/mappings.json @@ -0,0 +1,287 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultRoute": { + "type": "keyword" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/space_selector_page.js b/x-pack/test/functional/page_objects/space_selector_page.js index 3be1ae174ce468..ad0f48bdd50bff 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.js +++ b/x-pack/test/functional/page_objects/space_selector_page.js @@ -28,14 +28,18 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }) { } async expectHomePage(spaceId) { + return await this.expectRoute(spaceId, `/app/kibana#/home`); + } + + async expectRoute(spaceId, route) { return await retry.try(async () => { - log.debug(`expectHomePage(${spaceId})`); + log.debug(`expectRoute(${spaceId}, ${route})`); await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); const url = await browser.getCurrentUrl(); if (spaceId === 'default') { - expect(url).to.contain(`/app/kibana#/home`); + expect(url).to.contain(route); } else { - expect(url).to.contain(`/s/${spaceId}/app/kibana#/home`); + expect(url).to.contain(`/s/${spaceId}${route}`); } }); } diff --git a/x-pack/test/spaces_api_integration/common/suites/select.ts b/x-pack/test/spaces_api_integration/common/suites/select.ts deleted file mode 100644 index 07471fe4e324f0..00000000000000 --- a/x-pack/test/spaces_api_integration/common/suites/select.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import { SuperTest } from 'supertest'; -import { DEFAULT_SPACE_ID } from '../../../../legacy/plugins/spaces/common/constants'; -import { getUrlPrefix } from '../lib/space_test_utils'; -import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; - -interface SelectTest { - statusCode: number; - response: (resp: { [key: string]: any }) => void; -} - -interface SelectTests { - default: SelectTest; -} - -interface SelectTestDefinition { - user?: TestDefinitionAuthentication; - currentSpaceId: string; - selectSpaceId: string; - tests: SelectTests; -} - -const nonExistantSpaceId = 'not-a-space'; - -export function selectTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const createExpectEmptyResult = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql(''); - }; - - const createExpectNotFoundResult = () => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - error: 'Not Found', - message: 'Not Found', - statusCode: 404, - }); - }; - - const createExpectRbacForbidden = (spaceId: any) => (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: `Unauthorized to get ${spaceId} space`, - }); - }; - - const createExpectResults = (spaceId: string) => (resp: { [key: string]: any }) => { - const allSpaces = [ - { - id: 'default', - name: 'Default Space', - description: 'This is the default space', - disabledFeatures: [], - _reserved: true, - }, - { - id: 'space_1', - name: 'Space 1', - description: 'This is the first test space', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - description: 'This is the second test space', - disabledFeatures: [], - }, - ]; - expect(resp.body).to.eql(allSpaces.find(space => space.id === spaceId)); - }; - - const createExpectSpaceResponse = (spaceId: string) => (resp: { [key: string]: any }) => { - if (spaceId === DEFAULT_SPACE_ID) { - expectDefaultSpaceResponse(resp); - } else { - expect(resp.body).to.eql({ - location: `/s/${spaceId}/app/kibana`, - }); - } - }; - - const expectDefaultSpaceResponse = (resp: { [key: string]: any }) => { - expect(resp.body).to.eql({ - location: `/app/kibana`, - }); - }; - - const makeSelectTest = (describeFn: DescribeFn) => ( - description: string, - { user = {}, currentSpaceId, selectSpaceId, tests }: SelectTestDefinition - ) => { - describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); - - it(`should return ${tests.default.statusCode}`, async () => { - return supertest - .post(`${getUrlPrefix(currentSpaceId)}/api/spaces/v1/space/${selectSpaceId}/select`) - .auth(user.username, user.password) - .expect(tests.default.statusCode) - .then(tests.default.response); - }); - }); - }; - - const selectTest = makeSelectTest(describe); - // @ts-ignore - selectTest.only = makeSelectTest(describe.only); - - return { - createExpectEmptyResult, - createExpectNotFoundResult, - createExpectRbacForbidden, - createExpectResults, - createExpectSpaceResponse, - expectDefaultSpaceResponse, - nonExistantSpaceId, - selectTest, - }; -} diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index 4493a5332b62c1..300949f41f0369 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -25,7 +25,6 @@ export default function({ loadTestFile, getService }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./select')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts deleted file mode 100644 index a905fe623a7c1d..00000000000000 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/select.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* - * 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 { AUTHENTICATION } from '../../common/lib/authentication'; -import { SPACES } from '../../common/lib/spaces'; -import { TestInvoker } from '../../common/lib/types'; -import { selectTestSuiteFactory } from '../../common/suites/select'; - -// eslint-disable-next-line import/no-default-export -export default function selectSpaceTestSuite({ getService }: TestInvoker) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { - selectTest, - nonExistantSpaceId, - createExpectSpaceResponse, - createExpectRbacForbidden, - createExpectNotFoundResult, - } = selectTestSuiteFactory(esArchiver, supertestWithoutAuth); - - describe('select', () => { - // Tests with users that have privileges globally in Kibana - [ - { - currentSpaceId: SPACES.DEFAULT.spaceId, - selectSpaceId: SPACES.SPACE_1.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - }, - }, - { - currentSpaceId: SPACES.SPACE_1.spaceId, - selectSpaceId: SPACES.DEFAULT.spaceId, - users: { - noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, - superuser: AUTHENTICATION.SUPERUSER, - allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, - legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, - dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, - dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, - }, - }, - ].forEach(scenario => { - selectTest( - `user with no access selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.noAccess, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `superuser selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.superuser, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all globally selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.allGlobally, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `dual-privileges user selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.dualAll, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `legacy user selects ${scenario.selectSpaceId} space from the ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.legacyAll, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `user with read globally selects ${scenario.selectSpaceId} space from the - ${scenario.currentSpaceId} space`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.readGlobally, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `dual-privileges readonly user selects ${scenario.selectSpaceId} space from - the ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.dualRead, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - }); - - // Select the same space that you're currently in with users which have space specific privileges. - // Our intent is to ensure that you have privileges at the space that you're selecting. - [ - { - spaceId: SPACES.DEFAULT.spaceId, - users: { - allAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - users: { - allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_READ_USER, - allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - ].forEach(scenario => { - selectTest( - `rbac user with all at space can select ${scenario.spaceId} - from the same space`, - { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.spaceId, - user: scenario.users.allAtSpace, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.spaceId), - }, - }, - } - ); - - selectTest( - `rbac user with read at space can select ${scenario.spaceId} - from the same space`, - { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.spaceId, - user: scenario.users.readAtSpace, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.spaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all at other space cannot select ${scenario.spaceId} - from the same space`, - { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.spaceId, - user: scenario.users.allAtOtherSpace, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.spaceId), - }, - }, - } - ); - }); - - // Select a different space with users that only have privileges at certain spaces. Our intent - // is to ensure that a user can select a space based on their privileges at the space that they're selecting - // not at the space that they're currently in. - [ - { - currentSpaceId: SPACES.SPACE_2.spaceId, - selectSpaceId: SPACES.SPACE_1.spaceId, - users: { - userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - userWithAllAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_2_ALL_USER, - userWithAllAtBothSpaces: AUTHENTICATION.KIBANA_RBAC_SPACE_1_2_ALL_USER, - }, - }, - ].forEach(scenario => { - selectTest( - `rbac user with all at ${scenario.selectSpaceId} can select ${scenario.selectSpaceId} - from ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtSpace, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all at both spaces can select ${scenario.selectSpaceId} - from ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtBothSpaces, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.selectSpaceId), - }, - }, - } - ); - - selectTest( - `rbac user with all at ${scenario.currentSpaceId} space cannot select ${scenario.selectSpaceId} - from ${scenario.currentSpaceId}`, - { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtOtherSpace, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - } - ); - }); - - // Select non-existent spaces and ensure we get a 404 or a 403 - describe('non-existent space', () => { - [ - { - currentSpaceId: SPACES.DEFAULT.spaceId, - selectSpaceId: nonExistantSpaceId, - users: { - userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, - }, - }, - { - currentSpaceId: SPACES.SPACE_1.spaceId, - selectSpaceId: nonExistantSpaceId, - users: { - userWithAllGlobally: AUTHENTICATION.KIBANA_RBAC_USER, - userWithAllAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_1_ALL_USER, - }, - }, - ].forEach(scenario => { - selectTest(`rbac user with all globally cannot access non-existent space`, { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllGlobally, - tests: { - default: { - statusCode: 404, - response: createExpectNotFoundResult(), - }, - }, - }); - - selectTest(`rbac user with all at space cannot access non-existent space`, { - currentSpaceId: scenario.currentSpaceId, - selectSpaceId: scenario.selectSpaceId, - user: scenario.users.userWithAllAtSpace, - tests: { - default: { - statusCode: 403, - response: createExpectRbacForbidden(scenario.selectSpaceId), - }, - }, - }); - }); - }); - }); -} diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts index 764d1cfae22b61..1182f6bdabcff4 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/index.ts @@ -17,7 +17,6 @@ export default function spacesOnlyTestSuite({ loadTestFile }: TestInvoker) { loadTestFile(require.resolve('./delete')); loadTestFile(require.resolve('./get_all')); loadTestFile(require.resolve('./get')); - loadTestFile(require.resolve('./select')); loadTestFile(require.resolve('./update')); }); } diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/select.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/select.ts deleted file mode 100644 index 82a60f7d455558..00000000000000 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/select.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { SPACES } from '../../common/lib/spaces'; -import { TestInvoker } from '../../common/lib/types'; -import { selectTestSuiteFactory } from '../../common/suites/select'; - -// eslint-disable-next-line import/no-default-export -export default function selectSpaceTestSuite({ getService }: TestInvoker) { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - const esArchiver = getService('esArchiver'); - - const { - selectTest, - createExpectSpaceResponse, - createExpectNotFoundResult, - nonExistantSpaceId, - } = selectTestSuiteFactory(esArchiver, supertestWithoutAuth); - - describe('select', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: SPACES.SPACE_1.spaceId, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: SPACES.DEFAULT.spaceId, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: SPACES.SPACE_2.spaceId, - }, - ].forEach(scenario => { - selectTest(`can select ${scenario.otherSpaceId} from ${scenario.spaceId}`, { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.otherSpaceId, - tests: { - default: { - statusCode: 200, - response: createExpectSpaceResponse(scenario.otherSpaceId), - }, - }, - }); - }); - - describe('non-existant space', () => { - [ - { - spaceId: SPACES.DEFAULT.spaceId, - otherSpaceId: nonExistantSpaceId, - }, - { - spaceId: SPACES.SPACE_1.spaceId, - otherSpaceId: nonExistantSpaceId, - }, - ].forEach(scenario => { - selectTest(`cannot select non-existant space from ${scenario.spaceId}`, { - currentSpaceId: scenario.spaceId, - selectSpaceId: scenario.otherSpaceId, - tests: { - default: { - statusCode: 404, - response: createExpectNotFoundResult(), - }, - }, - }); - }); - }); - }); -} From d243697e81778d42357602064eb47a36fe87abd5 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 2 Oct 2019 18:30:27 +0200 Subject: [PATCH 42/53] [Console] Fix Safari layout issue (#47100) * Remove 100% height, migrate console root to display flex * Remove unused import * Removed vendor prefixes in CSS --- .../public/application/containers/main/main.tsx | 10 ++++------ .../core_plugins/console/public/quarantined/_app.scss | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index d7b369cc264816..82256cf7398820 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { debounce } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; @@ -52,8 +52,6 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); - const containerRef = useRef(null); - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ INITIAL_PANEL_WIDTH, INITIAL_PANEL_WIDTH, @@ -71,9 +69,9 @@ export function Main() { }; return ( -
+ <> setShowSettings(false)} /> : null} {showHelp ? setShowHelp(false)} /> : null} -
+ ); } diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 5fd2cd080d06d8..6ec94c8fb4c96f 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,6 +1,7 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). #consoleRoot { - height: 100%; + display: flex; + flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container // SASSTODO: Uncomment when tooltips are EUI-ified (inside portals) overflow: hidden; From d935b3da085485aa380b70c7e02caea626f4bef8 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 2 Oct 2019 12:31:26 -0400 Subject: [PATCH 43/53] [Monitoring] Metricbeat Migration Wizard (last step!!) (#45799) * Enable setup mode UI toggles * We want to keep the no data page but update the copy * More updated copy * Remove manual checks for logstash, beats, apm and kibana * Hide the setup mode controls on the no data page. There is nothing different in setup mode * Setup mode test * Fix bug with disabling internal collection for ES * First steps towards the redesign of setup mode * Consolidate UI code, design changes, use constants defined in our plugin * Fix tooltips * Design/copy feedback * Use badge and onClick * More feedback * Only detect usage on the live cluster * Fix existing tests, remove test that will be added in other PR * Fix failing test * Fix issue with wrong callout showing * Ensure we check for live nodes if no cluster uuid is provided * We need a custom listing callout for ES * Custom callout for kibana instances * More space from the bottom bar * Disable switching if they enabled internal collection * Copy updates * Fix broken tests * Fix more tests * Fix i18n * Update copy * Fix a couple i18n issues * Fixing a couple of missing scenarios * Fix translations * Update snapshots * PR feedback * PR feedback * We also need totalUniqueInternallyCollectedCount to identify when we have detected products but they are not monitored (thinking ES and Kibana) * Remove why documentation link until we have the resource available * Ensure tabs are properly disabled * Address issue with the ES nodes callout not working at times * Ensure we check if setup mode is enabled * Change internal collection to self monitoring, and remove the word 'collection' usage * Only show Enter setup mode on pages with valid setup mode options * Copy updates * Copy updates * Ensure we update the top nav item when we toggle setup mode on or off --- .../plugins/monitoring/common/constants.js | 7 +- .../components/apm/instances/instances.js | 205 ++++++++++-------- .../components/beats/listing/listing.js | 94 ++++---- .../components/cluster/overview/apm_panel.js | 61 ++---- .../cluster/overview/beats_panel.js | 60 ++--- .../cluster/overview/elasticsearch_panel.js | 66 ++---- .../cluster/overview/kibana_panel.js | 59 ++--- .../cluster/overview/logstash_panel.js | 63 ++---- .../elasticsearch/cluster_status/index.js | 4 +- .../index_detail_status/index.js | 4 +- .../components/elasticsearch/nodes/nodes.js | 167 ++++++++------ .../components/kibana/instances/instances.js | 126 +++++++---- .../__snapshots__/listing.test.js.snap | 8 +- .../components/logstash/listing/listing.js | 101 +++++---- .../metricbeat_migration/flyout/flyout.js | 80 +++---- .../apm/common_apm_instructions.js | 10 - ...isable_internal_collection_instructions.js | 142 +----------- .../apm/enable_metricbeat_instructions.js | 87 +------- .../beats/common_beats_instructions.js | 5 - ...isable_internal_collection_instructions.js | 143 +----------- .../beats/enable_metricbeat_instructions.js | 87 +------- .../instruction_steps/common_instructions.js | 176 +++++++++++++++ .../common_elasticsearch_instructions.js | 14 -- ...isable_internal_collection_instructions.js | 85 +------- .../enable_metricbeat_instructions.js | 103 ++------- .../get_instruction_steps.js | 15 +- .../kibana/common_kibana_instructions.js | 14 -- ...isable_internal_collection_instructions.js | 150 +------------ .../kibana/enable_metricbeat_instructions.js | 87 +------- .../logstash/common_logstash_instructions.js | 10 - ...isable_internal_collection_instructions.js | 142 +----------- .../enable_metricbeat_instructions.js | 87 +------- .../__snapshots__/no_data.test.js.snap | 150 +++++++++---- .../public/components/no_data/no_data.js | 99 ++++++++- .../public/components/renderers/setup_mode.js | 68 +++++- .../public/components/setup_mode/badge.js | 124 +++++++++++ .../components/setup_mode/formatting.js | 56 +++++ .../components/setup_mode/listing_callout.js | 174 +++++++++++++++ .../public/components/setup_mode/tooltip.js | 141 ++++++++++++ .../public/components/table/eui_table.js | 158 +------------- .../public/directives/main/index.js | 12 +- .../monitoring/public/lib/route_init.js | 6 +- .../monitoring/public/lib/setup_mode.js | 108 +++++---- .../public/services/breadcrumbs_provider.js | 11 +- .../public/views/apm/instances/index.js | 7 +- .../public/views/beats/listing/index.js | 6 +- .../public/views/cluster/overview/index.js | 3 +- .../public/views/elasticsearch/nodes/index.js | 7 +- .../public/views/kibana/instances/index.js | 6 +- .../monitoring/public/views/loading/index.js | 20 +- .../public/views/logstash/nodes/index.js | 6 +- .../public/views/no_data/controller.js | 5 +- .../__test__/get_collection_status.js | 25 ++- .../setup/collection/get_collection_status.js | 108 +++++---- .../translations/translations/ja-JP.json | 112 ---------- .../translations/translations/zh-CN.json | 112 ---------- .../setup/collection/fixtures/detect_apm.json | 5 + .../collection/fixtures/detect_beats.json | 5 + .../fixtures/detect_beats_management.json | 5 + .../collection/fixtures/detect_logstash.json | 5 + .../fixtures/detect_logstash_management.json | 5 + .../fixtures/es_and_kibana_exclusive_mb.json | 5 + .../collection/fixtures/es_and_kibana_mb.json | 5 + .../fixtures/kibana_exclusive_mb.json | 5 + .../setup/collection/fixtures/kibana_mb.json | 5 + .../monitoring/elasticsearch/index_detail.js | 12 +- .../apps/monitoring/elasticsearch/indices.js | 4 +- .../apps/monitoring/elasticsearch/nodes.js | 8 +- .../apps/monitoring/elasticsearch/overview.js | 4 +- .../functional/services/monitoring/no_data.js | 1 + 70 files changed, 1790 insertions(+), 2270 deletions(-) delete mode 100644 x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/common_apm_instructions.js create mode 100644 x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/common_elasticsearch_instructions.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/common_kibana_instructions.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/common_logstash_instructions.js create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js create mode 100644 x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/common/constants.js b/x-pack/legacy/plugins/monitoring/common/constants.js index 98246141130942..f953741cd2e02d 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.js +++ b/x-pack/legacy/plugins/monitoring/common/constants.js @@ -154,8 +154,11 @@ export const INDEX_PATTERN_FILEBEAT = 'filebeat-*'; export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; // We use this for metricbeat migration to identify specific products that we do not have constants for -export const ELASTICSEARCH_CUSTOM_ID = 'elasticsearch'; -export const APM_CUSTOM_ID = 'apm'; +export const ELASTICSEARCH_SYSTEM_ID = 'elasticsearch'; +export const KIBANA_SYSTEM_ID = 'kibana'; +export const BEATS_SYSTEM_ID = 'beats'; +export const APM_SYSTEM_ID = 'apm'; +export const LOGSTASH_SYSTEM_ID = 'logstash'; /** * The id of the infra source owned by the monitoring plugin. */ diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js index 04b6652c6ce0ab..1a660934053634 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js @@ -8,80 +8,111 @@ import React, { Fragment } from 'react'; import moment from 'moment'; import { uniq, get } from 'lodash'; import { EuiMonitoringTable } from '../../table'; -import { EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { Status } from './status'; import { formatMetric } from '../../../lib/format_number'; import { formatTimestampToDuration } from '../../../../common'; import { i18n } from '@kbn/i18n'; +import { APM_SYSTEM_ID } from '../../../../common/constants'; +import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { SetupModeBadge } from '../../setup_mode/badge'; -const columns = [ - { - name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', { - defaultMessage: 'Name' - }), - field: 'name', - render: (name, instance) => ( - - {name} - - ) - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', { - defaultMessage: 'Output Enabled' - }), - field: 'output' - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.totalEventsRateTitle', { - defaultMessage: 'Total Events Rate' - }), - field: 'total_events_rate', - render: value => formatMetric(value, '', '/s') - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.bytesSentRateTitle', { - defaultMessage: 'Bytes Sent Rate' - }), - field: 'bytes_sent_rate', - render: value => formatMetric(value, 'byte', '/s') - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.outputErrorsTitle', { - defaultMessage: 'Output Errors' - }), - field: 'errors', - render: value => formatMetric(value, '0') - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.lastEventTitle', { - defaultMessage: 'Last Event' - }), - field: 'time_of_last_event', - render: value => i18n.translate('xpack.monitoring.apm.instances.lastEventValue', { - defaultMessage: '{timeOfLastEvent} ago', - values: { - timeOfLastEvent: formatTimestampToDuration(+moment(value), 'since') +function getColumns(setupMode) { + return [ + { + name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', { + defaultMessage: 'Name' + }), + field: 'name', + render: (name, apm) => { + let setupModeStatus = null; + if (setupMode && setupMode.enabled) { + const list = get(setupMode, 'data.byUuid', {}); + const status = list[apm.uuid] || {}; + const instance = { + uuid: apm.uuid, + name: apm.name + }; + + setupModeStatus = ( +
+ +
+ ); + } + + return ( + + + {name} + + {setupModeStatus} + + ); } - }) - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.allocatedMemoryTitle', { - defaultMessage: 'Allocated Memory' - }), - field: 'memory', - render: value => formatMetric(value, 'byte') - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.versionTitle', { - defaultMessage: 'Version' - }), - field: 'version' - }, -]; + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', { + defaultMessage: 'Output Enabled' + }), + field: 'output' + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.totalEventsRateTitle', { + defaultMessage: 'Total Events Rate' + }), + field: 'total_events_rate', + render: value => formatMetric(value, '', '/s') + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.bytesSentRateTitle', { + defaultMessage: 'Bytes Sent Rate' + }), + field: 'bytes_sent_rate', + render: value => formatMetric(value, 'byte', '/s') + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.outputErrorsTitle', { + defaultMessage: 'Output Errors' + }), + field: 'errors', + render: value => formatMetric(value, '0') + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.lastEventTitle', { + defaultMessage: 'Last Event' + }), + field: 'time_of_last_event', + render: value => i18n.translate('xpack.monitoring.apm.instances.lastEventValue', { + defaultMessage: '{timeOfLastEvent} ago', + values: { + timeOfLastEvent: formatTimestampToDuration(+moment(value), 'since') + } + }) + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.allocatedMemoryTitle', { + defaultMessage: 'Allocated Memory' + }), + field: 'memory', + render: value => formatMetric(value, 'byte') + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.versionTitle', { + defaultMessage: 'Version' + }), + field: 'version' + }, + ]; +} export function ApmServerInstances({ apms, setupMode }) { const { @@ -91,26 +122,14 @@ export function ApmServerInstances({ apms, setupMode }) { data, } = apms; - let detectedInstanceMessage = null; - if (setupMode.enabled && setupMode.data && get(setupMode.data, 'detected.mightExist')) { - detectedInstanceMessage = ( - - -

- {i18n.translate('xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceDescription', { - defaultMessage: `Based on your indices, we think you might have an APM server. Click the 'Setup monitoring' - button below to start monitoring this APM server.` - })} -

-
- -
+ let setupModeCallout = null; + if (setupMode.enabled && setupMode.data) { + setupModeCallout = ( + ); } @@ -124,19 +143,15 @@ export function ApmServerInstances({ apms, setupMode }) { - {detectedInstanceMessage} + {setupModeCallout} ( - { - scope.$evalAsync(() => { - kbnUrl.changePath(`/beats/beat/${beat.uuid}`); - }); - }} - data-test-subj={`beatLink-${name}`} - > - {name} - - ) + render: (name, beat) => { + let setupModeStatus = null; + if (setupMode && setupMode.enabled) { + const list = get(setupMode, 'data.byUuid', {}); + const status = list[beat.uuid] || {}; + const instance = { + uuid: beat.uuid, + name: beat.name + }; + + setupModeStatus = ( +
+ +
+ ); + } + + return ( +
+ { + scope.$evalAsync(() => { + kbnUrl.changePath(`/beats/beat/${beat.uuid}`); + }); + }} + data-test-subj={`beatLink-${name}`} + > + {name} + + {setupModeStatus} +
+ ); + } }, { name: i18n.translate('xpack.monitoring.beats.instances.typeTitle', { defaultMessage: 'Type' }), @@ -78,26 +108,14 @@ export class Listing extends PureComponent { setupMode } = this.props; - let detectedInstanceMessage = null; - if (setupMode.enabled && setupMode.data && get(setupMode.data, 'detected.mightExist')) { - detectedInstanceMessage = ( - - -

- {i18n.translate('xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceDescription', { - defaultMessage: `Based on your indices, we think you might have a beats instance. Click the 'Setup monitoring' - button below to start monitoring this instance.` - })} -

-
- -
+ let setupModeCallOut = null; + if (setupMode.enabled && setupMode.data) { + setupModeCallOut = ( + ); } @@ -115,16 +133,12 @@ export class Listing extends PureComponent { - {detectedInstanceMessage} + {setupModeCallOut} props.changeUrl('apm'); const goToInstances = () => props.changeUrl('apm/instances'); - const setupModeAPMData = get(setupMode.data, 'apm'); - let setupModeInstancesData = null; - if (setupMode.enabled && setupMode.data) { - const { - totalUniqueInstanceCount, - totalUniqueFullyMigratedCount, - totalUniquePartiallyMigratedCount - } = setupModeAPMData; - const hasInstances = totalUniqueInstanceCount > 0 || get(setupModeAPMData, 'detected.mightExist', false); - const allMonitoredByMetricbeat = totalUniqueInstanceCount > 0 && - (totalUniqueFullyMigratedCount === totalUniqueInstanceCount || totalUniquePartiallyMigratedCount === totalUniqueInstanceCount); - const internalCollectionOn = totalUniquePartiallyMigratedCount > 0; - if (hasInstances && (!allMonitoredByMetricbeat || internalCollectionOn)) { - let tooltipText = null; - - if (!allMonitoredByMetricbeat) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip.oneInternal', { - defaultMessage: `There's at least one server that isn't being monitored using Metricbeat. Click the flag - icon to visit the servers listing page and find out more information about the status of each server.` - }); - } - else if (internalCollectionOn) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip.disableInternal', { - defaultMessage: `All servers are being monitored using Metricbeat but internal collection still needs to be turned - off. Click the flag icon to visit the servers listing page and disable internal collection.` - }); - } - - setupModeInstancesData = ( - - - - - - - - ); - } - } + const setupModeData = get(setupMode.data, 'apm'); + const setupModeTooltip = setupMode && setupMode.enabled + ? ( + + ) + : null; return (
- {setupModeInstancesData} + {setupModeTooltip} diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js index 8ea987d0a67623..935ee1f9cc1001 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -18,12 +18,12 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, EuiFlexGroup, - EuiToolTip, - EuiIcon } from '@elastic/eui'; import { ClusterItemContainer, DisabledIfNoDataAndInSetupModeLink } from './helpers'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { SetupModeTooltip } from '../../setup_mode/tooltip'; +import { BEATS_SYSTEM_ID } from '../../../../common/constants'; export function BeatsPanel(props) { const { setupMode } = props; @@ -36,48 +36,16 @@ export function BeatsPanel(props) { const goToBeats = () => props.changeUrl('beats'); const goToInstances = () => props.changeUrl('beats/beats'); - const setupModeBeatsData = get(setupMode.data, 'beats'); - let setupModeInstancesData = null; - if (setupMode.enabled && setupMode.data) { - const { - totalUniqueInstanceCount, - totalUniqueFullyMigratedCount, - totalUniquePartiallyMigratedCount - } = setupModeBeatsData; - const hasInstances = totalUniqueInstanceCount > 0 || get(setupModeBeatsData, 'detected.mightExist', false); - const allMonitoredByMetricbeat = totalUniqueInstanceCount > 0 && - (totalUniqueFullyMigratedCount === totalUniqueInstanceCount || totalUniquePartiallyMigratedCount === totalUniqueInstanceCount); - const internalCollectionOn = totalUniquePartiallyMigratedCount > 0; - if (hasInstances && (!allMonitoredByMetricbeat || internalCollectionOn)) { - let tooltipText = null; - - if (!allMonitoredByMetricbeat) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip.oneInternal', { - defaultMessage: `There's at least one instance that isn't being monitored using Metricbeat. Click the flag - icon to visit the instances listing page and find out more information about the status of each instance.` - }); - } - else if (internalCollectionOn) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip.disableInternal', { - defaultMessage: `All instances are being monitored using Metricbeat but internal collection still needs to be turned - off. Click the flag icon to visit the instances listing page and disable internal collection.` - }); - } - - setupModeInstancesData = ( - - - - - - - - ); - } - } + const setupModeData = get(setupMode.data, 'beats'); + const setupModeTooltip = setupMode && setupMode.enabled + ? ( + + ) + : null; const beatTypes = props.beats.types.map((beat, index) => { return [ @@ -111,7 +79,7 @@ export function BeatsPanel(props) {

- {setupModeInstancesData} + {setupModeTooltip} diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 3f45d6e07297ca..9bf18adf50069f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -27,12 +27,13 @@ import { EuiBadge, EuiToolTip, EuiFlexGroup, - EuiIcon } from '@elastic/eui'; import { LicenseText } from './license_text'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from '../../logs/reason'; +import { SetupModeTooltip } from '../../setup_mode/tooltip'; +import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; const calculateShards = shards => { const total = get(shards, 'total', 0); @@ -160,47 +161,16 @@ export function ElasticsearchPanel(props) { const licenseText = ; - const setupModeElasticsearchData = get(setupMode.data, 'elasticsearch'); - let setupModeNodesData = null; - if (setupMode.enabled && setupModeElasticsearchData) { - const { - totalUniqueInstanceCount, - totalUniqueFullyMigratedCount, - totalUniquePartiallyMigratedCount - } = setupModeElasticsearchData; - const allMonitoredByMetricbeat = totalUniqueInstanceCount > 0 && - (totalUniqueFullyMigratedCount === totalUniqueInstanceCount || totalUniquePartiallyMigratedCount === totalUniqueInstanceCount); - const internalCollectionOn = totalUniquePartiallyMigratedCount > 0; - if (!allMonitoredByMetricbeat || internalCollectionOn) { - let tooltipText = null; - - if (!allMonitoredByMetricbeat) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.elasticsearchPanel.setupModeNodesTooltip.oneInternal', { - defaultMessage: `There's at least one node that isn't being monitored using Metricbeat. Click the flag icon to visit the nodes - listing page and find out more information about the status of each node.` - }); - } - else if (internalCollectionOn) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.elasticsearchPanel.setupModeNodesTooltip.disableInternal', { - defaultMessage: `All nodes are being monitored using Metricbeat but internal collection still needs to be turned off. Click the - flag icon to visit the nodes listing page and disable internal collection.` - }); - } - - setupModeNodesData = ( - - - - - - - - ); - } - } + const setupModeData = get(setupMode.data, 'elasticsearch'); + const setupModeTooltip = setupMode && setupMode.enabled + ? ( + + ) + : null; const showMlJobs = () => { // if license doesn't support ML, then `ml === null` @@ -211,7 +181,7 @@ export function ElasticsearchPanel(props) { {props.ml.jobs} @@ -251,7 +221,7 @@ export function ElasticsearchPanel(props) {

- {setupModeNodesData} + {setupModeTooltip} @@ -353,7 +323,7 @@ export function ElasticsearchPanel(props) {

props.changeUrl('kibana'); const goToInstances = () => props.changeUrl('kibana/instances'); - const setupModeKibanaData = get(setupMode.data, 'kibana'); - let setupModeInstancesData = null; - if (setupMode.enabled && setupMode.data) { - const { - totalUniqueInstanceCount, - totalUniqueFullyMigratedCount, - totalUniquePartiallyMigratedCount - } = setupModeKibanaData; - const allMonitoredByMetricbeat = totalUniqueInstanceCount > 0 && - (totalUniqueFullyMigratedCount === totalUniqueInstanceCount || totalUniquePartiallyMigratedCount === totalUniqueInstanceCount); - const internalCollectionOn = totalUniquePartiallyMigratedCount > 0; - if (!allMonitoredByMetricbeat || internalCollectionOn) { - let tooltipText = null; - - if (!allMonitoredByMetricbeat) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.kibanaPanel.setupModeNodesTooltip.oneInternal', { - defaultMessage: `There's at least one instance that isn't being monitored using Metricbeat. Click the flag - icon to visit the instances listing page and find out more information about the status of each instance.` - }); - } - else if (internalCollectionOn) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.kibanaPanel.setupModeNodesTooltip.disableInternal', { - defaultMessage: `All instances are being monitored using Metricbeat but internal collection still needs to be turned - off. Click the flag icon to visit the instances listing page and disable internal collection.` - }); - } - - setupModeInstancesData = ( - - - - - - - - ); - } - } + const setupModeData = get(setupMode.data, 'kibana'); + const setupModeTooltip = setupMode && setupMode.enabled + ? ( + + ) + : null; return ( - {setupModeInstancesData} + {setupModeTooltip} diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index ff647c1c219aa4..1033f178b7010c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -7,7 +7,7 @@ import React from 'react'; import { formatNumber } from 'plugins/monitoring/lib/format_number'; import { ClusterItemContainer, BytesPercentageUsage, DisabledIfNoDataAndInSetupModeLink } from './helpers'; -import { LOGSTASH } from '../../../../common/constants'; +import { LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; import { EuiFlexGrid, @@ -21,12 +21,11 @@ import { EuiDescriptionListDescription, EuiHorizontalRule, EuiIconTip, - EuiToolTip, - EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; +import { SetupModeTooltip } from '../../setup_mode/tooltip'; export function LogstashPanel(props) { const { setupMode } = props; @@ -42,48 +41,16 @@ export function LogstashPanel(props) { const goToNodes = () => props.changeUrl('logstash/nodes'); const goToPipelines = () => props.changeUrl('logstash/pipelines'); - const setupModeLogstashData = get(setupMode.data, 'logstash'); - let setupModeInstancesData = null; - if (setupMode.enabled && setupMode.data) { - const { - totalUniqueInstanceCount, - totalUniqueFullyMigratedCount, - totalUniquePartiallyMigratedCount - } = setupModeLogstashData; - const hasInstances = totalUniqueInstanceCount > 0 || get(setupModeLogstashData, 'detected.mightExist', false); - const allMonitoredByMetricbeat = totalUniqueInstanceCount > 0 && - (totalUniqueFullyMigratedCount === totalUniqueInstanceCount || totalUniquePartiallyMigratedCount === totalUniqueInstanceCount); - const internalCollectionOn = totalUniquePartiallyMigratedCount > 0; - if (hasInstances && (!allMonitoredByMetricbeat || internalCollectionOn)) { - let tooltipText = null; - - if (!allMonitoredByMetricbeat) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip.oneInternal', { - defaultMessage: `There's at least one node that isn't being monitored using Metricbeat. Click the flag - icon to visit the nodes listing page and find out more information about the status of each node.` - }); - } - else if (internalCollectionOn) { - tooltipText = i18n.translate('xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip.disableInternal', { - defaultMessage: `All nodes are being monitored using Metricbeat but internal collection still needs to be turned - off. Click the flag icon to visit the nodes listing page and disable internal collection.` - }); - } - - setupModeInstancesData = ( - - - - - - - - ); - } - } + const setupModeData = get(setupMode.data, 'logstash'); + const setupModeTooltip = setupMode && setupMode.enabled + ? ( + + ) + : null; return ( - {setupModeInstancesData} + {setupModeTooltip} @@ -198,7 +165,7 @@ export function LogstashPanel(props) {

(item) => _.get(item, [type, 'summary', 'lastVal']); -const getColumns = (showCgroupMetricsElasticsearch, setupMode) => { +const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { const cols = []; const cpuUsageColumnTitle = i18n.translate('xpack.monitoring.elasticsearch.nodes.cpuUsageColumnTitle', { @@ -50,9 +53,26 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode) => { ); + let setupModeStatus = null; if (setupMode && setupMode.enabled) { const list = _.get(setupMode, 'data.byUuid', {}); const status = list[node.resolver] || {}; + const instance = { + uuid: node.resolver, + name: node.name + }; + + setupModeStatus = ( +
+ +
+ ); if (status.isNetNewUser) { nameLink = value; } @@ -77,6 +97,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode) => {
{extractIp(node.transport_address)}
+ {setupModeStatus}

); } @@ -223,7 +244,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode) => { export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsearch, ...props }) { const { sorting, pagination, onTableChange, clusterUuid, setupMode } = props; - const columns = getColumns(showCgroupMetricsElasticsearch, setupMode); + const columns = getColumns(showCgroupMetricsElasticsearch, setupMode, clusterUuid); // Merge the nodes data with the setup data if enabled const nodes = props.nodes || []; @@ -238,70 +259,89 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear nodes.push(...Object.entries(setupMode.data.byUuid) .reduce((nodes, [nodeUuid, instance]) => { - if (!nodesByUuid[nodeUuid]) { + if (!nodesByUuid[nodeUuid] && instance.node) { nodes.push(instance.node); } return nodes; }, [])); } - let netNewUserMessage = null; - let disableInternalCollectionForMigrationMessage = null; - if (setupMode.data) { - // Think net new user scenario - const hasInstances = setupMode.data.totalUniqueInstanceCount > 0; - if (hasInstances && setupMode.data.totalUniquePartiallyMigratedCount === setupMode.data.totalUniqueInstanceCount) { - const finishMigrationAction = _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid - ? setupMode.shortcutToFinishMigration - : setupMode.openFlyout; + let setupModeCallout = null; + if (setupMode.enabled && setupMode.data) { + setupModeCallout = ( + { + const customRenderResponse = { + shouldRender: false, + componentToRender: null + }; - disableInternalCollectionForMigrationMessage = ( - - -

- {i18n.translate('xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionDescription', { - defaultMessage: `All of your Elasticsearch servers are monitored using Metricbeat, - but you need to disable internal collection to finish the migration.` - })} -

- - {i18n.translate('xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionMigrationButtonLabel', { - defaultMessage: 'Disable and finish migration' - })} - -
- -
- ); - } - else if (!hasInstances) { - netNewUserMessage = ( - - -

- {i18n.translate('xpack.monitoring.elasticsearch.nodes.metribeatMigration.netNewUserDescription', { - defaultMessage: `We did not detect any monitoring data, but we did detect the following Elasticsearch nodes. - Each detected node is listed below along with a Setup button. Clicking this button will guide you through - the process of enabling monitoring for each node.` - })} -

-
- -
- ); - } + const isNetNewUser = setupMode.data.totalUniqueInstanceCount === 0; + const hasNoInstances = setupMode.data.totalUniqueInternallyCollectedCount === 0 + && setupMode.data.totalUniqueFullyMigratedCount === 0 + && setupMode.data.totalUniquePartiallyMigratedCount === 0; + + if (isNetNewUser || hasNoInstances) { + customRenderResponse.shouldRender = true; + customRenderResponse.componentToRender = ( + + 0 ? 'danger' : 'warning'} + iconType="flag" + > +

+ {i18n.translate('xpack.monitoring.elasticsearch.nodes.metricbeatMigration.detectedNodeDescription', { + defaultMessage: `The following nodes are not monitored. Click 'Monitor with Metricbeat' below to start monitoring.`, + })} +

+
+ +
+ ); + } + else if (setupMode.data.totalUniquePartiallyMigratedCount === setupMode.data.totalUniqueInstanceCount) { + const finishMigrationAction = _.get(setupMode.meta, 'liveClusterUuid') === clusterUuid + ? setupMode.shortcutToFinishMigration + : setupMode.openFlyout; + + customRenderResponse.shouldRender = true; + customRenderResponse.componentToRender = ( + + +

+ {i18n.translate('xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionDescription', { + defaultMessage: `Disable self monitoring to finish the migration.` + })} +

+ + {i18n.translate( + 'xpack.monitoring.elasticsearch.nodes.metricbeatMigration.disableInternalCollectionMigrationButtonLabel', { + defaultMessage: 'Disable self monitoring' + } + )} + +
+ +
+ ); + } + + return customRenderResponse; + }} + /> + ); } function renderClusterStatus() { @@ -322,8 +362,7 @@ export function ElasticsearchNodes({ clusterStatus, showCgroupMetricsElasticsear {renderClusterStatus()} - {disableInternalCollectionForMigrationMessage} - {netNewUserMessage} + {setupModeCallout} { const columns = [ @@ -31,25 +34,50 @@ const getColumns = (kbnUrl, scope, setupMode) => { }), field: 'name', render: (name, kibana) => { + let setupModeStatus = null; if (setupMode && setupMode.enabled) { const list = get(setupMode, 'data.byUuid', {}); - const status = list[get(kibana, 'kibana.uuid')] || {}; + const uuid = get(kibana, 'kibana.uuid'); + const status = list[uuid] || {}; + const instance = { + uuid, + name: kibana.name + }; + + setupModeStatus = ( +
+ +
+ ); if (status.isNetNewUser) { - return name; + return ( +
+ {name} + {setupModeStatus} +
+ ); } } return ( - { - scope.$evalAsync(() => { - kbnUrl.changePath(`/kibana/instances/${kibana.kibana.uuid}`); - }); - }} - data-test-subj={`kibanaLink-${name}`} - > - { name } - +
+ { + scope.$evalAsync(() => { + kbnUrl.changePath(`/kibana/instances/${kibana.kibana.uuid}`); + }); + }} + data-test-subj={`kibanaLink-${name}`} + > + { name } + + {setupModeStatus} +
); } }, @@ -152,7 +180,7 @@ export class KibanaInstances extends PureComponent { onTableChange } = this.props; - let netNewUserMessage = null; + let setupModeCallOut = null; // Merge the instances data with the setup data if enabled const instances = this.props.instances || []; if (setupMode.enabled && setupMode.data) { @@ -177,29 +205,45 @@ export class KibanaInstances extends PureComponent { return instances; }, [])); - const hasInstances = setupMode.data.totalUniqueInstanceCount > 0; - if (!hasInstances) { - netNewUserMessage = ( - - -

- {i18n.translate('xpack.monitoring.kibana.nodes.metribeatMigration.netNewUserDescription', { - defaultMessage: `We did not detect any monitoring data, but we did detect the following Kibana instance. - This detected instance is listed below along with a Setup button. Clicking this button will guide you through - the process of enabling monitoring for this instance.` - })} -

-
- -
- ); - } + setupModeCallOut = ( + { + const customRenderResponse = { + shouldRender: false, + componentToRender: null + }; + + const hasInstances = setupMode.data.totalUniqueInstanceCount > 0; + if (!hasInstances) { + customRenderResponse.shouldRender = true; + customRenderResponse.componentToRender = ( + + +

+ {i18n.translate('xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription', { + defaultMessage: `The following instances are not monitored. + Click 'Monitor with Metricbeat' below to start monitoring.`, + })} +

+
+ +
+ ); + } + + return customRenderResponse; + }} + /> + ); } const dataFlattened = instances.map(item => ({ @@ -216,7 +260,7 @@ export class KibanaInstances extends PureComponent { - {netNewUserMessage} + {setupModeCallOut} `; @@ -152,7 +150,7 @@ exports[`Listing should render with expected props 1`] = ` ], } } - nameField="name" + productName="logstash" rows={ Array [ Object { @@ -211,7 +209,6 @@ exports[`Listing should render with expected props 1`] = ` } } setupMode={Object {}} - setupNewButtonLabel="Setup monitoring for new Logstash node" sorting={ Object { "sort": Object { @@ -222,6 +219,5 @@ exports[`Listing should render with expected props 1`] = ` }, } } - uuidField="logstash.uuid" /> `; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js index bc51d6278c1421..94f83e4046c618 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { PureComponent, Fragment } from 'react'; +import React, { PureComponent } from 'react'; import { get } from 'lodash'; -import { EuiPage, EuiLink, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { EuiPage, EuiLink, EuiPageBody, EuiPageContent, EuiPanel, EuiSpacer } from '@elastic/eui'; import { formatPercentageUsage, formatNumber } from '../../../lib/format_number'; import { ClusterStatus } from '..//cluster_status'; import { EuiMonitoringTable } from '../../table'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { SetupModeBadge } from '../../setup_mode/badge'; +import { ListingCallOut } from '../../setup_mode/listing_callout'; export class Listing extends PureComponent { getColumns() { const { kbnUrl, scope } = this.props.angular; + const setupMode = this.props.setupMode; return [ { @@ -24,24 +28,49 @@ export class Listing extends PureComponent { }), field: 'name', sortable: true, - render: (name, node) => ( -
-
- { - scope.$evalAsync(() => { - kbnUrl.changePath(`/logstash/node/${node.logstash.uuid}`); - }); - }} - > - {name} - -
+ render: (name, node) => { + let setupModeStatus = null; + if (setupMode && setupMode.enabled) { + const list = get(setupMode, 'data.byUuid', {}); + const uuid = get(node, 'logstash.uuid'); + const status = list[uuid] || {}; + const instance = { + uuid, + name: node.name + }; + + setupModeStatus = ( +
+ +
+ ); + } + + return (
- {node.logstash.http_address} +
+ { + scope.$evalAsync(() => { + kbnUrl.changePath(`/logstash/node/${node.logstash.uuid}`); + }); + }} + > + {name} + +
+
+ {node.logstash.http_address} +
+ {setupModeStatus}
-
- ) + ); + } }, { name: i18n.translate('xpack.monitoring.logstash.nodes.cpuUsageTitle', { @@ -124,26 +153,14 @@ export class Listing extends PureComponent { version: get(item, 'logstash.version', 'N/A'), })); - let netNewUserMessage = null; - if (setupMode.enabled && setupMode.data && get(setupMode.data, 'detected.mightExist')) { - netNewUserMessage = ( - - -

- {i18n.translate('xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserDescription', { - defaultMessage: `Based on your indices, we think you might have a Logstash node. Click the 'Setup monitoring' - button below to start monitoring this node.` - })} -

-
- -
+ let setupModeCallOut = null; + if (setupMode.enabled && setupMode.data) { + setupModeCallOut = ( + ); } @@ -154,17 +171,13 @@ export class Listing extends PureComponent { - {netNewUserMessage} + {setupModeCallOut} `"${url}"`); const instructionSteps = getInstructionSteps(productName, product, activeStep, meta, { doneWithMigration: onClose, - esMonitoringUrl, + esMonitoringUrl: esMonitoringUrls, hasCheckedStatus: checkedStatusByStep[activeStep], }); @@ -142,7 +143,7 @@ export class Flyout extends Component { let willShowNextButton = activeStep !== INSTRUCTION_STEP_DISABLE_INTERNAL; if (activeStep === INSTRUCTION_STEP_ENABLE_METRICBEAT) { - if (productName === ELASTICSEARCH_CUSTOM_ID) { + if (productName === ELASTICSEARCH_SYSTEM_ID) { willShowNextButton = false; // ES can be fully migrated for net new users willDisableDoneButton = !product.isPartiallyMigrated && !product.isFullyMigrated; @@ -222,7 +223,7 @@ export class Flyout extends Component { if (productName === KIBANA_SYSTEM_ID) { documentationUrl = `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`; } - else if (productName === ELASTICSEARCH_CUSTOM_ID) { + else if (productName === ELASTICSEARCH_SYSTEM_ID) { documentationUrl = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html`; } @@ -233,7 +234,9 @@ export class Flyout extends Component { return ( - Read more about this migration. + {i18n.translate('xpack.monitoring.metricbeatMigration.flyout.learnMore', { + defaultMessage: 'Learn about why.' + })} ); @@ -242,59 +245,29 @@ export class Flyout extends Component { render() { const { onClose, instance, productName, product } = this.props; - let instanceType = null; - let instanceName = instance ? instance.name : null; - - if (productName === KIBANA_SYSTEM_ID) { - instanceType = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.kibanaInstance', { - defaultMessage: 'instance', - }); - } - else if (productName === ELASTICSEARCH_CUSTOM_ID) { - if (instance) { - instanceType = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.elasticsearchNode', { - defaultMessage: 'node', - }); - } - else { - instanceName = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.elasticsearchNodesTitle', { - defaultMessage: 'Elasticsearch nodes', - }); - } - } + const instanceIdentifier = getIdentifier(productName); + const instanceName = (instance && instance.name) || formatProductName(productName); let title = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.flyoutTitle', { - defaultMessage: 'Migrate {instanceName} {instanceType} to Metricbeat', + defaultMessage: 'Monitor `{instanceName}` {instanceIdentifier} with Metricbeat', values: { instanceName, - instanceType + instanceIdentifier } }); if (product.isNetNewUser) { title = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.flyoutTitleNewUser', { - defaultMessage: 'Monitor {instanceName} {instanceType} with Metricbeat', + defaultMessage: 'Monitor {instanceName} {instanceIdentifier} with Metricbeat', values: { - instanceName, - instanceType + instanceIdentifier, + instanceName } }); } let noClusterUuidPrompt = null; if (product.isFullyMigrated && product.clusterUuid === null) { - const nodeText = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.node', { - defaultMessage: 'node' - }); - const instanceText = i18n.translate('xpack.monitoring.metricbeatMigration.flyout.instance', { - defaultMessage: 'instance' - }); - - let typeText = nodeText; - if (productName === BEATS_SYSTEM_ID) { - typeText = instanceText; - } - noClusterUuidPrompt = ( Click here to view the Standalone cluster. @@ -330,10 +303,10 @@ export class Flyout extends Component { 'xpack.monitoring.metricbeatMigration.flyout.noClusterUuidCheckboxLabel', { defaultMessage: `Yes, I understand that I will need to look in the Standalone cluster for - this {productName} {typeText}.`, + this {productName} {instanceIdentifier}.`, values: { productName, - typeText + instanceIdentifier } } )} @@ -357,13 +330,14 @@ export class Flyout extends Component { {title} - {this.getDocumentationTitle()} + {/* Remove until we have a why article: https://github.com/elastic/kibana/pull/45799#issuecomment-536778656 */} + {/* {this.getDocumentationTitle()} */} {this.renderActiveStep()} {noClusterUuidPrompt} - + @@ -65,130 +54,7 @@ export function getApmInstructionsForDisablingInternalCollection(product, meta, ) }; - let migrationStatusStep = null; - if (!product || !product.isFullyMigrated) { - let status = null; - if (hasCheckedStatus) { - let lastInternallyCollectedMessage = ''; - // It is possible that, during the migration steps, products are not reporting - // monitoring data for a period of time outside the window of our server-side check - // and this is most likely temporary so we want to be defensive and not error out - // and hopefully wait for the next check and this state will be self-corrected. - if (product) { - const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp; - const secondsSinceLastInternalCollectionLabel = - formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE); - lastInternallyCollectedMessage = (); - } - - status = ( - - - -

- -

-

- {lastInternallyCollectedMessage} -

-
-
- ); - } - - let buttonLabel; - if (checkingMigrationStatus) { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkingStatusButtonLabel', - { - defaultMessage: 'Checking...' - } - ); - } else { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkStatusButtonLabel', - { - defaultMessage: 'Check' - } - ); - } - - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - - - -

- {i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.statusDescription', - { - defaultMessage: 'Check that no documents are coming from internal collection.' - } - )} -

-
-
- - - {buttonLabel} - - -
- {status} -
- ) - }; - } - else { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getDisableStatusStep(product, meta); return [ disableInternalCollectionStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js index 3f27cdd35ace0d..eaf7066c92e65a 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js @@ -9,50 +9,20 @@ import { EuiSpacer, EuiCodeBlock, EuiLink, - EuiCallOut, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle } from './common_apm_instructions'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl, }) { - const securitySetup = ( - - - - - {` `} - - - - - ) - }} - /> - - )} - /> -
+ const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/apm/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` ); + const installMetricbeatStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatTitle', { defaultMessage: 'Install Metricbeat on the same server as the APM server' @@ -66,7 +36,7 @@ export function getApmInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -130,7 +100,7 @@ export function getApmInstructionsForEnablingMetricbeat(product, _meta, { isCopyable > {`output.elasticsearch: - hosts: ["${esMonitoringUrl}"] ## Monitoring cluster + hosts: [${esMonitoringUrl}] ## Monitoring cluster # Optional protocol and basic auth credentials. #protocol: "https" @@ -157,7 +127,7 @@ export function getApmInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -165,48 +135,7 @@ export function getApmInstructionsForEnablingMetricbeat(product, _meta, { ) }; - let migrationStatusStep = null; - if (product.isInternalCollector || product.isNetNewUser) { - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - ) - }; - } - else if (product.isPartiallyMigrated || product.isFullyMigrated) { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getMigrationStatusStep(product); return [ installMetricbeatStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js index 0ada632f9779e0..8953b8a858d43e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js @@ -3,11 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; - -export const statusTitle = i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.statusTitle', { - defaultMessage: `Migration status` -}); export const UNDETECTED_BEAT_TYPE = 'beat'; export const DEFAULT_BEAT_FOR_URLS = 'metricbeat'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js index 4a843ff286598a..b7400f2a798cad 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js @@ -8,28 +8,18 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, EuiText } from '@elastic/eui'; -import { formatTimestampToDuration } from '../../../../../common'; -import { CALCULATE_DURATION_SINCE } from '../../../../../common/constants'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle, UNDETECTED_BEAT_TYPE } from './common_beats_instructions'; +import { UNDETECTED_BEAT_TYPE } from './common_beats_instructions'; +import { getDisableStatusStep } from '../common_instructions'; -export function getBeatsInstructionsForDisablingInternalCollection(product, meta, { - checkForMigrationStatus, - checkingMigrationStatus, - hasCheckedStatus, - autoCheckIntervalInMs, -}) { +export function getBeatsInstructionsForDisablingInternalCollection(product, meta) { const beatType = product.beatType; const disableInternalCollectionStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.title', { - defaultMessage: 'Disable internal collection of {beatType}\'s monitoring metrics', + defaultMessage: 'Disable self monitoring of {beatType}\'s monitoring metrics', values: { beatType: beatType || UNDETECTED_BEAT_TYPE } @@ -73,130 +63,7 @@ export function getBeatsInstructionsForDisablingInternalCollection(product, meta ) }; - let migrationStatusStep = null; - if (!product || !product.isFullyMigrated) { - let status = null; - if (hasCheckedStatus) { - let lastInternallyCollectedMessage = ''; - // It is possible that, during the migration steps, products are not reporting - // monitoring data for a period of time outside the window of our server-side check - // and this is most likely temporary so we want to be defensive and not error out - // and hopefully wait for the next check and this state will be self-corrected. - if (product) { - const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp; - const secondsSinceLastInternalCollectionLabel = - formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE); - lastInternallyCollectedMessage = (); - } - - status = ( - - - -

- -

-

- {lastInternallyCollectedMessage} -

-
-
- ); - } - - let buttonLabel; - if (checkingMigrationStatus) { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkingStatusButtonLabel', - { - defaultMessage: 'Checking...' - } - ); - } else { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkStatusButtonLabel', - { - defaultMessage: 'Check' - } - ); - } - - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - - - -

- {i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.statusDescription', - { - defaultMessage: 'Check that no documents are coming from internal collection.' - } - )} -

-
-
- - - {buttonLabel} - - -
- {status} -
- ) - }; - } - else { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getDisableStatusStep(product, meta); return [ disableInternalCollectionStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js index 8d167379615d5c..f36fb49521a1ea 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js @@ -14,46 +14,18 @@ import { } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle, UNDETECTED_BEAT_TYPE, DEFAULT_BEAT_FOR_URLS } from './common_beats_instructions'; +import { UNDETECTED_BEAT_TYPE, DEFAULT_BEAT_FOR_URLS } from './common_beats_instructions'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl, }) { const beatType = product.beatType; - const securitySetup = ( - - - - - {` `} - - - - - ) - }} - /> - - )} - /> - + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/beats/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` ); + const installMetricbeatStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatTitle', { defaultMessage: 'Install Metricbeat on the same server as this {beatType}', @@ -70,7 +42,7 @@ export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -170,7 +142,7 @@ export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { isCopyable > {`output.elasticsearch: - hosts: ["${esMonitoringUrl}"] ## Monitoring cluster + hosts: [${esMonitoringUrl}] ## Monitoring cluster # Optional protocol and basic auth credentials. #protocol: "https" @@ -197,7 +169,7 @@ export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -205,48 +177,7 @@ export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { ) }; - let migrationStatusStep = null; - if (product.isInternalCollector || product.isNetNewUser) { - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - ) - }; - } - else if (product.isPartiallyMigrated || product.isFullyMigrated) { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getMigrationStatusStep(product); return [ installMetricbeatStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js new file mode 100644 index 00000000000000..f263837b80cc3c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js @@ -0,0 +1,176 @@ +/* + * 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, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCallOut, + EuiSpacer, + EuiText, + EuiLink +} from '@elastic/eui'; +import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; +import { formatTimestampToDuration } from '../../../../common'; + +export const MIGRATION_STATUS_LABEL = i18n.translate('xpack.monitoring.metricbeatMigration.migrationStatus', { + defaultMessage: `Migration status` +}); + +export const MONITORING_STATUS_LABEL = i18n.translate('xpack.monitoring.metricbeatMigration.monitoringStatus', { + defaultMessage: `Monitoring status` +}); + +export function getSecurityStep(url) { + return ( + + + + + {` `} + + + + + ) + }} + /> + + )} + /> + + ); +} + +export function getMigrationStatusStep(product) { + if (product.isInternalCollector || product.isNetNewUser) { + return { + title: product.isNetNewUser ? MONITORING_STATUS_LABEL : MIGRATION_STATUS_LABEL, + status: 'incomplete', + children: ( + + ) + }; + } + else if (product.isPartiallyMigrated || product.isFullyMigrated) { + return { + title: MIGRATION_STATUS_LABEL, + status: 'complete', + children: ( + +

+ {i18n.translate('xpack.monitoring.metricbeatMigration.fullyMigratedStatusDescription', { + defaultMessage: 'Metricbeat is shipping monitoring data.' + })} +

+
+ ) + }; + } + + return null; +} + +export function getDisableStatusStep(product, meta) { + if (!product || !product.isFullyMigrated) { + let lastInternallyCollectedMessage = ''; + // It is possible that, during the migration steps, products are not reporting + // monitoring data for a period of time outside the window of our server-side check + // and this is most likely temporary so we want to be defensive and not error out + // and hopefully wait for the next check and this state will be self-corrected. + if (product) { + const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp; + const secondsSinceLastInternalCollectionLabel = + formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE); + lastInternallyCollectedMessage = i18n.translate( + 'xpack.monitoring.metricbeatMigration.disableInternalCollection.partiallyMigratedStatusDescription', + { + defaultMessage: 'Last self monitoring was {secondsSinceLastInternalCollectionLabel} ago.', + values: { + secondsSinceLastInternalCollectionLabel + } + } + ); + } + + return { + title: MIGRATION_STATUS_LABEL, + status: 'incomplete', + children: ( + +

+ {i18n.translate('xpack.monitoring.metricbeatMigration.partiallyMigratedStatusDescription', { + defaultMessage: `It can take up to {secondsAgo} seconds to detect data.`, + values: { + secondsAgo: meta.secondsAgo + } + })} +

+

+ {lastInternallyCollectedMessage} +

+
+ ) + }; + } + + return { + title: MIGRATION_STATUS_LABEL, + status: 'complete', + children: ( + +

+ {i18n.translate('xpack.monitoring.metricbeatMigration.disableInternalCollection.fullyMigratedStatusDescription', { + defaultMessage: 'We are not seeing any documents from self monitoring. Migration complete!' + })} +

+
+ ) + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/common_elasticsearch_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/common_elasticsearch_instructions.js deleted file mode 100644 index 3c55fef3ab7f3f..00000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/common_elasticsearch_instructions.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export const statusTitle = i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitle', { - defaultMessage: `Migration status` -}); - -export const statusTitleNewUser = i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitleNewUser', { - defaultMessage: `Monitoring status` -}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js index d09f134b1d2991..361b1262f4481f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js @@ -8,19 +8,16 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, - EuiCallOut, EuiText } from '@elastic/eui'; -import { formatTimestampToDuration } from '../../../../../common'; -import { CALCULATE_DURATION_SINCE } from '../../../../../common/constants'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle } from './common_elasticsearch_instructions'; +import { getDisableStatusStep } from '../common_instructions'; export function getElasticsearchInstructionsForDisablingInternalCollection(product, meta) { const disableInternalCollectionStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionTitle', { - defaultMessage: 'Disable internal collection of Elasticsearch monitoring metrics' + defaultMessage: 'Disable self monitoring of Elasticsearch monitoring metrics' }), children: ( @@ -28,7 +25,7 @@ export function getElasticsearchInstructionsForDisablingInternalCollection(produ

); - } - - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - -

- -

-

- {lastInternallyCollectedMessage} -

-
- ) - }; - } - else { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getDisableStatusStep(product, meta); return [ disableInternalCollectionStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js index 2de84fa883e6ef..eea188461b088d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js @@ -9,49 +9,18 @@ import { EuiSpacer, EuiCodeBlock, EuiLink, - EuiCallOut, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle, statusTitleNewUser } from './common_elasticsearch_instructions'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { getSecurityStep, getMigrationStatusStep } from '../common_instructions'; export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl, }) { - const securitySetup = ( - - - - - {` `} - - - - - ) - }} - /> - - )} - /> - + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` ); const installMetricbeatStep = { @@ -67,7 +36,7 @@ export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta >

@@ -82,6 +51,14 @@ export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta }), children: ( + +

+ {i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleInstallDirectory', { + defaultMessage: 'From the installation directory, run:' + })} +

+
+ modules.d/elasticsearch-xpack.yml @@ -114,14 +90,14 @@ export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta const configureMetricbeatStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatTitle', { - defaultMessage: 'Configure Metricbeat to send to the monitoring cluster' + defaultMessage: 'Configure Metricbeat to send data to the monitoring cluster' }), children: ( metricbeat.yml @@ -134,7 +110,7 @@ export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta isCopyable > {`output.elasticsearch: - hosts: ["${esMonitoringUrl}"] ## Monitoring cluster + hosts: [${esMonitoringUrl}] ## Monitoring cluster # Optional protocol and basic auth credentials. #protocol: "https" @@ -161,7 +137,7 @@ export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta >

@@ -169,48 +145,7 @@ export function getElasticsearchInstructionsForEnablingMetricbeat(product, _meta ) }; - let migrationStatusStep = null; - if (product.isInternalCollector || product.isNetNewUser) { - migrationStatusStep = { - title: product.isNetNewUser ? statusTitleNewUser : statusTitle, - status: 'incomplete', - children: ( - - ) - }; - } - else if (product.isPartiallyMigrated || product.isFullyMigrated) { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getMigrationStatusStep(product); return [ installMetricbeatStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js index 7075be3def9bbf..933e95a6c2e7a4 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js @@ -21,15 +21,20 @@ import { getBeatsInstructionsForDisablingInternalCollection, } from './beats'; import { - getApmInstructionsForEnablingMetricbeat, getApmInstructionsForDisablingInternalCollection, + getApmInstructionsForEnablingMetricbeat, } from './apm'; import { INSTRUCTION_STEP_ENABLE_METRICBEAT, INSTRUCTION_STEP_DISABLE_INTERNAL } from '../constants'; -import { ELASTICSEARCH_CUSTOM_ID, APM_CUSTOM_ID } from '../../../../common/constants'; -import { KIBANA_SYSTEM_ID, LOGSTASH_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../../../../telemetry/common/constants'; +import { + ELASTICSEARCH_SYSTEM_ID, + APM_SYSTEM_ID, + KIBANA_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + BEATS_SYSTEM_ID +} from '../../../../common/constants'; export function getInstructionSteps(productName, product, step, meta, opts) { switch (productName) { @@ -40,7 +45,7 @@ export function getInstructionSteps(productName, product, step, meta, opts) { if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) { return getKibanaInstructionsForDisablingInternalCollection(product, meta, opts); } - case ELASTICSEARCH_CUSTOM_ID: + case ELASTICSEARCH_SYSTEM_ID: if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) { return getElasticsearchInstructionsForEnablingMetricbeat(product, meta, opts); } @@ -61,7 +66,7 @@ export function getInstructionSteps(productName, product, step, meta, opts) { if (step === INSTRUCTION_STEP_DISABLE_INTERNAL) { return getBeatsInstructionsForDisablingInternalCollection(product, meta, opts); } - case APM_CUSTOM_ID: + case APM_SYSTEM_ID: if (step === INSTRUCTION_STEP_ENABLE_METRICBEAT) { return getApmInstructionsForEnablingMetricbeat(product, meta, opts); } diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/common_kibana_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/common_kibana_instructions.js deleted file mode 100644 index 25b869e32b9b76..00000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/common_kibana_instructions.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export const statusTitle = i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitle', { - defaultMessage: `Migration status` -}); - -export const statusTitleNewUser = i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitleNewUser', { - defaultMessage: `Monitoring status` -}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js index e4219fe47c3c26..02bade281940bc 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js @@ -8,24 +8,14 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiButton, EuiCallOut, EuiText } from '@elastic/eui'; -import { formatTimestampToDuration } from '../../../../../common'; -import { CALCULATE_DURATION_SINCE } from '../../../../../common/constants'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle } from './common_kibana_instructions'; +import { getDisableStatusStep } from '../common_instructions'; -export function getKibanaInstructionsForDisablingInternalCollection(product, meta, { - checkForMigrationStatus, - checkingMigrationStatus, - hasCheckedStatus, - autoCheckIntervalInMs, -}) { +export function getKibanaInstructionsForDisablingInternalCollection(product, meta) { let restartWarning = null; if (product.isPrimary) { restartWarning = ( @@ -35,7 +25,7 @@ export function getKibanaInstructionsForDisablingInternalCollection(product, met title={i18n.translate( 'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartWarningTitle', { - defaultMessage: 'Warning' + defaultMessage: 'This step requires you to restart the Kibana server' } )} color="warning" @@ -45,8 +35,7 @@ export function getKibanaInstructionsForDisablingInternalCollection(product, met

@@ -57,7 +46,7 @@ export function getKibanaInstructionsForDisablingInternalCollection(product, met const disableInternalCollectionStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.title', { - defaultMessage: 'Disable internal collection of Kibana monitoring metrics' + defaultMessage: 'Disable self monitoring of Kibana monitoring metrics' }), children: ( @@ -65,7 +54,7 @@ export function getKibanaInstructionsForDisablingInternalCollection(product, met

kibana.yml @@ -86,7 +75,7 @@ export function getKibanaInstructionsForDisablingInternalCollection(product, met

xpack.monitoring.enabled @@ -103,130 +92,7 @@ export function getKibanaInstructionsForDisablingInternalCollection(product, met ) }; - let migrationStatusStep = null; - if (!product || !product.isFullyMigrated) { - let status = null; - if (hasCheckedStatus) { - let lastInternallyCollectedMessage = ''; - // It is possible that, during the migration steps, products are not reporting - // monitoring data for a period of time outside the window of our server-side check - // and this is most likely temporary so we want to be defensive and not error out - // and hopefully wait for the next check and this state will be self-corrected. - if (product) { - const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp; - const secondsSinceLastInternalCollectionLabel = - formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE); - lastInternallyCollectedMessage = (); - } - - status = ( - - - -

- -

-

- {lastInternallyCollectedMessage} -

-
- - ); - } - - let buttonLabel; - if (checkingMigrationStatus) { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkingStatusButtonLabel', - { - defaultMessage: 'Checking...' - } - ); - } else { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkStatusButtonLabel', - { - defaultMessage: 'Check' - } - ); - } - - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - - - -

- {i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.statusDescription', - { - defaultMessage: 'Check that no documents are coming from internal collection.' - } - )} -

-
-
- - - {buttonLabel} - - -
- {status} -
- ) - }; - } - else { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getDisableStatusStep(product, meta); return [ disableInternalCollectionStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js index 1cd8fdea25151e..be4bdf9f2ac1d7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js @@ -9,50 +9,20 @@ import { EuiSpacer, EuiCodeBlock, EuiLink, - EuiCallOut, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle, statusTitleNewUser } from './common_kibana_instructions'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl, }) { - const securitySetup = ( - - - - - {` `} - - - - - ) - }} - /> - - )} - /> - + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/kibana/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` ); + const installMetricbeatStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatTitle', { defaultMessage: 'Install Metricbeat on the same server as Kibana' @@ -66,7 +36,7 @@ export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -130,7 +100,7 @@ export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { isCopyable > {`output.elasticsearch: - hosts: ["${esMonitoringUrl}"] ## Monitoring cluster + hosts: [${esMonitoringUrl}] ## Monitoring cluster # Optional protocol and basic auth credentials. #protocol: "https" @@ -157,7 +127,7 @@ export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -165,48 +135,7 @@ export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { ) }; - let migrationStatusStep = null; - if (product.isInternalCollector || product.isNetNewUser) { - migrationStatusStep = { - title: product.isNetNewUser ? statusTitleNewUser : statusTitle, - status: 'incomplete', - children: ( - - ) - }; - } - else if (product.isPartiallyMigrated || product.isFullyMigrated) { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getMigrationStatusStep(product); return [ installMetricbeatStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/common_logstash_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/common_logstash_instructions.js deleted file mode 100644 index 642add4d43fc4c..00000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/common_logstash_instructions.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export const statusTitle = i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.statusTitle', { - defaultMessage: `Migration status` -}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js index 9efc5a26ef8223..350a50b973b29a 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js @@ -8,27 +8,16 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, EuiText } from '@elastic/eui'; -import { formatTimestampToDuration } from '../../../../../common'; -import { CALCULATE_DURATION_SINCE } from '../../../../../common/constants'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle } from './common_logstash_instructions'; +import { getDisableStatusStep } from '../common_instructions'; -export function getLogstashInstructionsForDisablingInternalCollection(product, meta, { - checkForMigrationStatus, - checkingMigrationStatus, - hasCheckedStatus, - autoCheckIntervalInMs, -}) { +export function getLogstashInstructionsForDisablingInternalCollection(product, meta) { const disableInternalCollectionStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.title', { - defaultMessage: 'Disable internal collection of Logstash monitoring metrics' + defaultMessage: 'Disable self monitoring of Logstash monitoring metrics' }), children: ( @@ -65,130 +54,7 @@ export function getLogstashInstructionsForDisablingInternalCollection(product, m ) }; - let migrationStatusStep = null; - if (!product || !product.isFullyMigrated) { - let status = null; - if (hasCheckedStatus) { - let lastInternallyCollectedMessage = ''; - // It is possible that, during the migration steps, products are not reporting - // monitoring data for a period of time outside the window of our server-side check - // and this is most likely temporary so we want to be defensive and not error out - // and hopefully wait for the next check and this state will be self-corrected. - if (product) { - const lastInternallyCollectedTimestamp = product.lastInternallyCollectedTimestamp || product.lastTimestamp; - const secondsSinceLastInternalCollectionLabel = - formatTimestampToDuration(lastInternallyCollectedTimestamp, CALCULATE_DURATION_SINCE); - lastInternallyCollectedMessage = (); - } - - status = ( - - - -

- -

-

- {lastInternallyCollectedMessage} -

-
-
- ); - } - - let buttonLabel; - if (checkingMigrationStatus) { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkingStatusButtonLabel', - { - defaultMessage: 'Checking...' - } - ); - } else { - buttonLabel = i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkStatusButtonLabel', - { - defaultMessage: 'Check' - } - ); - } - - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - - - -

- {i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.statusDescription', - { - defaultMessage: 'Check that no documents are coming from internal collection.' - } - )} -

-
-
- - - {buttonLabel} - - -
- {status} -
- ) - }; - } - else { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getDisableStatusStep(product, meta); return [ disableInternalCollectionStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js index 71300163ce6d2a..875ab89c99454b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js +++ b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js @@ -9,50 +9,20 @@ import { EuiSpacer, EuiCodeBlock, EuiLink, - EuiCallOut, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { statusTitle } from './common_logstash_instructions'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl, }) { - const securitySetup = ( - - - - - {` `} - - - - - ) - }} - /> - - )} - /> -
+ const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/logstash/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` ); + const installMetricbeatStep = { title: i18n.translate('xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatTitle', { defaultMessage: 'Install Metricbeat on the same server as Logstash' @@ -66,7 +36,7 @@ export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -130,7 +100,7 @@ export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { isCopyable > {`output.elasticsearch: - hosts: ["${esMonitoringUrl}"] ## Monitoring cluster + hosts: [${esMonitoringUrl}] ## Monitoring cluster # Optional protocol and basic auth credentials. #protocol: "https" @@ -157,7 +127,7 @@ export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { >

@@ -165,48 +135,7 @@ export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { ) }; - let migrationStatusStep = null; - if (product.isInternalCollector || product.isNetNewUser) { - migrationStatusStep = { - title: statusTitle, - status: 'incomplete', - children: ( - - ) - }; - } - else if (product.isPartiallyMigrated || product.isFullyMigrated) { - migrationStatusStep = { - title: statusTitle, - status: 'complete', - children: ( - -

- -

-
- ) - }; - } + const migrationStatusStep = getMigrationStatusStep(product); return [ installMetricbeatStep, diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap index 7c663a26bc9df1..1d0f58b6235952 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap @@ -22,43 +22,69 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
+

+ No monitoring data found +

+

- There is a - - - food - - - setting that has - +

+
+
+
+ +
+
+
+
+ +
@@ -89,38 +115,66 @@ exports[`NoData should show text next to the spinner while checking a setting 1`

- We're looking for your monitoring data + No monitoring data found

- -
-

- Monitoring provides insight to your hardware performance and load. -

-
-

+

+ Have you set up monitoring yet? If so, make sure that the selected time period in the upper right includes monitoring data. +

+
+
+
- -
-
- checking something to test... +
+
+
diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js b/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js index 286727449eb755..e50e49eec9f6eb 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js +++ b/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { EuiSpacer, @@ -12,7 +12,17 @@ import { EuiPage, EuiPageBody, EuiPageContent, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiText, + EuiTitle, + EuiTextColor, + EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { toggleSetupMode } from '../../lib/setup_mode'; import { CheckingSettings } from './checking_settings'; import { ReasonFound, WeTried } from './reasons'; import { CheckerErrors } from './checker_errors'; @@ -32,6 +42,42 @@ function NoDataMessage(props) { } export function NoData(props) { + const [isLoading, setIsLoading] = useState(false); + const [useInternalCollection, setUseInternalCollection] = useState(false); + + async function startSetup() { + setIsLoading(true); + await toggleSetupMode(true); + props.changePath('/elasticsearch/nodes'); + } + + if (useInternalCollection) { + return ( + + + + + + + + + setUseInternalCollection(false)}> + + + + + + + + ); + } return ( @@ -43,8 +89,54 @@ export function NoData(props) { > - - + +

+ +

+
+ + +

+ +

+
+ + + + + + + + + + setUseInternalCollection(true)} data-test-subj="useInternalCollection"> + + + +
@@ -52,6 +144,7 @@ export function NoData(props) { } NoData.propTypes = { + changePath: PropTypes.func, isLoading: PropTypes.bool.isRequired, reason: PropTypes.object, checkMessage: PropTypes.string diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js index f1bfebe7851ca9..079a3e7eeae093 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js @@ -3,10 +3,28 @@ * 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 { getSetupModeState, initSetupModeState, updateSetupModeData, disableElasticsearchInternalCollection } from '../../lib/setup_mode'; +import React, { Fragment } from 'react'; +import { + getSetupModeState, + initSetupModeState, + updateSetupModeData, + disableElasticsearchInternalCollection, + toggleSetupMode, + setSetupModeMenuItem +} from '../../lib/setup_mode'; import { Flyout } from '../metricbeat_migration/flyout'; +import { + EuiBottomBar, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, + EuiIcon, + EuiSpacer +} from '@elastic/eui'; import { findNewUuid } from './lib/find_new_uuid'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; export class SetupModeRenderer extends React.Component { state = { @@ -44,6 +62,7 @@ export class SetupModeRenderer extends React.Component { this.setState(newState); }); + setSetupModeMenuItem(); } reset() { @@ -97,6 +116,50 @@ export class SetupModeRenderer extends React.Component { ); } + getBottomBar(setupModeState) { + if (!setupModeState.enabled) { + return null; + } + + return ( + + + + + + + + + + ) + }} + /> + + + + + + + + toggleSetupMode(false)}> + {i18n.translate('xpack.monitoring.setupMode.exit', { + defaultMessage: `Exit setup mode` + })} + + + + + + + + ); + } + async shortcutToFinishMigration() { await disableElasticsearchInternalCollection(); await updateSetupModeData(); @@ -130,6 +193,7 @@ export class SetupModeRenderer extends React.Component { closeFlyout: () => this.setState({ isFlyoutOpen: false }), }, flyoutComponent: this.getFlyout(data, meta), + bottomBarComponent: this.getBottomBar(setupModeState) }); } } diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js new file mode 100644 index 00000000000000..d4dcd2df1fec2c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js @@ -0,0 +1,124 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiTextColor, + EuiIcon, + EuiBadge +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ELASTICSEARCH_SYSTEM_ID } from '../../../common/constants'; + +const clickToMonitorWithMetricbeat = i18n.translate('xpack.monitoring.setupMode.clickToMonitorWithMetricbeat', { + defaultMessage: 'Monitor with Metricbeat' +}); + +const clickToDisableInternalCollection = i18n.translate('xpack.monitoring.setupMode.clickToDisableInternalCollection', { + defaultMessage: 'Disable self monitoring' +}); + +const monitoredWithMetricbeat = i18n.translate('xpack.monitoring.setupMode.usingMetricbeatCollection', { + defaultMessage: 'Monitored with Metricbeat' +}); + +const unknown = i18n.translate('xpack.monitoring.setupMode.unknown', { + defaultMessage: 'N/A' +}); + +export function SetupModeBadge({ setupMode, productName, status, instance, clusterUuid }) { + let customAction = null; + let customText = null; + + const setupModeData = setupMode.data || {}; + const setupModeMeta = setupMode.meta || {}; + + // Migrating from partially to fully for Elasticsearch involves changing a cluster + // setting which impacts all nodes in the cluster so the action text needs to reflect that + const allPartiallyMigrated = setupModeData.totalUniquePartiallyMigratedCount === setupModeData.totalUniqueInstanceCount; + + if (status.isPartiallyMigrated && productName === ELASTICSEARCH_SYSTEM_ID) { + if (allPartiallyMigrated) { + customText = clickToDisableInternalCollection; + if (setupModeMeta.liveClusterUuid === clusterUuid) { + customAction = setupMode.shortcutToFinishMigration; + } + } + else { + return ( + + +   + + {i18n.translate('xpack.monitoring.setupMode.monitorAllNodes', { + defaultMessage: 'Some nodes use only self monitoring' + })} + + + ); + } + } + + const badgeProps = {}; + if (status.isInternalCollector || status.isPartiallyMigrated || status.isNetNewUser) { + badgeProps.onClick = customAction ? customAction : () => setupMode.openFlyout(instance); + } + + + let statusText = null; + if (status.isInternalCollector) { + if (badgeProps.onClick) { + badgeProps.onClickAriaLabel = customText || clickToMonitorWithMetricbeat; + } + statusText = ( + + {customText || clickToMonitorWithMetricbeat} + + ); + } + else if (status.isPartiallyMigrated) { + if (badgeProps.onClick) { + badgeProps.onClickAriaLabel = customText || clickToDisableInternalCollection; + } + statusText = ( + + {customText || clickToDisableInternalCollection} + + ); + } + else if (status.isFullyMigrated) { + if (badgeProps.onClick) { + badgeProps.onClickAriaLabel = customText || monitoredWithMetricbeat; + } + statusText = ( + + {customText || monitoredWithMetricbeat} + + ); + } + else if (status.isNetNewUser) { + if (badgeProps.onClick) { + badgeProps.onClickAriaLabel = customText || clickToMonitorWithMetricbeat; + } + statusText = ( + + {customText || clickToMonitorWithMetricbeat} + + ); + } + else { + if (badgeProps.onClick) { + badgeProps.onClickAriaLabel = customText || unknown; + } + statusText = ( + + {customText || unknown} + + ); + } + + return statusText; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js new file mode 100644 index 00000000000000..d88608bbb1afe3 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js @@ -0,0 +1,56 @@ +/* + * 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 { capitalize } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + ELASTICSEARCH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID +} from '../../../common/constants'; + +const NODE_IDENTIFIER_SINGULAR = i18n.translate('xpack.monitoring.setupMode.node', { + defaultMessage: `node`, +}); +const NODE_IDENTIFIER_PLURAL = i18n.translate('xpack.monitoring.setupMode.nodes', { + defaultMessage: `nodes`, +}); +const INSTANCE_IDENTIFIER_SINGULAR = i18n.translate('xpack.monitoring.setupMode.instance', { + defaultMessage: `instance`, +}); +const INSTANCE_IDENTIFIER_PLURAL = i18n.translate('xpack.monitoring.setupMode.instances', { + defaultMessage: `instances`, +}); +const SERVER_IDENTIFIER_SINGULAR = i18n.translate('xpack.monitoring.setupMode.server', { + defaultMessage: `server`, +}); +const SERVER_IDENTIFIER_PLURAL = i18n.translate('xpack.monitoring.setupMode.servers', { + defaultMessage: `servers`, +}); + + +export function formatProductName(productName) { + if (productName === APM_SYSTEM_ID) { + return productName.toUpperCase(); + } + return capitalize(productName); +} + +const PRODUCTS_THAT_USE_NODES = [LOGSTASH_SYSTEM_ID, ELASTICSEARCH_SYSTEM_ID]; +const PRODUCTS_THAT_USE_INSTANCES = [KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID]; +export function getIdentifier(productName, usePlural = false) { + if (PRODUCTS_THAT_USE_INSTANCES.includes(productName)) { + return usePlural ? INSTANCE_IDENTIFIER_PLURAL : INSTANCE_IDENTIFIER_SINGULAR; + } + if (PRODUCTS_THAT_USE_NODES.includes(productName)) { + return usePlural ? NODE_IDENTIFIER_PLURAL : NODE_IDENTIFIER_SINGULAR; + } + if (productName === APM_SYSTEM_ID) { + return usePlural ? SERVER_IDENTIFIER_PLURAL : SERVER_IDENTIFIER_SINGULAR; + } + return productName; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js new file mode 100644 index 00000000000000..adede59d384d6a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js @@ -0,0 +1,174 @@ +/* + * 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, { Fragment } from 'react'; +import { get } from 'lodash'; +import { + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { formatProductName, getIdentifier } from './formatting'; + +const MIGRATE_TO_MB_LABEL = i18n.translate('xpack.monitoring.setupMode.migrateToMetricbeat', { + defaultMessage: 'Monitor with Metricbeat', +}); + +export function ListingCallOut({ setupModeData, productName, customRenderer = null }) { + if (customRenderer) { + const { shouldRender, componentToRender } = customRenderer(); + if (shouldRender) { + return componentToRender; + } + } + + const mightExist = get(setupModeData, 'detected.mightExist'); + + const hasInstances = setupModeData.totalUniqueInstanceCount > 0; + if (!hasInstances) { + if (mightExist) { + return ( + + +

+ {i18n.translate('xpack.monitoring.setupMode.detectedNodeDescription', { + defaultMessage: `Click 'Set up monitoring' below to start monitoring this {identifier}.`, + values: { + identifier: getIdentifier(productName) + } + })} +

+
+ +
+ ); + } + return ( + + +

+ {i18n.translate('xpack.monitoring.setupMode.netNewUserDescription', { + defaultMessage: `Click 'Set up monitoring' to start monitoring with Metricbeat.`, + })} +

+
+ +
+ ); + } + + if (setupModeData.totalUniqueFullyMigratedCount === setupModeData.totalUniqueInstanceCount) { + return ( + + + + + ); + } + + if (setupModeData.totalUniquePartiallyMigratedCount === setupModeData.totalUniqueInstanceCount) { + return ( + + +

+ {i18n.translate('xpack.monitoring.setupMode.disableInternalCollectionDescription', { + defaultMessage: `Metricbeat is now monitoring your {product} {identifier}. + Disable self monitoring to finish the migration.`, + values: { + product: formatProductName(productName), + identifier: getIdentifier(productName, true) + } + })} +

+
+ +
+ ); + } + + if (setupModeData.totalUniqueInstanceCount > 0) { + if (setupModeData.totalUniqueFullyMigratedCount === 0 && setupModeData.totalUniquePartiallyMigratedCount === 0) { + return ( + + +

+ {i18n.translate('xpack.monitoring.setupMode.migrateToMetricbeatDescription', { + defaultMessage: `These {product} {identifier} are self monitored. + Click 'Monitor with Metricbeat' to migrate.`, + values: { + product: formatProductName(productName), + identifier: getIdentifier(productName, true) + } + })} +

+
+ +
+ ); + } + + return ( + + +

+ {i18n.translate('xpack.monitoring.setupMode.migrateSomeToMetricbeatDescription', { + defaultMessage: `Some {product} {identifier} are monitored through self monitoring. Migrate to monitor with Metricbeat.`, + values: { + product: formatProductName(productName), + identifier: getIdentifier(productName, true) + } + })} +

+
+ +
+ ); + } + + return null; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js new file mode 100644 index 00000000000000..cc73a4d29536c9 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js @@ -0,0 +1,141 @@ +/* + * 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 { get } from 'lodash'; +import { + EuiBadge, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { getIdentifier } from './formatting'; + +export function SetupModeTooltip({ setupModeData, badgeClickAction, productName }) { + if (!setupModeData) { + return null; + } + + const { + totalUniqueInstanceCount, + totalUniqueFullyMigratedCount, + totalUniquePartiallyMigratedCount + } = setupModeData; + const allMonitoredByMetricbeat = totalUniqueInstanceCount > 0 && + (totalUniqueFullyMigratedCount === totalUniqueInstanceCount || totalUniquePartiallyMigratedCount === totalUniqueInstanceCount); + const internalCollectionOn = totalUniquePartiallyMigratedCount > 0; + const mightExist = get(setupModeData, 'detected.mightExist') || get(setupModeData, 'detected.doesExist'); + + let tooltip = null; + + if (totalUniqueInstanceCount === 0) { + if (mightExist) { + const detectedText = i18n.translate('xpack.monitoring.setupMode.tooltip.detected', { + defaultMessage: 'No monitoring' + }); + tooltip = ( + + + {detectedText} + + + ); + } + else { + const noMonitoringText = i18n.translate('xpack.monitoring.setupMode.tooltip.noUsage', { + defaultMessage: 'No usage' + }); + + tooltip = ( + + + {noMonitoringText} + + + ); + } + } + + else if (!allMonitoredByMetricbeat) { + const internalCollection = i18n.translate('xpack.monitoring.euiTable.isInternalCollectorLabel', { + defaultMessage: 'Self monitoring' + }); + tooltip = ( + + + {internalCollection} + + + ); + } + else if (internalCollectionOn) { + const internalAndMB = i18n.translate('xpack.monitoring.euiTable.isPartiallyMigratedLabel', { + defaultMessage: 'Self monitoring is on' + }); + tooltip = ( + + + {internalAndMB} + + + ); + } + else { + const metricbeatCollection = i18n.translate('xpack.monitoring.euiTable.isFullyMigratedLabel', { + defaultMessage: 'Metricbeat monitoring' + }); + tooltip = ( + + + {metricbeatCollection} + + + ); + } + + return ( + + {tooltip} + + ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js b/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js index 9bd04e89c85121..53b16c29143bc7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js @@ -5,17 +5,13 @@ */ import React, { Fragment } from 'react'; -import { get } from 'lodash'; import { EuiInMemoryTable, - EuiBadge, - EuiButtonEmpty, - EuiHealth, EuiButton, EuiSpacer } from '@elastic/eui'; -import { ELASTICSEARCH_CUSTOM_ID } from '../../../common/constants'; import { i18n } from '@kbn/i18n'; +import { getIdentifier } from '../setup_mode/formatting'; export class EuiMonitoringTable extends React.PureComponent { render() { @@ -24,9 +20,7 @@ export class EuiMonitoringTable extends React.PureComponent { search = {}, columns: _columns, setupMode, - uuidField, - nameField, - setupNewButtonLabel, + productName, ...props } = this.props; @@ -51,150 +45,16 @@ export class EuiMonitoringTable extends React.PureComponent { let footerContent = null; if (setupMode && setupMode.enabled) { - columns.push({ - name: i18n.translate('xpack.monitoring.euiTable.setupStatusTitle', { - defaultMessage: 'Setup Status' - }), - sortable: product => { - const list = get(setupMode, 'data.byUuid', {}); - const status = list[get(product, uuidField)] || {}; - - if (status.isInternalCollector) { - return 4; - } - - if (status.isPartiallyMigrated) { - return 3; - } - - if (status.isFullyMigrated) { - return 2; - } - - if (status.isNetNewUser) { - return 1; - } - - return 0; - }, - render: (product) => { - const list = get(setupMode, 'data.byUuid', {}); - const status = list[get(product, uuidField)] || {}; - - let statusBadge = null; - if (status.isInternalCollector) { - statusBadge = ( - - {i18n.translate('xpack.monitoring.euiTable.isInternalCollectorLabel', { - defaultMessage: 'Internal collection' - })} - - ); - } - else if (status.isPartiallyMigrated) { - statusBadge = ( - - {i18n.translate('xpack.monitoring.euiTable.isPartiallyMigratedLabel', { - defaultMessage: 'Internal collection and Metricbeat collection' - })} - - ); - } - else if (status.isFullyMigrated) { - statusBadge = ( - - {i18n.translate('xpack.monitoring.euiTable.isFullyMigratedLabel', { - defaultMessage: 'Metricbeat collection' - })} - - ); - } - else if (status.isNetNewUser) { - statusBadge = ( - - {i18n.translate('xpack.monitoring.euiTable.isNetNewUserLabel', { - defaultMessage: 'No monitoring detected' - })} - - ); - } - else { - statusBadge = i18n.translate('xpack.monitoring.euiTable.migrationStatusUnknown', { - defaultMessage: 'N/A' - }); - } - - return statusBadge; - } - }); - - columns.push({ - name: i18n.translate('xpack.monitoring.euiTable.setupActionTitle', { - defaultMessage: 'Setup Action' - }), - sortable: product => { - const list = get(setupMode, 'data.byUuid', {}); - const status = list[get(product, uuidField)] || {}; - - if (status.isInternalCollector || status.isNetNewUser) { - return 1; - } - - if (status.isPartiallyMigrated) { - if (setupMode.productName === ELASTICSEARCH_CUSTOM_ID) { - // See comment for same conditional in render function - return 0; - } - return 1; - } - - return 0; - }, - render: (product) => { - const uuid = get(product, uuidField); - const list = get(setupMode, 'data.byUuid', {}); - const status = list[uuid] || {}; - const instance = { - uuid: get(product, uuidField), - name: get(product, nameField), - }; - - // Migrating from partially to fully for Elasticsearch involves changing a cluster - // setting which impacts all nodes in the cluster, which we have a separate callout - // for. Since it does not make sense to do this on a per node basis, show nothing here - if (status.isPartiallyMigrated && setupMode.productName === ELASTICSEARCH_CUSTOM_ID) { - return null; - } - - if (status.isInternalCollector || status.isPartiallyMigrated) { - return ( - setupMode.openFlyout(instance)}> - {i18n.translate('xpack.monitoring.euiTable.migrateButtonLabel', { - defaultMessage: 'Migrate' - })} - - ); - } - - if (status.isNetNewUser) { - return ( - setupMode.openFlyout(instance)}> - {i18n.translate('xpack.monitoring.euiTable.setupButtonLabel', { - defaultMessage: 'Setup' - })} - - ); - } - - return null; - } - }); - footerContent = ( - setupMode.openFlyout({}, true)}> - {setupNewButtonLabel} + setupMode.openFlyout({}, true)}> + {i18n.translate('xpack.monitoring.euiTable.setupNewButtonLabel', { + defaultMessage: 'Set up monitoring for new {identifier}', + values: { + identifier: getIdentifier(productName) + } + })} ); diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.js b/x-pack/legacy/plugins/monitoring/public/directives/main/index.js index 9f7debb73de9ba..a7aee9ae780588 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/legacy/plugins/monitoring/public/directives/main/index.js @@ -18,7 +18,7 @@ import template from './index.html'; import { timefilter } from 'ui/timefilter'; import { shortenPipelineHash } from '../../../common/formatting'; import 'ui/directives/kbn_href'; -import { getSetupModeState } from '../../lib/setup_mode'; +import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; const setOptions = (controller) => { if (!controller.pipelineVersions || !controller.pipelineVersions.length || !controller.pipelineDropdownElement) { @@ -56,6 +56,7 @@ const setOptions = (controller) => { , controller.pipelineDropdownElement); }; + /* * Manage data and provide helper methods for the "main" directive's template */ @@ -97,7 +98,7 @@ export class MonitoringMainController { } else { this.inOverview = this.name === 'overview'; this.inAlerts = this.name === 'alerts'; - this.inListing = this.name === 'listing' || this.name === 'no-data'; + this.inListing = this.name === 'listing';// || this.name === 'no-data'; } if (!this.inListing) { @@ -155,6 +156,10 @@ export class MonitoringMainController { if (data.totalUniqueInstanceCount === 0) { return true; } + if (data.totalUniqueInternallyCollectedCount === 0 + && data.totalUniqueFullyMigratedCount === 0 && data.totalUniquePartiallyMigratedCount === 0) { + return true; + } return false; } } @@ -169,6 +174,9 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) = controllerAs: 'monitoringMain', bindToController: true, link(scope, _element, attributes, controller) { + initSetupModeState(scope, $injector, () => { + controller.setup(getSetupObj()); + }); if (!scope.cluster) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/route_init.js b/x-pack/legacy/plugins/monitoring/public/lib/route_init.js index 2f1eef85be7257..1cd8e688854ebc 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/route_init.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/route_init.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { getSetupModeState } from './setup_mode'; +import { isInSetupMode } from './setup_mode'; import { getClusterFromClusters } from './get_cluster_from_clusters'; export function routeInitProvider(Private, monitoringClusters, globalState, license, kbnUrl) { @@ -26,8 +26,8 @@ export function routeInitProvider(Private, monitoringClusters, globalState, lice const clusterUuid = fetchAllClusters ? null : globalState.cluster_uuid; return monitoringClusters(clusterUuid, undefined, codePaths) // Set the clusters collection and current cluster in globalState - .then((clusters) => { - const inSetupMode = getSetupModeState().enabled; + .then(async (clusters) => { + const inSetupMode = await isInSetupMode(); const cluster = getClusterFromClusters(clusters, globalState); if (!cluster && !inSetupMode) { return kbnUrl.redirect('/no-data'); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js index dbfbdb324f7aaf..3e7d182f1514c5 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js @@ -5,7 +5,12 @@ */ import { ajaxErrorHandlersProvider } from './ajax_error_handler'; -import { get } from 'lodash'; +import { get, contains } from 'lodash'; +import chrome from 'ui/chrome'; + +function isOnPage(hash) { + return contains(window.location.hash, hash); +} const angularState = { injector: null, @@ -111,70 +116,63 @@ export const disableElasticsearchInternalCollection = async () => { }; export const toggleSetupMode = inSetupMode => { - return new Promise(async (resolve, reject) => { - try { - checkAngularState(); - } catch (err) { - return reject(err); - } + checkAngularState(); - const globalState = angularState.injector.get('globalState'); - setupModeState.enabled = inSetupMode; - globalState.inSetupMode = inSetupMode; - globalState.save(); - setSetupModeMenuItem(); // eslint-disable-line no-use-before-define - notifySetupModeDataChange(); + const globalState = angularState.injector.get('globalState'); + setupModeState.enabled = inSetupMode; + globalState.inSetupMode = inSetupMode; + globalState.save(); + setSetupModeMenuItem(); // eslint-disable-line no-use-before-define + notifySetupModeDataChange(); + + if (inSetupMode) { + // Intentionally do not await this so we don't block UI operations + updateSetupModeData(); + } +}; - if (inSetupMode) { - await updateSetupModeData(); - } +export const setSetupModeMenuItem = () => { + checkAngularState(); - resolve(); - }); -}; + if (isOnPage('no-data')) { + return; + } -const setSetupModeMenuItem = () => { - // Disabling this for this initial release. This will be added back in - // in a subsequent PR - // checkAngularState(); - - // const globalState = angularState.injector.get('globalState'); - // const navItems = globalState.inSetupMode - // ? [ - // { - // key: 'exit', - // label: 'Exit Setup Mode', - // description: 'Exit setup mode', - // run: () => toggleSetupMode(false), - // testId: 'exitSetupMode' - // }, - // { - // key: 'refresh', - // label: 'Refresh Setup Data', - // description: 'Refresh data used for setup mode', - // run: () => updateSetupModeData(), - // testId: 'refreshSetupModeData' - // } - // ] - // : [{ - // key: 'enter', - // label: 'Enter Setup Mode', - // description: 'Enter setup mode', - // run: () => toggleSetupMode(true), - // testId: 'enterSetupMode' - // }]; - - // angularState.scope.topNavMenu = [...navItems]; + const globalState = angularState.injector.get('globalState'); + const navItems = globalState.inSetupMode + ? [] + : [{ + id: 'enter', + label: 'Enter Setup Mode', + description: 'Enter setup', + run: () => toggleSetupMode(true), + testId: 'enterSetupMode' + }]; + + angularState.scope.topNavMenu = [...navItems]; + // LOL angular + if (!angularState.scope.$$phase) { + angularState.scope.$apply(); + } }; -export const initSetupModeState = ($scope, $injector, callback) => { +export const initSetupModeState = async ($scope, $injector, callback) => { angularState.scope = $scope; angularState.injector = $injector; - setSetupModeMenuItem(); callback && setupModeState.callbacks.push(callback); - const globalState = angularState.injector.get('globalState'); + const globalState = $injector.get('globalState'); if (globalState.inSetupMode) { - toggleSetupMode(true); + await toggleSetupMode(true); } }; + +export const isInSetupMode = async () => { + if (setupModeState.enabled) { + return true; + } + + const $injector = angularState.injector || await chrome.dangerouslyGetActiveInjector(); + const globalState = $injector.get('globalState'); + return globalState.inSetupMode; +}; diff --git a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js b/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js index 4bc7d31b0a92ce..aa91d0ff6e6edd 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js +++ b/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js @@ -120,12 +120,11 @@ function getApmBreadcrumbs(mainInstance) { export function breadcrumbsProvider() { return function createBreadcrumbs(clusterName, mainInstance) { - let breadcrumbs = [ createCrumb('#/home', - i18n.translate( - 'xpack.monitoring.breadcrumbs.clustersLabel', { defaultMessage: 'Clusters' } - ), - 'breadcrumbClusters') - ]; + const homeCrumb = i18n.translate( + 'xpack.monitoring.breadcrumbs.clustersLabel', { defaultMessage: 'Clusters' } + ); + + let breadcrumbs = [ createCrumb('#/home', homeCrumb, 'breadcrumbClusters')]; if (!mainInstance.inOverview && clusterName) { breadcrumbs.push(createCrumb('#/overview', clusterName)); diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js index 367c9f78a44d8f..35116924f5d5c1 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js @@ -14,7 +14,7 @@ import { ApmServerInstances } from '../../../components/apm/instances'; import { MonitoringViewBaseEuiTableController } from '../..'; import { I18nContext } from 'ui/i18n'; import { SetupModeRenderer } from '../../../components/renderers'; -import { APM_CUSTOM_ID, CODE_PATH_APM } from '../../../../common/constants'; +import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants'; uiRoutes.when('/apm/instances', { template, @@ -67,8 +67,8 @@ uiRoutes.when('/apm/instances', { ( + productName={APM_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( {flyoutComponent} + {bottomBarComponent} )} /> diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js index 2878874e1a4a93..deb195df1d8108 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js @@ -15,8 +15,7 @@ import React, { Fragment } from 'react'; import { I18nContext } from 'ui/i18n'; import { Listing } from '../../../components/beats/listing/listing'; import { SetupModeRenderer } from '../../../components/renderers'; -import { BEATS_SYSTEM_ID } from '../../../../../telemetry/common/constants'; -import { CODE_PATH_BEATS } from '../../../../common/constants'; +import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants'; uiRoutes.when('/beats/beats', { template, @@ -63,7 +62,7 @@ uiRoutes.when('/beats/beats', { scope={this.scope} injector={this.injector} productName={BEATS_SYSTEM_ID} - render={({ setupMode, flyoutComponent }) => ( + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( {flyoutComponent} + {bottomBarComponent} )} /> diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js index 5cf212abf901f9..60cb8349070e49 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js @@ -58,7 +58,7 @@ uiRoutes.when('/overview', { ( + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( {flyoutComponent} + {bottomBarComponent} )} /> diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js index 67658c665d3cfa..ce7e81a80e5217 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -16,7 +16,7 @@ import { ElasticsearchNodes } from '../../../components'; import { I18nContext } from 'ui/i18n'; import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; import { SetupModeRenderer } from '../../../components/renderers'; -import { ELASTICSEARCH_CUSTOM_ID, CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; +import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; uiRoutes.when('/elasticsearch/nodes', { template, @@ -83,8 +83,8 @@ uiRoutes.when('/elasticsearch/nodes', { ( + productName={ELASTICSEARCH_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( {flyoutComponent} + {bottomBarComponent} )} /> diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js index 1b8e2b193c97b7..f5f2f5a5a76df1 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js @@ -13,8 +13,7 @@ import template from './index.html'; import { KibanaInstances } from 'plugins/monitoring/components/kibana/instances'; import { SetupModeRenderer } from '../../../components/renderers'; import { I18nContext } from 'ui/i18n'; -import { KIBANA_SYSTEM_ID } from '../../../../../telemetry/common/constants'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; +import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA } from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { template, @@ -47,7 +46,7 @@ uiRoutes.when('/kibana/instances', { scope={$scope} injector={$injector} productName={KIBANA_SYSTEM_ID} - render={({ setupMode, flyoutComponent }) => ( + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( {flyoutComponent} + {bottomBarComponent} )} /> diff --git a/x-pack/legacy/plugins/monitoring/public/views/loading/index.js b/x-pack/legacy/plugins/monitoring/public/views/loading/index.js index 683a1e1ac5264a..2b79e047177a61 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/loading/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/loading/index.js @@ -10,7 +10,6 @@ import { PageLoading } from 'plugins/monitoring/components'; import uiRoutes from 'ui/routes'; import { I18nContext } from 'ui/i18n'; import template from './index.html'; -import { toggleSetupMode, getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; import { CODE_PATH_LICENSE } from '../../../common/constants'; const REACT_DOM_ID = 'monitoringLoadingReactApp'; @@ -23,17 +22,6 @@ uiRoutes const monitoringClusters = $injector.get('monitoringClusters'); const kbnUrl = $injector.get('kbnUrl'); - initSetupModeState($scope, $injector); - - const setupMode = getSetupModeState(); - // For phase 3, this is not an valid route unless - // setup mode is currently enabled. For phase 4, - // we will remove this check. - if (!setupMode.enabled) { - kbnUrl.changePath('/no-data'); - return; - } - $scope.$on('$destroy', () => { unmountComponentAtNode(document.getElementById(REACT_DOM_ID)); }); @@ -48,12 +36,8 @@ uiRoutes kbnUrl.changePath('/home'); return; } - initSetupModeState($scope, $injector); - return toggleSetupMode(true) - .then(() => { - kbnUrl.changePath('/elasticsearch/nodes'); - $scope.$apply(); - }); + kbnUrl.changePath('/no-data'); + return; }); } diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js index a392911dcda1dd..1f4b37b77ad64a 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js @@ -12,8 +12,7 @@ import template from './index.html'; import { I18nContext } from 'ui/i18n'; import { Listing } from '../../../components/logstash/listing'; import { SetupModeRenderer } from '../../../components/renderers'; -import { LOGSTASH_SYSTEM_ID } from '../../../../../telemetry/common/constants'; -import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; +import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; uiRoutes.when('/logstash/nodes', { template, @@ -46,7 +45,7 @@ uiRoutes.when('/logstash/nodes', { scope={$scope} injector={$injector} productName={LOGSTASH_SYSTEM_ID} - render={({ setupMode, flyoutComponent }) => ( + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( {flyoutComponent} + {bottomBarComponent} )} /> diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js index 0fef0fbe125e2e..0ecd6c83265fff 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js +++ b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js @@ -78,6 +78,8 @@ export class NoDataController extends MonitoringViewBaseController { } this.render(enabler); }, true); + + this.changePath = path => kbnUrl.changePath(path); } getDefaultModel() { @@ -94,9 +96,10 @@ export class NoDataController extends MonitoringViewBaseController { render(enabler) { const props = this; + this.renderReact( - + ); } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js index 228e2b602dfbb0..bb42dad26786a5 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/__test__/get_collection_status.js @@ -9,7 +9,8 @@ import sinon from 'sinon'; import { getCollectionStatus } from '../'; import { getIndexPatterns } from '../../../cluster/get_index_patterns'; -const mockReq = (searchResult = {}, msearchResult = { responses: [] }) => { +const liveClusterUuid = 'a12'; +const mockReq = (searchResult = {}) => { return { server: { config() { @@ -18,12 +19,28 @@ const mockReq = (searchResult = {}, msearchResult = { responses: [] }) => { .withArgs('server.uuid').returns('kibana-1234') }; }, + usage: { + collectorSet: { + getCollectorByType: () => ({ + isReady: () => false + }) + } + }, plugins: { elasticsearch: { getCluster() { return { - callWithRequest(_req, type) { - return Promise.resolve(type === 'search' ? searchResult : msearchResult); + callWithRequest(_req, type, params) { + if (type === 'transport.request' && (params && params.path === '/_cluster/state/cluster_uuid')) { + return Promise.resolve({ cluster_uuid: liveClusterUuid }); + } + if (type === 'transport.request' && (params && params.path === '/_nodes')) { + return Promise.resolve({ nodes: {} }); + } + if (type === 'cat.indices') { + return Promise.resolve([1]); + } + return Promise.resolve(searchResult); } }; } @@ -192,7 +209,7 @@ describe('getCollectionStatus', () => { ] }); - const result = await getCollectionStatus(req, getIndexPatterns(req.server)); + const result = await getCollectionStatus(req, getIndexPatterns(req.server), liveClusterUuid); expect(result.kibana.detected.doesExist).to.be(true); expect(result.elasticsearch.detected.doesExist).to.be(true); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 85e0745436463d..a49da8ba60200a 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -5,8 +5,14 @@ */ import { get, uniq } from 'lodash'; -import { METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, ELASTICSEARCH_CUSTOM_ID, APM_CUSTOM_ID } from '../../../../common/constants'; -import { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, LOGSTASH_SYSTEM_ID } from '../../../../../telemetry/common/constants'; +import { + METRICBEAT_INDEX_NAME_UNIQUE_TOKEN, + ELASTICSEARCH_SYSTEM_ID, + APM_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + LOGSTASH_SYSTEM_ID +} from '../../../../common/constants'; import { getLivesNodes } from '../../elasticsearch/nodes/get_nodes/get_live_nodes'; import { KIBANA_STATS_TYPE } from '../../../../../../../../src/legacy/server/status/constants'; @@ -139,18 +145,18 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod return await callWithRequest(req, 'search', params); }; -async function detectProducts(req) { +async function detectProducts(req, isLiveCluster) { const result = { [KIBANA_SYSTEM_ID]: { doesExist: true, }, - [ELASTICSEARCH_CUSTOM_ID]: { + [ELASTICSEARCH_SYSTEM_ID]: { doesExist: true, }, [BEATS_SYSTEM_ID]: { mightExist: false, }, - [APM_CUSTOM_ID]: { + [APM_SYSTEM_ID]: { mightExist: false, }, [LOGSTASH_SYSTEM_ID]: { @@ -174,18 +180,20 @@ async function detectProducts(req) { ] }, { - id: APM_CUSTOM_ID, + id: APM_SYSTEM_ID, indices: [ 'apm-*' ] } ]; - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); - for (const { id, indices } of detectionSearch) { - const response = await callWithRequest(req, 'cat.indices', { index: indices, format: 'json' }); - if (response.length) { - result[id].mightExist = true; + if (isLiveCluster) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); + for (const { id, indices } of detectionSearch) { + const response = await callWithRequest(req, 'cat.indices', { index: indices, format: 'json' }); + if (response.length) { + result[id].mightExist = true; + } } } @@ -194,12 +202,12 @@ async function detectProducts(req) { function getUuidBucketName(productName) { switch (productName) { - case ELASTICSEARCH_CUSTOM_ID: + case ELASTICSEARCH_SYSTEM_ID: return 'es_uuids'; case KIBANA_SYSTEM_ID: return 'kibana_uuids'; case BEATS_SYSTEM_ID: - case APM_CUSTOM_ID: + case APM_SYSTEM_ID: return 'beats_uuids'; case LOGSTASH_SYSTEM_ID: return 'logstash_uuids'; @@ -232,7 +240,7 @@ function shouldSkipBucket(product, bucket) { if (product.name === BEATS_SYSTEM_ID && isBeatFromAPM(bucket)) { return true; } - if (product.name === APM_CUSTOM_ID && !isBeatFromAPM(bucket)) { + if (product.name === APM_SYSTEM_ID && !isBeatFromAPM(bucket)) { return true; } return false; @@ -271,7 +279,7 @@ async function getLiveElasticsearchCollectionEnabled(req) { }); const sources = ['persistent', 'transient', 'defaults']; for (const source of sources) { - const collectionSettings = get(response[source], 'xpack.monitoring.collection'); + const collectionSettings = get(response[source], 'xpack.monitoring.elasticsearch.collection'); if (collectionSettings && collectionSettings.enabled === 'true') { return true; } @@ -308,13 +316,15 @@ async function getLiveElasticsearchCollectionEnabled(req) { export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => { const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); + const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); + const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; const PRODUCTS = [ { name: KIBANA_SYSTEM_ID }, { name: BEATS_SYSTEM_ID }, { name: LOGSTASH_SYSTEM_ID }, - { name: APM_CUSTOM_ID, token: '-beats-' }, - { name: ELASTICSEARCH_CUSTOM_ID, token: '-es-' }, + { name: APM_SYSTEM_ID, token: '-beats-' }, + { name: ELASTICSEARCH_SYSTEM_ID, token: '-es-' }, ]; const [ @@ -322,12 +332,12 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU detectedProducts ] = await Promise.all([ await getRecentMonitoringDocuments(req, indexPatterns, clusterUuid, nodeUuid), - await detectProducts(req) + await detectProducts(req, isLiveCluster) ]); - const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); - const liveEsNodes = skipLiveData || (clusterUuid && liveClusterUuid !== clusterUuid) ? [] : await getLivesNodes(req); - const liveKibanaInstance = skipLiveData || (clusterUuid && liveClusterUuid !== clusterUuid) ? {} : await getLiveKibanaInstance(req); + + const liveEsNodes = skipLiveData || !isLiveCluster ? [] : await getLivesNodes(req); + const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(req); const indicesBuckets = get(recentDocuments, 'aggregations.indices.buckets', []); const liveClusterInternalCollectionEnabled = await getLiveElasticsearchCollectionEnabled(req); @@ -338,6 +348,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU const productStatus = { totalUniqueInstanceCount: 0, + totalUniqueInternallyCollectedCount: 0, totalUniqueFullyMigratedCount: 0, totalUniquePartiallyMigratedCount: 0, detected: null, @@ -348,28 +359,6 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU const internalCollectorsUuidsMap = {}; const partiallyMigratedUuidsMap = {}; - if (product.name === ELASTICSEARCH_CUSTOM_ID && liveEsNodes.length) { - productStatus.byUuid = liveEsNodes.reduce((accum, esNode) => ({ - ...accum, - [esNode.id]: { - node: esNode, - isNetNewUser: true - }, - }), {}); - } - - if (product.name === KIBANA_SYSTEM_ID && liveKibanaInstance) { - const kibanaLiveUuid = get(liveKibanaInstance, 'kibana.uuid'); - if (kibanaLiveUuid) { - productStatus.byUuid = { - [kibanaLiveUuid]: { - instance: liveKibanaInstance, - isNetNewUser: true - } - }; - } - } - // If there is no data, then they are a net new user if (!indexBuckets || indexBuckets.length === 0) { productStatus.totalUniqueInstanceCount = 0; @@ -400,6 +389,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU } } productStatus.totalUniqueInstanceCount = Object.keys(map).length; + productStatus.totalUniqueInternallyCollectedCount = Object.keys(internalCollectorsUuidsMap).length; productStatus.totalUniquePartiallyMigratedCount = Object.keys(partiallyMigratedUuidsMap).length; productStatus.totalUniqueFullyMigratedCount = Object.keys(fullyMigratedUuidsMap).length; productStatus.byUuid = { @@ -435,7 +425,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU } // If there are multiple buckets, they are partially upgraded assuming a single mb index exists else { - const considerAllInstancesMigrated = product.name === ELASTICSEARCH_CUSTOM_ID && + const considerAllInstancesMigrated = product.name === ELASTICSEARCH_SYSTEM_ID && clusterUuid === liveClusterUuid && !liveClusterInternalCollectionEnabled; const internalTimestamps = []; for (const indexBucket of indexBuckets) { @@ -479,6 +469,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU ...Object.keys(fullyMigratedUuidsMap), ...Object.keys(partiallyMigratedUuidsMap) ]).length; + productStatus.totalUniqueInternallyCollectedCount = Object.keys(internalCollectorsUuidsMap).length; productStatus.totalUniquePartiallyMigratedCount = Object.keys(partiallyMigratedUuidsMap).length; productStatus.totalUniqueFullyMigratedCount = Object.keys(fullyMigratedUuidsMap).length; productStatus.byUuid = { @@ -518,6 +509,35 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU productStatus.detected = detectedProducts[product.name]; } + if (product.name === ELASTICSEARCH_SYSTEM_ID && liveEsNodes.length) { + productStatus.byUuid = liveEsNodes.reduce((byUuid, esNode) => { + if (!byUuid[esNode.id]) { + productStatus.totalUniqueInstanceCount++; + return { + ...byUuid, + [esNode.id]: { + node: esNode, + isNetNewUser: true + }, + }; + } + return byUuid; + }, productStatus.byUuid); + } + + if (product.name === KIBANA_SYSTEM_ID && liveKibanaInstance) { + const kibanaLiveUuid = get(liveKibanaInstance, 'kibana.uuid'); + if (kibanaLiveUuid && !productStatus.byUuid[kibanaLiveUuid]) { + productStatus.totalUniqueInstanceCount++; + productStatus.byUuid = { + [kibanaLiveUuid]: { + instance: liveKibanaInstance, + isNetNewUser: true + } + }; + } + } + return { ...products, [product.name]: productStatus, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6887dab9425cee..adc64130aab423 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8171,9 +8171,6 @@ "xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle": "ディスクの空き容量", "xpack.monitoring.elasticsearch.nodes.jvmMemoryColumnTitle": "{javaVirtualMachine} メモリー", "xpack.monitoring.elasticsearch.nodes.loadAverageColumnTitle": "平均負荷", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionDescription": "ご使用の Elasticsearch サーバーはすべて Metricbeat により監視されています。\n 但し、移行を完了させるには内部収集を無効にする必要があります。", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionMigrationButtonLabel": "無効にして移行を完了させる", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionTitle": "内部収集を無効にして移行を完了させる", "xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder": "ノードをフィルタリング…", "xpack.monitoring.elasticsearch.nodes.nameColumnTitle": "名前", "xpack.monitoring.elasticsearch.nodes.routeTitle": "Elasticsearch - ノード", @@ -8241,10 +8238,6 @@ "xpack.monitoring.euiTable.isFullyMigratedLabel": "Metricbeat 収集", "xpack.monitoring.euiTable.isInternalCollectorLabel": "内部収集", "xpack.monitoring.euiTable.isPartiallyMigratedLabel": "内部収集と Metricbeat 収集", - "xpack.monitoring.euiTable.migrateButtonLabel": "移行", - "xpack.monitoring.euiTable.migrationStatusUnknown": "N/A", - "xpack.monitoring.euiTable.setupActionTitle": "セットアップアクション", - "xpack.monitoring.euiTable.setupStatusTitle": "セットアップステータス", "xpack.monitoring.feature.reserved.description": "ユーザーアクセスを許可するには、monitoring_user ロールも割り当てる必要があります。", "xpack.monitoring.featureRegistry.monitoringFeatureName": "スタック監視", "xpack.monitoring.formatNumbers.notAvailableLabel": "N/A", @@ -8366,58 +8359,32 @@ "xpack.monitoring.logstashNavigation.pipelineVersionDescription": "バージョンは {relativeLastSeen} 時点でアクティブ、初回検知 {relativeFirstSeen}", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatDescription": "{file} にこれらの変更を加えます。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatTitle": "Metricbeat を構成して監視クラスターに送ります", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.fullyMigratedStatusDescription": "内部収集からのドキュメントがありません。移行完了!", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.fullyMigratedStatusTitle": "お疲れさまでした!", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "最後の内部収集は {secondsSinceLastInternalCollectionLabel} 前に行われました。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionDescription": "Elasticsearch 監視メトリックの内部収集を無効にします。本番クラスターの各サーバーの {monospace} を false に設定します。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionTitle": "Elasticsearch 監視メトリックの内部収集を無効にする", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleDescription": "モジュールはデフォルトで {url} から Elasticsearch 監視メトリックを収集します。ローカル Elasticsearch サーバーのアドレスが異なる場合は、{module} ファイルのホスト設定で指定する必要があります。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleTitle": "Metricbeat の Elasticsearch X-Pack モジュールの有効化と構成", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.fullyMigratedStatusDescription": "Metricbeat から監視データが送信され始めました!", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatTitle": "Metricbeat を Elasticsearch と同じサーバーにインストール", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.metricbeatSecuritySetup": "セキュリティ機能が有効な場合、追加セットアップが必要な可能性があります。{link}", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.metricbeatSecuritySetupLinkText": "詳細をご覧ください。", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.partiallyMigratedStatusTitle": "現在も Elasticsearch の内部収集からデータが送信されています。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatTitle": "Metricbeat を起動します", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitle": "移行ステータス", "xpack.monitoring.metricbeatMigration.flyout.closeButtonLabel": "閉じる", "xpack.monitoring.metricbeatMigration.flyout.doneButtonLabel": "完了", - "xpack.monitoring.metricbeatMigration.flyout.elasticsearchNode": "ノード", - "xpack.monitoring.metricbeatMigration.flyout.elasticsearchNodesTitle": "Elasticsearch ノード", - "xpack.monitoring.metricbeatMigration.flyout.flyoutTitle": "{instanceName} {instanceType} の Metricbeat への移行", - "xpack.monitoring.metricbeatMigration.flyout.kibanaInstance": "インスタンス", "xpack.monitoring.metricbeatMigration.flyout.nextButtonLabel": "次へ", "xpack.monitoring.metricbeatMigration.flyout.step1.monitoringUrlHelpText": "これは通常単一のインスタンスですが、複数ある場合は、すべてのインスタンス URL をコンマ区切りで入力します。\n Metricbeat インスタンスの実行には、Elasticsearch サーバーとの通信が必要です。", "xpack.monitoring.metricbeatMigration.flyout.step1.monitoringUrlLabel": "監視クラスター URL", "xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatDescription": "{file} にこれらの変更を加えます。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatTitle": "Metricbeat を構成して監視クラスターに送ります", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkingStatusButtonLabel": "確認中...", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkStatusButtonLabel": "確認", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.description": "Kibana 構成ファイル ({file}) に次の設定を追加します:", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.fullyMigratedStatusDescription": "内部収集からのドキュメントがありません。移行完了!", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.note": "{config} をデフォルト値のままにします ({defaultValue})。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "最後の内部収集は {secondsSinceLastInternalCollectionLabel} 前に行われました。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartNote": "このステップには Kibana サーバーの再起動が必要です。サーバーの再起動が完了するまでエラーが表示されます。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartWarningTitle": "警告", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.statusDescription": "内部収集からのドキュメントがないことを確認してください。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.title": "Kibana 監視メトリックの内部収集を無効にする", "xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleDescription": "モジュールはデフォルトで http://localhost:5601 から Kibana 監視メトリックを収集します。ローカル Kibana インスタンスのアドレスが異なる場合は、{file} ファイルの {hosts} 設定で指定する必要があります。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleTitle": "Metricbeat の Kibana X-Pack もウールの有効化と構成", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.fullyMigratedStatusDescription": "Metricbeat から監視データが送信され始めました!", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatTitle": "Metricbeat を Kibana と同じサーバーにインストール", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.metricbeatSecuritySetup": "セキュリティ機能が有効な場合、追加セットアップが必要な可能性があります。{link}", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.metricbeatSecuritySetupLinkText": "詳細をご覧ください。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.partiallyMigratedStatusDescription": "検出には最長 {secondsAgo} 秒かかる場合がありますが、引き続きバックグラウンドで {timePeriod} 秒ごとに確認します。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.partiallyMigratedStatusTitle": "現在も Kibana の内部収集からデータが送信されています。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatTitle": "Metricbeat を起動します", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitle": "移行ステータス", "xpack.monitoring.metrics.apm.outputAckedEventsRate.ackedDescription": "アウトプットにより処理されたイベントです (再試行を含む)", "xpack.monitoring.metrics.apm.outputAckedEventsRate.ackedLabel": "承認済み", "xpack.monitoring.metrics.apm.outputAckedEventsRateTitle": "アウトプット承認イベントレート", @@ -8957,131 +8924,52 @@ "xpack.monitoring.summaryStatus.statusIconLabel": "ステータス: {status}", "xpack.monitoring.summaryStatus.statusIconTitle": "ステータス: {statusIcon}", "xpack.monitoring.uiExportsDescription": "Elastic Stack の監視です", - "xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceDescription": "インデックスによると、APM サーバーがあると思われます。下の「監視をセットアップ」をクリックして\n この APM サーバーの監視を開始してください。", - "xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceTitle": "APM サーバーが検出されました", - "xpack.monitoring.apm.metricbeatMigration.setupNewButtonLabel": "新規 APM サーバーの監視をセットアップ", - "xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceDescription": "インデックスによると、Beats インスタンスがあると思われます。下の「監視をセットアップ」をクリックして\n このインスタンスの監視を開始してください。", - "xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceTitle": "Beats インスタンスが検出されました", - "xpack.monitoring.beats.metricbeatMigration.setupNewButtonLabel": "新規 Beats インスタンスの監視をセットアップ", "xpack.monitoring.chart.timeSeries.zoomOut": "ズームアウト", - "xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip.disableInternal": "すべてのサーバーが Metricbeat によって監視されていますが、内部収集を\n オフにする必要があります。フラッグアイコンをクリックして、サーバーリストページにアクセスし、内部収集を無効にしてください。", - "xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip.oneInternal": "Metricbeat により監視されていないサーバーが少なくとも 1 つあります。フラッグ\n アイコンをクリックして、サーバーリストページにアクセスし、各サーバーの詳細なステータスを確認してください。", - "xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip.disableInternal": "すべてのインスタンスが Metricbeat によって監視されていますが、内部収集を\n オフにする必要があります。フラッグアイコンをクリックして、インスタンスリストページにアクセスし、内部収集を無効にしてください。", - "xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip.oneInternal": "Metricbeat により監視されていないインスタンスが少なくとも 1 つあります。フラッグ\n アイコンをクリックして、インスタンスリストページにアクセスし、各インスタンスの詳細なステータスを確認してください。", - "xpack.monitoring.cluster.overview.elasticsearchPanel.setupModeNodesTooltip.disableInternal": "すべてのノードが Metricbeat によって監視されていますが、内部収集を上のメニューバーの\n フラッグアイコンをクリックして、ノードリストページにアクセスし、内部収集を無効にしてください。", - "xpack.monitoring.cluster.overview.elasticsearchPanel.setupModeNodesTooltip.oneInternal": "Metricbeat により監視されていないノードが少なくとも 1 つあります。フラッグアイコンをクリックして、ノード\n リストページにアクセスし、各ノードの詳細なステータスを確認してください。", - "xpack.monitoring.cluster.overview.kibanaPanel.setupModeNodesTooltip.disableInternal": "すべてのインスタンスが Metricbeat によって監視されていますが、内部収集を\n オフにする必要があります。フラッグアイコンをクリックして、インスタンスリストページにアクセスし、内部収集を無効にしてください。", - "xpack.monitoring.cluster.overview.kibanaPanel.setupModeNodesTooltip.oneInternal": "Metricbeat により監視されていないインスタンスが少なくとも 1 つあります。フラッグ\n アイコンをクリックして、インスタンスリストページにアクセスし、各インスタンスの詳細なステータスを確認してください。", - "xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip.disableInternal": "すべてのノードが Metricbeat によって監視されていますが、内部収集を\n オフにする必要があります。フラッグアイコンをクリックして、ノードリストページにアクセスし、内部収集を無効にしてください。", - "xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip.oneInternal": "Metricbeat により監視されていないノードが少なくとも 1 つあります。フラッグ\n アイコンをクリックして、ノードリストページにアクセスし、各ノードの詳細なステータスを確認してください。", - "xpack.monitoring.elasticsearch.metricbeatMigration.setupNewButtonLabel": "新規 Elasticsearch ノードの監視をセットアップ", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.netNewUserDescription": "監視データは検出されませんでしたが、次の Elasticsearch ノードが検出されました。\n 検出された各ノードとセットアップボタンが下に表示されます。このボタンをクリックすると、\n 各ノードの監視を有効にするプロセスをご案内します。", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.netNewUserTitle": "監視データが検出されませんでした", "xpack.monitoring.errors.monitoringLicenseErrorDescription": "クラスター = 「{clusterId}」のライセンス情報が見つかりませんでした。クラスターのマスターノードサーバーログにエラーや警告がないか確認してください。", "xpack.monitoring.errors.monitoringLicenseErrorTitle": "監視ライセンスエラー", - "xpack.monitoring.euiTable.isNetNewUserLabel": "監視が検出されませんでした", - "xpack.monitoring.euiTable.setupButtonLabel": "セットアップ", - "xpack.monitoring.kibana.metricbeatMigration.setupNewButtonLabel": "新規 Kibana インスタンスの監視をセットアップ", - "xpack.monitoring.kibana.nodes.metribeatMigration.netNewUserDescription": "監視データは検出されませんでしたが、次の Kibana インスタンスが検出されました。\n 検出されたインスタンスとセットアップボタンが下に表示されます。このボタンをクリックすると、\n このインスタンスの監視を有効にするプロセスをご案内します。", - "xpack.monitoring.kibana.nodes.metribeatMigration.netNewUserTitle": "監視データが検出されませんでした", "xpack.monitoring.logs.reason.defaultMessage": "ログデータが見つからず、理由を診断することができません。{link}", "xpack.monitoring.logs.reason.defaultMessageLink": "正しくセットアップされていることを確認してください。", "xpack.monitoring.logs.reason.defaultTitle": "ログデータが見つかりませんでした", - "xpack.monitoring.logstash.metricbeatMigration.setupNewButtonLabel": "新規 Logstash サーバーの監視をセットアップ", - "xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserDescription": "インデックスによると、Logstash ノードがあると思われます。下の「監視をセットアップ」をクリックして\n このノードの監視を開始してください。", - "xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserTitle": "監視データが検出されませんでした", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatDescription": "{file} にこれらの変更を加えます。", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle": "Metricbeat を構成して監視クラスターに送ります", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkingStatusButtonLabel": "確認中...", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkStatusButtonLabel": "確認", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.description": "APM サーバーの構成ファイル ({file}) に次の設定を追加します:", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.fullyMigratedStatusDescription": "内部収集からのドキュメントがありません。移行完了!", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.note": "この変更後、APM サーバーの再起動が必要です。", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "最後の内部収集は {secondsSinceLastInternalCollectionLabel} 前に行われました。", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.statusDescription": "内部収集からのドキュメントがないことを確認してください。", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.title": "APM サーバーの監視メトリックの内部収集を無効にする", "xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleDescription": "モジュールはデフォルトで http://localhost:5066 から APM サーバーの監視メトリックを収集します。ローカル APM サーバーのアドレスが異なる場合は、{file} ファイルの {hosts} 設定で指定する必要があります。", "xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleTitle": "Metricbeat の Beat X-Pack モジュールの有効化と構成", - "xpack.monitoring.metricbeatMigration.apmInstructions.fullyMigratedStatusDescription": "Metricbeat から監視データが送信され始めました!", - "xpack.monitoring.metricbeatMigration.apmInstructions.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatTitle": "Metricbeat を APM サーバーと同じサーバーにインストール", - "xpack.monitoring.metricbeatMigration.apmInstructions.isInternalCollectorStatusTitle": "この APM サーバーの Metricbeat からの監視データが検出されていません。\n 引き続きバックグラウンドで確認を続けます。", - "xpack.monitoring.metricbeatMigration.apmInstructions.metricbeatSecuritySetup": "セキュリティ機能が有効な場合、追加セットアップが必要な可能性があります。{link}", - "xpack.monitoring.metricbeatMigration.apmInstructions.metricbeatSecuritySetupLinkText": "詳細をご覧ください。", - "xpack.monitoring.metricbeatMigration.apmInstructions.partiallyMigratedStatusDescription": "検出には最長 {secondsAgo} 秒かかる場合がありますが、引き続きバックグラウンドで {timePeriod} 秒ごとに確認します。", - "xpack.monitoring.metricbeatMigration.apmInstructions.partiallyMigratedStatusTitle": "現在も APM サーバーの内部収集からデータが送信されています。", "xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatTitle": "Metricbeat を起動します", - "xpack.monitoring.metricbeatMigration.apmInstructions.statusTitle": "移行ステータス", "xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatDescription": "{file} にこれらの変更を加えます。", "xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatTitle": "Metricbeat を構成して監視クラスターに送ります", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkingStatusButtonLabel": "確認中...", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkStatusButtonLabel": "確認", "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.description": "{beatType} の構成ファイル ({file}) に次の設定を追加します:", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.fullyMigratedStatusDescription": "内部収集からのドキュメントがありません。移行完了!", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.note": "この変更後、{beatType} の再起動が必要です。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "最後の内部収集は {secondsSinceLastInternalCollectionLabel} 前に行われました。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.statusDescription": "内部収集からのドキュメントがないことを確認してください。", "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.title": "{beatType} の監視メトリックの内部収集を無効にする", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleDescription": "モジュールはデフォルトで http://localhost:5066 から {beatType} 監視メトリックを収集します。監視されている {beatType} インスタンスのアドレスが異なる場合は、{file} ファイルの {hosts} 設定で指定する必要があります。", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirections": "Metricbeat が実行中の {beatType} からメトリックを収集するには、{link} 必要があります。", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirectionsLinkText": "監視されている {beatType} の HTTP エンドポイントを有効にする", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleTitle": "Metricbeat の Beat X-Pack モジュールの有効化と構成", - "xpack.monitoring.metricbeatMigration.beatsInstructions.fullyMigratedStatusDescription": "Metricbeat から監視データが送信され始めました!", - "xpack.monitoring.metricbeatMigration.beatsInstructions.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatTitle": "Metricbeat を {beatType} と同じサーバーにインストール", - "xpack.monitoring.metricbeatMigration.beatsInstructions.isInternalCollectorStatusTitle": "この Beat の Metricbeat からの監視データが検出されていません。\n 引き続きバックグラウンドで確認を続けます。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.metricbeatSecuritySetup": "セキュリティ機能が有効な場合、追加セットアップが必要な可能性があります。{link}", - "xpack.monitoring.metricbeatMigration.beatsInstructions.metricbeatSecuritySetupLinkText": "詳細をご覧ください。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.partiallyMigratedStatusDescription": "検出には最長 {secondsAgo} 秒かかる場合がありますが、引き続きバックグラウンドで {timePeriod} 秒ごとに確認します。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.partiallyMigratedStatusTitle": "現在も Beat の内部収集からデータが送信されています。", "xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatTitle": "Metricbeat を起動します", - "xpack.monitoring.metricbeatMigration.beatsInstructions.statusTitle": "移行ステータス", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitleNewUser": "監視ステータス", - "xpack.monitoring.metricbeatMigration.flyout.flyoutTitleNewUser": "Metricbeat で {instanceName} {instanceType} を監視", - "xpack.monitoring.metricbeatMigration.flyout.instance": "インスタンス", - "xpack.monitoring.metricbeatMigration.flyout.noClusterUuidCheckboxLabel": "はい、\n この {productName} {typeText} スタンドアロンクラスターを調べる必要があることを理解しています。", - "xpack.monitoring.metricbeatMigration.flyout.noClusterUuidDescription": "この {productName} {typeText} は Elasticsearch クラスターに接続されていないため、完全に移行された時点で、この {productName} {typeText} はこのクラスターではなくスタンドアロンクラスターに表示されます。 {link}", "xpack.monitoring.metricbeatMigration.flyout.noClusterUuidTitle": "クラスターが検出されてませんでした", - "xpack.monitoring.metricbeatMigration.flyout.node": "ノード", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitleNewUser": "監視ステータス", "xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatDescription": "{file} にこれらの変更を加えます。", "xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatTitle": "Metricbeat を構成して監視クラスターに送ります", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkingStatusButtonLabel": "確認中...", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkStatusButtonLabel": "確認", "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.description": "Logstash 構成ファイル ({file}) に次の設定を追加します:", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.fullyMigratedStatusDescription": "内部収集からのドキュメントがありません。移行完了!", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.note": "この変更後、Logstash の再起動が必要です。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "最後の内部収集は {secondsSinceLastInternalCollectionLabel} 前に行われました。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.statusDescription": "内部収集からのドキュメントがないことを確認してください。", "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.title": "Logstash 監視メトリックの内部収集を無効にする", "xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleDescription": "モジュールはデフォルトで http://localhost:9600 から Logstash 監視メトリックを収集します。ローカル Logstash インスタンスのアドレスが異なる場合は、{file} ファイルの {hosts} 設定で指定する必要があります。", "xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleTitle": "Metricbeat の Logstash X-Pack もウールの有効化と構成", - "xpack.monitoring.metricbeatMigration.logstashInstructions.fullyMigratedStatusDescription": "Metricbeat から監視データが送信され始めました!", - "xpack.monitoring.metricbeatMigration.logstashInstructions.fullyMigratedStatusTitle": "お疲れさまでした!", "xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatTitle": "Metricbeat を Logstash と同じサーバーにインストール", - "xpack.monitoring.metricbeatMigration.logstashInstructions.isInternalCollectorStatusTitle": "この Metricbeat ノードの Metricbeat からの監視データが検出されていません。\n 引き続きバックグラウンドで確認を続けます。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.metricbeatSecuritySetup": "セキュリティ機能が有効な場合、追加セットアップが必要な可能性があります。{link}", - "xpack.monitoring.metricbeatMigration.logstashInstructions.metricbeatSecuritySetupLinkText": "詳細をご覧ください。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.partiallyMigratedStatusDescription": "検出には最長 {secondsAgo} 秒かかる場合がありますが、引き続きバックグラウンドで {timePeriod} 秒ごとに確認します。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.partiallyMigratedStatusTitle": "現在も Logstash の内部収集からデータが送信されています。", "xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatLinkText": "こちらの手順に従ってください", "xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatTitle": "Metricbeat を起動します", - "xpack.monitoring.metricbeatMigration.logstashInstructions.statusTitle": "移行ステータス", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "次の場所に戻ってください: ", "xpack.monitoring.noData.blurbs.cloudDeploymentDescriptionMore": "Elastic Cloud での監視の詳細は、 ", "xpack.monitoring.noData.blurbs.cloudDeploymentTitle": "監視データはこちらに表示されません。", "xpack.monitoring.noData.explanations.exportersCloudDescription": "Elastic Cloud では、監視データが専用の監視クラスターに格納されます。", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.isInternalCollectorStatusTitle": "この Elasticsearch ノードの Metricbeat からの監視データが検出されていません。\n 引き続きバックグラウンドで確認を続けます。", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.partiallyMigratedStatusDescription": "検出には最長 {secondsAgo} 秒かかる場合がありますが、引き続きバックグラウンドで確認します。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.isInternalCollectorStatusTitle": "この Kibana インスタンスの Metricbeat からの監視データが検出されていません。\n 引き続きバックグラウンドで確認を続けます。", "xpack.remoteClusters.addAction.clusterNameAlreadyExistsErrorMessage": "「{clusterName}」という名前のクラスターが既に存在します。", "xpack.remoteClusters.addAction.errorTitle": "クラスターの追加中にエラーが発生", "xpack.remoteClusters.addAction.failedDefaultErrorMessage": "{statusCode} エラーでリクエスト失敗: {message}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 649ead90b63561..31616c9e6db7cf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8173,9 +8173,6 @@ "xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle": "磁盘可用空间", "xpack.monitoring.elasticsearch.nodes.jvmMemoryColumnTitle": "{javaVirtualMachine} 内存", "xpack.monitoring.elasticsearch.nodes.loadAverageColumnTitle": "负载平均值", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionDescription": "系统将使用 Metricbeat 监测所有 Elasticsearch 服务器,\n 但您需要禁用内部收集以完成迁移。", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionMigrationButtonLabel": "禁用并完成迁移", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.disableInternalCollectionTitle": "禁用内部收集以完成迁移", "xpack.monitoring.elasticsearch.nodes.monitoringTablePlaceholder": "筛选节点……", "xpack.monitoring.elasticsearch.nodes.nameColumnTitle": "名称", "xpack.monitoring.elasticsearch.nodes.routeTitle": "Elasticsearch - 节点", @@ -8243,10 +8240,6 @@ "xpack.monitoring.euiTable.isFullyMigratedLabel": "Metricbeat 收集", "xpack.monitoring.euiTable.isInternalCollectorLabel": "内部收集", "xpack.monitoring.euiTable.isPartiallyMigratedLabel": "内部收集和 Metricbeat 收集", - "xpack.monitoring.euiTable.migrateButtonLabel": "迁移", - "xpack.monitoring.euiTable.migrationStatusUnknown": "不适用", - "xpack.monitoring.euiTable.setupActionTitle": "设置操作", - "xpack.monitoring.euiTable.setupStatusTitle": "设置状态", "xpack.monitoring.feature.reserved.description": "要向用户授予访问权限,还应分配 monitoring_user 角色。", "xpack.monitoring.featureRegistry.monitoringFeatureName": "堆栈监测", "xpack.monitoring.formatNumbers.notAvailableLabel": "不适用", @@ -8368,58 +8361,32 @@ "xpack.monitoring.logstashNavigation.pipelineVersionDescription": "活动版本 {relativeLastSeen} 和首次看到 {relativeFirstSeen}", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatDescription": "在 {file} 文件中进行这些更改。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatTitle": "配置 Metricbeat 以发送至监测集群", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.fullyMigratedStatusDescription": "我们未看到任何来自内部收集的文档。迁移完成!", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.fullyMigratedStatusTitle": "恭喜您!", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "上次内部收集发生于 {secondsSinceLastInternalCollectionLabel}前。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionDescription": "禁用 Elasticsearch 监测指标的内部收集。在生产集群中的每个服务器上将 {monospace} 设置为 false。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.disableInternalCollectionTitle": "禁用 Elasticsearch 监测指标的内部收集", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleDescription": "默认情况下,该模块将从 {url} 收集 Elasticsearch 监测指标。如果本地 Elasticsearch 服务器有不同的地址,则必须通过 {module} 文件中的 hosts 设置来进行指定。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleTitle": "在 Metricbeat 中启用并配置 Elasticsearch x-pack 模块", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.fullyMigratedStatusDescription": "我们现在看到来自 Metricbeat 的监测数据传送!", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatTitle": "在安装 Elasticsearch 的同一台服务器上安装 Metricbeat", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.metricbeatSecuritySetup": "如果已启用安全功能,则可能需要更多的设置。{link}", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.metricbeatSecuritySetupLinkText": "查看更多信息。", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.partiallyMigratedStatusTitle": "我们仍看到数据来自 Elasticsearch 的内部收集。", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatTitle": "启动 Metricbeat", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitle": "迁移状态", "xpack.monitoring.metricbeatMigration.flyout.closeButtonLabel": "关闭", "xpack.monitoring.metricbeatMigration.flyout.doneButtonLabel": "完成", - "xpack.monitoring.metricbeatMigration.flyout.elasticsearchNode": "节点", - "xpack.monitoring.metricbeatMigration.flyout.elasticsearchNodesTitle": "Elasticsearch 节点", - "xpack.monitoring.metricbeatMigration.flyout.flyoutTitle": "将 {instanceName} {instanceType} 迁移到 Metricbeat", - "xpack.monitoring.metricbeatMigration.flyout.kibanaInstance": "实例", "xpack.monitoring.metricbeatMigration.flyout.nextButtonLabel": "下一个", "xpack.monitoring.metricbeatMigration.flyout.step1.monitoringUrlHelpText": "这通常是单个实例,但如果您有多个,请输入所有实例 url,以逗号分隔。\n 切记运行的 Metricbeat 实例需要能够与这些 Elasticsearch 实例通信。", "xpack.monitoring.metricbeatMigration.flyout.step1.monitoringUrlLabel": "监测集群 URL", "xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatDescription": "在 {file} 文件中进行这些更改。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatTitle": "配置 Metricbeat 以发送至监测集群", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkingStatusButtonLabel": "正在检查......", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.checkStatusButtonLabel": "检查", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.description": "在 Kibana 配置文件 ({file}) 中添加以下设置:", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.fullyMigratedStatusDescription": "我们未看到任何来自内部收集的文档。迁移完成!", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.note": "将 {config} 设置为其默认值 ({defaultValue})。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "上次内部收集发生于 {secondsSinceLastInternalCollectionLabel}前。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartNote": "此步骤需要您重新启动 Kibana 服务器。在服务器再次运行之前应会看到错误。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.restartWarningTitle": "警告", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.statusDescription": "确认没有文档来自内部收集。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.disableInternalCollection.title": "禁用 Kibana 监测指标的内部收集", "xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleDescription": "该模块将默认从 http://localhost:5601 收集 Kibana 监测指标。如果本地 Kibana 实例有不同的地址,则必须通过 {file} 文件中的 {hosts} 设置进行指定。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleTitle": "在 Metricbeat 中启用并配置 Kibana x-pack 模块", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.fullyMigratedStatusDescription": "我们现在看到来自 Metricbeat 的监测数据传送!", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatTitle": "在安装 Kibana 的同一台服务器上安装 Metricbeat", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.metricbeatSecuritySetup": "如果已启用安全功能,则可能需要更多的设置。{link}", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.metricbeatSecuritySetupLinkText": "查看更多信息。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.partiallyMigratedStatusDescription": "请注意,检测最多花费 {secondsAgo} 秒,但我们在后台每 {timePeriod} 秒检查一次。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.partiallyMigratedStatusTitle": "我们仍看到数据来自 Kibana 的内部收集。", "xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatTitle": "启动 Metricbeat", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitle": "迁移状态", "xpack.monitoring.metrics.apm.outputAckedEventsRate.ackedDescription": "输出处理的事件(包括重试)", "xpack.monitoring.metrics.apm.outputAckedEventsRate.ackedLabel": "已确认", "xpack.monitoring.metrics.apm.outputAckedEventsRateTitle": "输出已确认事件速率", @@ -8959,131 +8926,52 @@ "xpack.monitoring.summaryStatus.statusIconLabel": "状态:{status}", "xpack.monitoring.summaryStatus.statusIconTitle": "状态:{statusIcon}", "xpack.monitoring.uiExportsDescription": "Elastic Stack 的 Monitoring 组件", - "xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceDescription": "基于您的索引,我们认为您可能有 APM Server。单击下面的“设置监测”\n 按钮以开始监测此 APM Server。", - "xpack.monitoring.apm.instances.metricbeatMigration.detectedInstanceTitle": "检测到 APM Server", - "xpack.monitoring.apm.metricbeatMigration.setupNewButtonLabel": "为新的 APM Server 设置监测", - "xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceDescription": "基于您的索引,我们认为您可能有 Beats 实例。单击下面的“设置监测”\n 按钮以开始监测此实例。", - "xpack.monitoring.beats.instances.metricbeatMigration.detectedInstanceTitle": "检测到 Beats 实例", - "xpack.monitoring.beats.metricbeatMigration.setupNewButtonLabel": "为新的 Beats 实例设置监测", "xpack.monitoring.chart.timeSeries.zoomOut": "缩小", - "xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip.disableInternal": "正在使用 Metricbeat 监测所有服务器,但内部收集仍需要\n 关闭。单击旗帜图标可访问服务器列表页面以及禁用内部收集。", - "xpack.monitoring.cluster.overview.apmPanel.setupModeNodesTooltip.oneInternal": "至少有一个服务器未使用 Metricbeat 进行监测。单击旗帜\n 图标可访问服务器列表页面以及详细了解每个服务器的状态。", - "xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip.disableInternal": "正在使用 Metricbeat 监测所有实例,但内部收集仍需要\n 关闭。单击旗帜图标可访问实例列表页面以及禁用内部收集。", - "xpack.monitoring.cluster.overview.beatsPanel.setupModeNodesTooltip.oneInternal": "至少有一个实例未使用 Metricbeat 进行监测。单击旗帜\n 图标可访问实例列表页面以及详细了解每个实例的状态。", - "xpack.monitoring.cluster.overview.elasticsearchPanel.setupModeNodesTooltip.disableInternal": "正在使用 Metricbeat 监测所有节点,但内部收集仍需要关闭。单击\n 旗帜图标可访问节点列表页面以及禁用内部收集。", - "xpack.monitoring.cluster.overview.elasticsearchPanel.setupModeNodesTooltip.oneInternal": "至少有一个节点未使用 Metricbeat 进行监测。单击旗帜图标可访问节点\n 列表页面以及详细了解每个节点的状态。", - "xpack.monitoring.cluster.overview.kibanaPanel.setupModeNodesTooltip.disableInternal": "正在使用 Metricbeat 监测所有实例,但内部收集仍需要\n 关闭。单击旗帜图标可访问实例列表页面以及禁用内部收集。", - "xpack.monitoring.cluster.overview.kibanaPanel.setupModeNodesTooltip.oneInternal": "至少有一个实例未使用 Metricbeat 进行监测。单击旗帜\n 图标可访问实例列表页面以及详细了解每个实例的状态。", - "xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip.disableInternal": "正在使用 Metricbeat 监测所有节点,但内部收集仍需要\n 关闭。单击旗帜图标可访问节点列表页面以及禁用内部收集。", - "xpack.monitoring.cluster.overview.logstashPanel.setupModeNodesTooltip.oneInternal": "至少有一个节点未使用 Metricbeat 进行监测。单击旗帜\n 图标可访问节点列表页面以及详细了解每个节点的状态。", - "xpack.monitoring.elasticsearch.metricbeatMigration.setupNewButtonLabel": "为新的 Elasticsearch 节点设置监测", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.netNewUserDescription": "我们未检测到任何监测数据,但我们却检测到以下 Elasticsearch 节点。\n 检测到的每个节点与相应的“设置”按钮在下面一起列出。单击此按钮,系统将指导您完成\n 为每个节点启用监测的过程。", - "xpack.monitoring.elasticsearch.nodes.metribeatMigration.netNewUserTitle": "未检测到任何监测数据", "xpack.monitoring.errors.monitoringLicenseErrorDescription": "无法找到集群“{clusterId}”的许可信息。请在集群的主节点服务器日志中查看相关错误或警告。", "xpack.monitoring.errors.monitoringLicenseErrorTitle": "监测许可错误", - "xpack.monitoring.euiTable.isNetNewUserLabel": "未检测到监测", - "xpack.monitoring.euiTable.setupButtonLabel": "设置", - "xpack.monitoring.kibana.metricbeatMigration.setupNewButtonLabel": "为新的 Kibana 实例设置监测", - "xpack.monitoring.kibana.nodes.metribeatMigration.netNewUserDescription": "我们未检测到任何监测数据,但我们却检测到以下 Kibana 实例。\n 该检测到的实例与相应的“设置”按钮在下面一起列出。单击此按钮,系统将指导您完成\n 为此实例启用监测的过程。", - "xpack.monitoring.kibana.nodes.metribeatMigration.netNewUserTitle": "未检测到任何监测数据", "xpack.monitoring.logs.reason.defaultMessage": "我们未找到任何日志数据,我们无法诊断原因。{link}", "xpack.monitoring.logs.reason.defaultMessageLink": "请确认您的设置正确。", "xpack.monitoring.logs.reason.defaultTitle": "未找到任何日志数据", - "xpack.monitoring.logstash.metricbeatMigration.setupNewButtonLabel": "为新的 Logstash 节点设置监测", - "xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserDescription": "基于您的索引,我们认为您可能有 Logstash 节点。单击下面的“设置监测”\n 按钮以开始监测此节点。", - "xpack.monitoring.logstash.nodes.metribeatMigration.netNewUserTitle": "未检测到任何监测数据", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatDescription": "在 {file} 文件中进行这些更改。", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle": "配置 Metricbeat 以发送至监测集群", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkingStatusButtonLabel": "正在检查......", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.checkStatusButtonLabel": "检查", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.description": "在 APM Server 的配置文件 ({file}) 中添加以下设置:", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.fullyMigratedStatusDescription": "我们未看到任何来自内部收集的文档。迁移完成!", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.note": "进行此更改后,需要重新启动 APM Server。", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "上次内部收集发生于 {secondsSinceLastInternalCollectionLabel}前。", - "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.statusDescription": "确认没有文档来自内部收集。", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.title": "禁用 APM Server 监测指标的内部收集", "xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleDescription": "该模块将默认从 http://localhost:5066 收集 APM Server 监测指标。如果本地 APM Server 有不同的地址,则必须通过 {file} 文件中的 {hosts} 设置进行指定。", "xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleTitle": "在 Metricbeat 中启用并配置 Beat x-pack 模块", - "xpack.monitoring.metricbeatMigration.apmInstructions.fullyMigratedStatusDescription": "我们现在看到来自 Metricbeat 的监测数据传送!", - "xpack.monitoring.metricbeatMigration.apmInstructions.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatTitle": "在安装 APM Server 的同一台服务器上安装 Metricbeat", - "xpack.monitoring.metricbeatMigration.apmInstructions.isInternalCollectorStatusTitle": "我们未检测到任何监测数据来自此 APM Server 的 Metricbeat。\n 我们将持续在后台检查。", - "xpack.monitoring.metricbeatMigration.apmInstructions.metricbeatSecuritySetup": "如果已启用安全功能,则可能需要更多的设置。{link}", - "xpack.monitoring.metricbeatMigration.apmInstructions.metricbeatSecuritySetupLinkText": "查看更多信息。", - "xpack.monitoring.metricbeatMigration.apmInstructions.partiallyMigratedStatusDescription": "请注意,检测最多花费 {secondsAgo} 秒,但我们在后台每 {timePeriod} 秒检查一次。", - "xpack.monitoring.metricbeatMigration.apmInstructions.partiallyMigratedStatusTitle": "我们仍看到数据来自此 APM Server 的内部收集。", "xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatTitle": "启动 Metricbeat", - "xpack.monitoring.metricbeatMigration.apmInstructions.statusTitle": "迁移状态", "xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatDescription": "在 {file} 文件中进行这些更改。", "xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatTitle": "配置 Metricbeat 以发送至监测集群", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkingStatusButtonLabel": "正在检查......", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.checkStatusButtonLabel": "检查", "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.description": "在 {beatType} 的配置文件 ({file}) 中添加以下设置:", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.fullyMigratedStatusDescription": "我们未看到任何来自内部收集的文档。迁移完成!", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.note": "进行此更改后,您需要重新启动 {beatType}。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "上次内部收集发生于 {secondsSinceLastInternalCollectionLabel}前。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.statusDescription": "确认没有文档来自内部收集。", "xpack.monitoring.metricbeatMigration.beatsInstructions.disableInternalCollection.title": "禁用 {beatType} 监测指标的内部收集", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleDescription": "该模块将默认从 http://localhost:5066 收集 {beatType} 监测指标。如果正在监测的 {beatType} 实例有不同的地址,则必须通过 {file} 文件中的 {hosts} 设置进行指定。", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirections": "要使 Metricbeat 从正在运行的 {beatType} 收集指标,需要{link}。", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirectionsLinkText": "为正在监测的 {beatType} 实例启用 HTTP 终端节点", "xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleTitle": "在 Metricbeat 中启用并配置 Beat x-pack 模块", - "xpack.monitoring.metricbeatMigration.beatsInstructions.fullyMigratedStatusDescription": "我们现在看到来自 Metricbeat 的监测数据传送!", - "xpack.monitoring.metricbeatMigration.beatsInstructions.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatTitle": "在安装此 {beatType} 的同一台服务器上安装 Metricbeat", - "xpack.monitoring.metricbeatMigration.beatsInstructions.isInternalCollectorStatusTitle": "我们未检测到任何监测数据来自此 Beat 的 Metricbeat。\n 我们将持续在后台检查。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.metricbeatSecuritySetup": "如果已启用安全功能,则可能需要更多的设置。{link}", - "xpack.monitoring.metricbeatMigration.beatsInstructions.metricbeatSecuritySetupLinkText": "查看更多信息。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.partiallyMigratedStatusDescription": "请注意,检测最多花费 {secondsAgo} 秒,但我们在后台每 {timePeriod} 秒检查一次。", - "xpack.monitoring.metricbeatMigration.beatsInstructions.partiallyMigratedStatusTitle": "我们仍看到数据来自此 Beat 的内部收集。", "xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatTitle": "启动 Metricbeat", - "xpack.monitoring.metricbeatMigration.beatsInstructions.statusTitle": "迁移状态", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.statusTitleNewUser": "监测状态", - "xpack.monitoring.metricbeatMigration.flyout.flyoutTitleNewUser": "使用 Metricbeat 监测 {instanceName} {instanceType}", - "xpack.monitoring.metricbeatMigration.flyout.instance": "实例", - "xpack.monitoring.metricbeatMigration.flyout.noClusterUuidCheckboxLabel": "是的,我明白我将需要在独立集群中寻找\n 此 {productName} {typeText}。", - "xpack.monitoring.metricbeatMigration.flyout.noClusterUuidDescription": "此 {productName} {typeText} 未连接到 Elasticsearch 集群,因此完全迁移后,此 {productName} {typeText} 将显示在独立集群中,而非此集群中。{link}", "xpack.monitoring.metricbeatMigration.flyout.noClusterUuidTitle": "未检测到集群", - "xpack.monitoring.metricbeatMigration.flyout.node": "节点", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.statusTitleNewUser": "监测状态", "xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatDescription": "在 {file} 文件中进行这些更改。", "xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatTitle": "配置 Metricbeat 以发送至监测集群", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkingStatusButtonLabel": "正在检查......", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.checkStatusButtonLabel": "检查", "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.description": "在 Logstash 配置文件 ({file}) 中添加以下设置:", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.fullyMigratedStatusDescription": "我们未看到任何来自内部收集的文档。迁移完成!", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.note": "进行此更改后,您需要重新启动 Logstash。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.partiallyMigratedStatusDescription": "上次内部收集发生于 {secondsSinceLastInternalCollectionLabel}前。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.statusDescription": "确认没有文档来自内部收集。", "xpack.monitoring.metricbeatMigration.logstashInstructions.disableInternalCollection.title": "禁用 Logstash 监测指标的内部收集", "xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleDescription": "该模块将默认从 http://localhost:9600 收集 Logstash 监测指标。如果本地 Logstash 实例有不同的地址,则必须通过 {file} 文件中的 {hosts} 设置进行指定。", "xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleTitle": "在 Metricbeat 中启用并配置 Logstash x-pack 模块", - "xpack.monitoring.metricbeatMigration.logstashInstructions.fullyMigratedStatusDescription": "我们现在看到来自 Metricbeat 的监测数据传送!", - "xpack.monitoring.metricbeatMigration.logstashInstructions.fullyMigratedStatusTitle": "恭喜您!", "xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatTitle": "在安装 Logstash 的同一台服务器上安装 Metricbeat", - "xpack.monitoring.metricbeatMigration.logstashInstructions.isInternalCollectorStatusTitle": "我们未检测到任何监测数据来自此 Logstash 节点的 Metricbeat。\n 我们将持续在后台检查。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.metricbeatSecuritySetup": "如果已启用安全功能,则可能需要更多的设置。{link}", - "xpack.monitoring.metricbeatMigration.logstashInstructions.metricbeatSecuritySetupLinkText": "查看更多信息。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.partiallyMigratedStatusDescription": "请注意,检测最多花费 {secondsAgo} 秒,但我们在后台每 {timePeriod} 秒检查一次。", - "xpack.monitoring.metricbeatMigration.logstashInstructions.partiallyMigratedStatusTitle": "我们仍看到数据来自 Logstash 的内部收集。", "xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatLinkText": "按照此处的说明执行操作", "xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatTitle": "启动 Metricbeat", - "xpack.monitoring.metricbeatMigration.logstashInstructions.statusTitle": "迁移状态", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "请返回到您的 ", "xpack.monitoring.noData.blurbs.cloudDeploymentDescriptionMore": "有关在 Elastic Cloud 中监测的详情,请参阅 ", "xpack.monitoring.noData.blurbs.cloudDeploymentTitle": "此处没有您的监测数据。", "xpack.monitoring.noData.explanations.exportersCloudDescription": "在 Elastic Cloud 中,您的监测数据将存储在专用监测集群中。", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.isInternalCollectorStatusTitle": "我们未检测到任何监测数据来自此 Elasticsearch 节点的 Metricbeat。\n 我们将持续在后台检查。", - "xpack.monitoring.metricbeatMigration.elasticsearchInstructions.partiallyMigratedStatusDescription": "请注意,检测最多花费 {secondsAgo} 秒,但我们将在后台持续检查。", - "xpack.monitoring.metricbeatMigration.kibanaInstructions.isInternalCollectorStatusTitle": "我们未检测到任何监测数据来自此 Kibana 实例的 Metricbeat。\n 我们将持续在后台检查。", "xpack.remoteClusters.addAction.clusterNameAlreadyExistsErrorMessage": "名为 “{clusterName}” 的集群已存在。", "xpack.remoteClusters.addAction.errorTitle": "添加集群时出错", "xpack.remoteClusters.addAction.failedDefaultErrorMessage": "请求失败,显示 {statusCode} 错误。{message}", diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json index 518c074bc985d7..a791c2b2b7419d 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, @@ -12,6 +13,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -21,6 +23,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -30,6 +33,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": true }, @@ -39,6 +43,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json index 219b2194d04db1..3ce2f20415b5f2 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 1, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "5b2de169-2785-441b-ae8c-186a1936b17d": { @@ -17,6 +18,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": true }, @@ -26,6 +28,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -35,6 +38,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -44,6 +48,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 1, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "agI8JhXhShasvuDgq0VxRg": { diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json index 72e44a3227728e..a64e2f40b33dc9 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, @@ -12,6 +13,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": true }, @@ -21,6 +23,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -30,6 +33,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -39,6 +43,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json index a30ee6c04640d4..cc870216d405bd 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, @@ -12,6 +13,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -21,6 +23,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": true }, @@ -30,6 +33,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -39,6 +43,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json index a30ee6c04640d4..cc870216d405bd 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, @@ -12,6 +13,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -21,6 +23,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": true }, @@ -30,6 +33,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -39,6 +43,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "doesExist": true }, diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json index 28f90aeba7ceb2..4ae753aca52251 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 1, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "5b2de169-2785-441b-ae8c-186a1936b17d": { @@ -17,6 +18,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "8eba4902-df80-43b0-b6c2-ed8ca290984e": { @@ -32,6 +34,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "4134a00e-89e4-4896-a3d4-c3a9aa03a594": { @@ -46,6 +49,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -55,6 +59,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 1, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "agI8JhXhShasvuDgq0VxRg": { diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json index 16d079ea9b7e94..6935060b4d5f7e 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 1, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "5b2de169-2785-441b-ae8c-186a1936b17d": { @@ -17,6 +18,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "8eba4902-df80-43b0-b6c2-ed8ca290984e": { @@ -32,6 +34,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "4134a00e-89e4-4896-a3d4-c3a9aa03a594": { @@ -46,6 +49,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -55,6 +59,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 1, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "agI8JhXhShasvuDgq0VxRg": { diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json index 7f5e8cb8897825..161ce32e8ff5f3 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 1, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "5b2de169-2785-441b-ae8c-186a1936b17d": { @@ -17,6 +18,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "8eba4902-df80-43b0-b6c2-ed8ca290984e": { @@ -32,6 +34,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "4134a00e-89e4-4896-a3d4-c3a9aa03a594": { @@ -46,6 +49,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -55,6 +59,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 1, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "agI8JhXhShasvuDgq0VxRg": { diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json index 3c6da934bbf94e..b93edacd82b310 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json @@ -3,6 +3,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 1, + "totalUniqueInternallyCollectedCount": 0, "detected": null, "byUuid": { "5b2de169-2785-441b-ae8c-186a1936b17d": { @@ -17,6 +18,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "8eba4902-df80-43b0-b6c2-ed8ca290984e": { @@ -32,6 +34,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "4134a00e-89e4-4896-a3d4-c3a9aa03a594": { @@ -46,6 +49,7 @@ "totalUniqueInstanceCount": 0, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 0, "detected": { "mightExist": false }, @@ -55,6 +59,7 @@ "totalUniqueInstanceCount": 1, "totalUniqueFullyMigratedCount": 0, "totalUniquePartiallyMigratedCount": 0, + "totalUniqueInternallyCollectedCount": 1, "detected": null, "byUuid": { "agI8JhXhShasvuDgq0VxRg": { diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js index 7fbb8990d1f27d..c3fe5f9273a897 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/index_detail.js @@ -45,8 +45,8 @@ export default function ({ getService, getPageObjects }) { dataSize: 'Total\n8.8 MB', dataSizePrimaries: 'Primaries\n4.4 MB', documentCount: 'Documents\n628', - totalShards: 'Total Shards\n10', - unassignedShards: 'Unassigned Shards\n0', + totalShards: 'Total shards\n10', + unassignedShards: 'Unassigned shards\n0', health: 'Health: green', }); }); @@ -58,8 +58,8 @@ export default function ({ getService, getPageObjects }) { dataSize: 'Total\n4.8 KB', dataSizePrimaries: 'Primaries\n4.8 KB', documentCount: 'Documents\n1', - totalShards: 'Total Shards\n1', - unassignedShards: 'Unassigned Shards\n0', + totalShards: 'Total shards\n1', + unassignedShards: 'Unassigned shards\n0', health: 'Health: green', }); }); @@ -71,8 +71,8 @@ export default function ({ getService, getPageObjects }) { dataSize: 'Total\n1.2 MB', dataSizePrimaries: 'Primaries\n657.6 KB', documentCount: 'Documents\n10', - totalShards: 'Total Shards\n10', - unassignedShards: 'Unassigned Shards\n1', + totalShards: 'Total shards\n10', + unassignedShards: 'Unassigned shards\n1', health: 'Health: yellow', }); }); diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js b/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js index fc3669079dbce2..ff809b95a834e1 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js @@ -35,8 +35,8 @@ export default function ({ getService, getPageObjects }) { nodesCount: 'Nodes\n1', indicesCount: 'Indices\n19', memory: 'Memory\n267.7 MB / 676.8 MB', - totalShards: 'Total Shards\n46', - unassignedShards: 'Unassigned Shards\n23', + totalShards: 'Total shards\n46', + unassignedShards: 'Unassigned shards\n23', documentCount: 'Documents\n4,535', dataSize: 'Data\n8.6 MB', health: 'Health: red', diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js index 81f683dc9a4cee..86f47775e50cdd 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js @@ -39,8 +39,8 @@ export default function ({ getService, getPageObjects }) { nodesCount: 'Nodes\n2', indicesCount: 'Indices\n20', memory: 'Memory\n696.6 MB / 1.3 GB', - totalShards: 'Total Shards\n79', - unassignedShards: 'Unassigned Shards\n7', + totalShards: 'Total shards\n79', + unassignedShards: 'Unassigned shards\n7', documentCount: 'Documents\n25,758', dataSize: 'Data\n100.0 MB', health: 'Health: yellow', @@ -214,8 +214,8 @@ export default function ({ getService, getPageObjects }) { nodesCount: 'Nodes\n3', indicesCount: 'Indices\n20', memory: 'Memory\n575.3 MB / 2.0 GB', - totalShards: 'Total Shards\n80', - unassignedShards: 'Unassigned Shards\n5', + totalShards: 'Total shards\n80', + unassignedShards: 'Unassigned shards\n5', documentCount: 'Documents\n25,927', dataSize: 'Data\n101.6 MB', health: 'Health: yellow', diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js b/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js index 3f90c6a096e901..d86127b3e6fb8d 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js @@ -35,8 +35,8 @@ export default function ({ getService, getPageObjects }) { nodesCount: 'Nodes\n3', indicesCount: 'Indices\n20', memory: 'Memory\n575.3 MB / 2.0 GB', - totalShards: 'Total Shards\n80', - unassignedShards: 'Unassigned Shards\n5', + totalShards: 'Total shards\n80', + unassignedShards: 'Unassigned shards\n5', documentCount: 'Documents\n25,927', dataSize: 'Data\n101.6 MB', health: 'Health: yellow', diff --git a/x-pack/test/functional/services/monitoring/no_data.js b/x-pack/test/functional/services/monitoring/no_data.js index a0a1998689ee1c..81ed366ce94b2a 100644 --- a/x-pack/test/functional/services/monitoring/no_data.js +++ b/x-pack/test/functional/services/monitoring/no_data.js @@ -10,6 +10,7 @@ export function MonitoringNoDataProvider({ getService }) { return new class NoData { async enableMonitoring() { + await testSubjects.click('useInternalCollection'); await testSubjects.click('enableCollectionEnabled'); } From 3f7c3e0d55bc5a863596a80b41c066d52b887e71 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 2 Oct 2019 12:43:16 -0400 Subject: [PATCH 44/53] [Monitoring] Ensure all charts use the configured timezone (#45949) * Consistently apply dateFormat:tz to all monitoring time-series data * Ensure browser timezone works properly * Fix tests * Fix other test * Simplfy timezone fetching * Fix tests --- .../plugins/monitoring/common/formatting.js | 6 +++-- .../public/components/chart/chart_target.js | 8 +++--- .../components/chart/get_chart_options.js | 7 ++++-- .../monitoring/public/components/logs/logs.js | 4 +-- .../lib/details/__test__/get_metrics.test.js | 5 +++- .../server/lib/details/get_metrics.js | 6 +++-- .../server/lib/details/get_series.js | 14 +++++++---- .../monitoring/server/lib/format_timezone.js | 25 +++++++++++++++++++ .../monitoring/server/lib/get_timezone.js | 9 +++++++ .../monitoring/server/lib/logs/get_logs.js | 7 +++++- .../logs/fixtures/index_detail.json | 2 +- .../monitoring/logs/fixtures/node_detail.json | 20 +++++++-------- 12 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js index e94ed44efff050..a3b3ce07c8c760 100644 --- a/x-pack/legacy/plugins/monitoring/common/formatting.js +++ b/x-pack/legacy/plugins/monitoring/common/formatting.js @@ -17,8 +17,10 @@ export const LARGE_ABBREVIATED = '0,0.[0]a'; * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date) { - return moment.tz(date, moment.tz.guess()).format('LL LTS'); +export function formatDateTimeLocal(date, useUTC = false) { + return useUTC + ? moment.utc(date).format('LL LTS') + : moment.tz(date, moment.tz.guess()).format('LL LTS'); } /** diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js index 9f425a81d86673..9d5ebd274ea9ec 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js @@ -76,8 +76,8 @@ export class ChartTarget extends React.Component { .value(); } - getOptions() { - const opts = getChartOptions({ + async getOptions() { + const opts = await getChartOptions({ yaxis: { tickFormatter: this.props.tickFormatter }, xaxis: this.props.timeRange }); @@ -88,12 +88,12 @@ export class ChartTarget extends React.Component { }; } - renderChart() { + async renderChart() { const { target } = this.refs; const { series } = this.props; const data = this.filterData(series, this.props.seriesToShow); - this.plot = $.plot(target, data, this.getOptions()); + this.plot = $.plot(target, data, await this.getOptions()); this._handleResize = () => { if (!this.plot) { return; } diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js index cd81aff14701aa..7f54b7ec0d2a76 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ +import chrome from 'ui/chrome'; import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; -export function getChartOptions(axisOptions) { +export async function getChartOptions(axisOptions) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const timezone = $injector.get('config').get('dateFormat:tz'); const opts = { legend: { show: false }, xaxis: { color: CHART_LINE_COLOR, - timezone: 'browser', + timezone: timezone === 'Browser' ? 'browser' : 'utc', mode: 'time', // requires `time` flot plugin font: { color: CHART_TEXT_COLOR diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js index 8a5e067a8a93ec..c59a3d595b14fc 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js @@ -50,7 +50,7 @@ const columns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp), + render: timestamp => formatDateTimeLocal(timestamp, true), }, { field: 'level', @@ -80,7 +80,7 @@ const clusterColumns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp), + render: timestamp => formatDateTimeLocal(timestamp, true), }, { field: 'level', diff --git a/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js b/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js index f7caf9de0d1fe5..5af714f7b3ba26 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/details/__test__/get_metrics.test.js @@ -51,7 +51,10 @@ function getMockReq(metricsBuckets = []) { }, params: { clusterUuid: '1234xyz' - } + }, + getUiSettingsService: () => ({ + get: () => 'Browser' + }) }; } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js b/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js index 57c936f960212b..c5d2ee2032b018 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/details/get_metrics.js @@ -10,8 +10,9 @@ import Promise from 'bluebird'; import { checkParam } from '../error_missing_required'; import { getSeries } from './get_series'; import { calculateTimeseriesInterval } from '../calculate_timeseries_interval'; +import { getTimezone } from '../get_timezone'; -export function getMetrics(req, indexPattern, metricSet = [], filters = []) { +export async function getMetrics(req, indexPattern, metricSet = [], filters = []) { checkParam(indexPattern, 'indexPattern in details/getMetrics'); checkParam(metricSet, 'metricSet in details/getMetrics'); @@ -21,6 +22,7 @@ export function getMetrics(req, indexPattern, metricSet = [], filters = []) { const max = moment.utc(req.payload.timeRange.max).valueOf(); const minIntervalSeconds = config.get('xpack.monitoring.min_interval_seconds'); const bucketSize = calculateTimeseriesInterval(min, max, minIntervalSeconds); + const timezone = await getTimezone(req); return Promise.map(metricSet, metric => { // metric names match the literal metric name, but they can be supplied in groups or individually @@ -33,7 +35,7 @@ export function getMetrics(req, indexPattern, metricSet = [], filters = []) { } return Promise.map(metricNames, metricName => { - return getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize }); + return getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize, timezone }); }); }) .then(rows => { diff --git a/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js b/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js index 306e93273c157d..e66878f522ecb8 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/details/get_series.js @@ -14,6 +14,7 @@ import { NORMALIZED_DERIVATIVE_UNIT, CALCULATE_DURATION_UNTIL } from '../../../common/constants'; +import { formatUTCTimestampForTimezone } from '../format_timezone'; /** * Derivative metrics for the first two agg buckets are unusable. For the first bucket, there @@ -177,7 +178,7 @@ const formatBucketSize = bucketSizeInSeconds => { return formatTimestampToDuration(timestamp, CALCULATE_DURATION_UNTIL, now); }; -function handleSeries(metric, min, max, bucketSizeInSeconds, response) { +function handleSeries(metric, min, max, bucketSizeInSeconds, timezone, response) { const { derivative, calculation: customCalculation } = metric; const buckets = get(response, 'aggregations.check.buckets', []); const firstUsableBucketIndex = findFirstUsableBucketIndex(buckets, min); @@ -193,14 +194,17 @@ function handleSeries(metric, min, max, bucketSizeInSeconds, response) { data = buckets .slice(firstUsableBucketIndex, lastUsableBucketIndex + 1) // take only the buckets we know are usable .map(bucket => ([ - bucket.key, + formatUTCTimestampForTimezone(bucket.key, timezone), calculation(bucket, key, metric, bucketSizeInSeconds) ])); // map buckets to X/Y coords for Flot charting } return { bucket_size: formatBucketSize(bucketSizeInSeconds), - timeRange: { min, max }, + timeRange: { + min: formatUTCTimestampForTimezone(min, timezone), + max: formatUTCTimestampForTimezone(max, timezone), + }, metric: metric.serialize(), data }; @@ -217,7 +221,7 @@ function handleSeries(metric, min, max, bucketSizeInSeconds, response) { * @param {Array} filters Any filters that should be applied to the query. * @return {Promise} The object response containing the {@code timeRange}, {@code metric}, and {@code data}. */ -export async function getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize }) { +export async function getSeries(req, indexPattern, metricName, filters, { min, max, bucketSize, timezone }) { checkParam(indexPattern, 'indexPattern in details/getSeries'); const metric = metrics[metricName]; @@ -226,5 +230,5 @@ export async function getSeries(req, indexPattern, metricName, filters, { min, m } const response = await fetchSeries(req, indexPattern, metric, min, max, bucketSize, filters); - return handleSeries(metric, min, max, bucketSize, response); + return handleSeries(metric, min, max, bucketSize, timezone, response); } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js b/x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js new file mode 100644 index 00000000000000..334477ac1c3596 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/format_timezone.js @@ -0,0 +1,25 @@ +/* + * 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 moment from 'moment'; + + +/** + * This function is designed to offset a UTC timestamp based on the provided timezone + * For example, EST is UTC-4h so this function will subtract (4 * 60 * 60 * 1000)ms + * from the UTC timestamp. This allows us to allow users to view monitoring data + * in various timezones without needing to not store UTC dates. + * + * @param {*} utcTimestamp UTC timestamp + * @param {*} timezone The timezone to convert into + */ +export const formatUTCTimestampForTimezone = (utcTimestamp, timezone) => { + if (timezone === 'Browser') { + return utcTimestamp; + } + const offsetInMinutes = moment.tz(timezone).utcOffset(); + const offsetTimestamp = utcTimestamp + (offsetInMinutes * 1 * 60 * 1000); + return offsetTimestamp; +}; diff --git a/x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js b/x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js new file mode 100644 index 00000000000000..0da15bf6b28e17 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/get_timezone.js @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export async function getTimezone(req) { + return await req.getUiSettingsService().get('dateFormat:tz'); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js b/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js index 80d7f21fc45db2..0d45b8c6a1c4e7 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/logs/get_logs.js @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { get } from 'lodash'; import { checkParam } from '../error_missing_required'; import { createTimeFilter } from '../create_query'; import { detectReason } from './detect_reason'; +import { formatUTCTimestampForTimezone } from '../format_timezone'; +import { getTimezone } from '../get_timezone'; async function handleResponse(response, req, filebeatIndexPattern, opts) { const result = { @@ -15,15 +18,17 @@ async function handleResponse(response, req, filebeatIndexPattern, opts) { logs: [] }; + const timezone = await getTimezone(req); const hits = get(response, 'hits.hits', []); if (hits.length) { result.enabled = true; result.logs = hits.map(hit => { const source = hit._source; const type = get(source, 'event.dataset').split('.')[1]; + const utcTimestamp = moment(get(source, '@timestamp')).valueOf(); return { - timestamp: get(source, '@timestamp'), + timestamp: formatUTCTimestampForTimezone(utcTimestamp, timezone), component: get(source, 'elasticsearch.component'), node: get(source, 'elasticsearch.node.name'), index: get(source, 'elasticsearch.index.name'), diff --git a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json index abba0d12dbef5d..2e9fd359962db9 100644 --- a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json +++ b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/index_detail.json @@ -1,7 +1,7 @@ { "enabled": true, "logs": [{ - "timestamp": "2019-03-15T17:07:21.089Z", + "timestamp": 1552669641089, "component": "o.e.n.Node", "node": "Elastic-MBP.local", "index": ".monitoring-es", diff --git a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json index ef197266273ec3..c79fd672057510 100644 --- a/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json +++ b/x-pack/test/api_integration/apis/monitoring/logs/fixtures/node_detail.json @@ -1,21 +1,21 @@ { "enabled": true, "logs": [{ - "timestamp": "2019-03-15T17:19:07.365Z", + "timestamp": 1552670347365, "component": "o.e.d.x.m.r.a.RestMonitoringBulkAction", "node": "Elastic-MBP.local", "level": "WARN", "type": "deprecation", "message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead." }, { - "timestamp": "2019-03-15T17:18:57.366Z", + "timestamp": 1552670337366, "component": "o.e.d.x.m.r.a.RestMonitoringBulkAction", "node": "Elastic-MBP.local", "level": "WARN", "type": "deprecation", "message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead." }, { - "timestamp": "2019-03-15T17:18:47.400Z", + "timestamp": 1552670327400, "component": "o.e.c.m.MetaDataCreateIndexService", "node": "Elastic-MBP.local", "index": ".monitoring-beats-7-2019.03.15", @@ -23,14 +23,14 @@ "type": "server", "message": "creating index, cause [auto(bulk api)], templates [.monitoring-beats], shards [1]/[0], mappings [_doc]" }, { - "timestamp": "2019-03-15T17:18:47.387Z", + "timestamp": 1552670327387, "component": "o.e.d.x.m.r.a.RestMonitoringBulkAction", "node": "Elastic-MBP.local", "level": "WARN", "type": "deprecation", "message": "[POST /_xpack/monitoring/_bulk] is deprecated! Use [POST /_monitoring/bulk] instead." }, { - "timestamp": "2019-03-15T17:18:42.084Z", + "timestamp": 1552670322084, "component": "o.e.c.m.MetaDataMappingService", "node": "Elastic-MBP.local", "index": "filebeat-8.0.0-2019.03.15-000001", @@ -38,7 +38,7 @@ "type": "server", "message": "update_mapping [_doc]" }, { - "timestamp": "2019-03-15T17:18:41.811Z", + "timestamp": 1552670321811, "component": "o.e.c.m.MetaDataMappingService", "node": "Elastic-MBP.local", "index": "filebeat-8.0.0-2019.03.15-000001", @@ -46,7 +46,7 @@ "type": "server", "message": "update_mapping [_doc]" }, { - "timestamp": "2019-03-15T17:18:41.447Z", + "timestamp": 1552670321447, "component": "o.e.c.m.MetaDataCreateIndexService", "node": "Elastic-MBP.local", "index": "filebeat-8.0.0-2019.03.15-000001", @@ -54,21 +54,21 @@ "type": "server", "message": "creating index, cause [api], templates [filebeat-8.0.0], shards [1]/[1], mappings [_doc]" }, { - "timestamp": "2019-03-15T17:18:41.385Z", + "timestamp": 1552670321385, "component": "o.e.c.m.MetaDataIndexTemplateService", "node": "Elastic-MBP.local", "level": "INFO", "type": "server", "message": "adding template [filebeat-8.0.0] for index patterns [filebeat-8.0.0-*]" }, { - "timestamp": "2019-03-15T17:18:41.185Z", + "timestamp": 1552670321185, "component": "o.e.x.i.a.TransportPutLifecycleAction", "node": "Elastic-MBP.local", "level": "INFO", "type": "server", "message": "adding index lifecycle policy [filebeat-8.0.0]" }, { - "timestamp": "2019-03-15T17:18:36.137Z", + "timestamp": 1552670316137, "component": "o.e.c.r.a.AllocationService", "node": "Elastic-MBP.local", "level": "INFO", From 6f09ecc0d9f189a805ca2e875916167b748ea449 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Wed, 2 Oct 2019 11:58:22 -0500 Subject: [PATCH 45/53] Upgrade EUI to 14.4.0 (#46949) * eui to 14.4.0 * euicard ts updates * snaps --- package.json | 2 +- .../__snapshots__/dashboard_listing.test.js.snap | 6 ------ .../embeddables/contact_card/contact_card.tsx | 15 +++++++-------- .../__snapshots__/data_view.test.tsx.snap | 1 - .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- .../kbn_tp_visualize_embedding/package.json | 2 +- typings/@elastic/eui/index.d.ts | 1 - .../__snapshots__/NoServicesMessage.test.tsx.snap | 4 ---- .../components/element_card/element_card.tsx | 8 ++------ .../__snapshots__/upgrade_failure.test.js.snap | 4 ---- .../__snapshots__/transform_list.test.tsx.snap | 1 - .../explorer_no_influencers_found.test.js.snap | 1 - .../explorer_no_jobs_found.test.js.snap | 1 - .../explorer_no_results_found.test.js.snap | 1 - .../__snapshots__/roles_grid_page.test.tsx.snap | 1 - .../index_patterns_missing_prompt.test.tsx.snap | 1 - .../__snapshots__/checkup_tab.test.tsx.snap | 1 - .../__snapshots__/data_missing.test.tsx.snap | 1 - .../__snapshots__/empty_state.test.tsx.snap | 4 ---- x-pack/package.json | 2 +- x-pack/typings/@elastic/eui/index.d.ts | 1 - yarn.lock | 8 ++++---- 25 files changed, 20 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 6f54c8683410a7..be43e242ce569c 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@babel/register": "^7.5.5", "@elastic/charts": "^12.0.2", "@elastic/datemath": "5.0.2", - "@elastic/eui": "14.3.0", + "@elastic/eui": "14.4.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 9b819443808c93..89b8e2ac83ec10 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -13,7 +13,6 @@ exports[`after fetch hideWriteControls 1`] = ` noItemsFragment={
@@ -106,7 +105,6 @@ exports[`after fetch initialFilter 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -199,7 +197,6 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -292,7 +289,6 @@ exports[`after fetch renders table rows 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -385,7 +381,6 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -478,7 +473,6 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx index a83364f22021a7..51640749bc2b41 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card.tsx @@ -17,13 +17,7 @@ * under the License. */ import React from 'react'; -import { - // @ts-ignore - EuiCard, - EuiFlexItem, - EuiFlexGroup, - EuiFormRow, -} from '@elastic/eui'; +import { EuiCard, EuiFlexItem, EuiFlexGroup, EuiFormRow } from '@elastic/eui'; import { Subscription } from 'rxjs'; import { EuiButton } from '@elastic/eui'; @@ -96,7 +90,12 @@ export class ContactCardEmbeddableComponent extends React.Component + ); } } diff --git a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap index 37c77c97fe39de..adea7831d6b805 100644 --- a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap @@ -250,7 +250,6 @@ exports[`Inspector Data View component should render empty state 1`] = `

} - iconColor="subdued" title={

; export const EuiCodeEditor: React.SFC; export const Query: any; - export const EuiCard: any; export interface EuiTableCriteria { page: { index: number; size: number }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap index de8e109e62324a..209b88f73b9e2c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap @@ -33,7 +33,6 @@ exports[`NoServicesMessage status: pending and historicalDataFound: false 1`] =

} - iconColor="subdued" title={
Looks like you don't have any APM services installed. Let's add some! @@ -45,7 +44,6 @@ exports[`NoServicesMessage status: pending and historicalDataFound: false 1`] = exports[`NoServicesMessage status: pending and historicalDataFound: true 1`] = ` No services found @@ -80,7 +78,6 @@ exports[`NoServicesMessage status: success and historicalDataFound: false 1`] =

} - iconColor="subdued" title={
Looks like you don't have any APM services installed. Let's add some! @@ -92,7 +89,6 @@ exports[`NoServicesMessage status: success and historicalDataFound: false 1`] = exports[`NoServicesMessage status: success and historicalDataFound: true 1`] = ` No services found diff --git a/x-pack/legacy/plugins/canvas/public/components/element_card/element_card.tsx b/x-pack/legacy/plugins/canvas/public/components/element_card/element_card.tsx index 9262a67cf393cc..819282d5881292 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_card/element_card.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/element_card/element_card.tsx @@ -5,11 +5,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { - // @ts-ignore unconverted EUI component - EuiCard, - EuiIcon, -} from '@elastic/eui'; +import { EuiCard, EuiIcon } from '@elastic/eui'; import { TagList } from '../tag_list/'; export interface Props { @@ -45,7 +41,7 @@ export const ElementCard = ({ title, description, image, tags = [], onClick, ... description={description} footer={} image={image} - icon={image ? null : } + icon={image ? undefined : } onClick={onClick} {...rest} /> diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap b/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap index b31bae263fa8f4..4d752888d3df4f 100644 --- a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap +++ b/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap @@ -146,7 +146,6 @@ exports[`UpgradeFailure component passes expected text for new pipeline 1`] = ` Before you can add a pipeline, we need to upgrade your configuration.

} - iconColor="subdued" title={ } - iconColor="subdued" title={ } - iconColor="subdued" title={ } - iconColor="subdued" title={ Minimal initializ ] } data-test-subj="mlNoDataFrameTransformsFound" - iconColor="subdued" title={

No data frame transforms found diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap index f6e083e31984ac..77821663783cfa 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap @@ -2,7 +2,6 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` } data-test-subj="mlNoJobsFound" - iconColor="subdued" iconType="alert" title={

diff --git a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap b/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap index a7eb6d8db8a59a..dc7e567380fdf9 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap @@ -13,7 +13,6 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = `

} - iconColor="subdued" iconType="iInCircle" title={

diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap index 5e3625a1f0fc4b..048fa74a72818f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap @@ -31,7 +31,6 @@ exports[` renders permission denied if required 1`] = ` />

} - iconColor="subdued" iconType="securityApp" title={

diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index 937ba229b88835..f482a864bed6dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -56,7 +56,6 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = `

} - iconColor="subdued" iconType="gisApp" title={

diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index 5f826ba30262c0..6f92d475ae6c50 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -457,7 +457,6 @@ exports[`CheckupTab render without deprecations 1`] = `

} - iconColor="subdued" iconType="faceHappy" title={

diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap index 30b2e7204e4047..b17d28f19335b5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/empty_state/__tests__/__snapshots__/data_missing.test.tsx.snap @@ -36,7 +36,6 @@ exports[`DataMissing component renders basePath and headingMessage 1`] = ` />

} - iconColor="subdued" iconType="uptimeApp" title={

} - iconColor="subdued" iconType="uptimeApp" title={ } - iconColor="subdued" >

} - iconColor="subdued" iconType="uptimeApp" title={ } - iconColor="subdued" >
; export const EuiCodeEditor: React.SFC; export const Query: any; - export const EuiCard: any; } declare module '@elastic/eui/lib/services' { diff --git a/yarn.lock b/yarn.lock index 824618215a8be7..9d73d21a044bf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1145,10 +1145,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@14.3.0": - version "14.3.0" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-14.3.0.tgz#256e1af8f6b15717904f8959742a23b3495ff0bb" - integrity sha512-gAbPNezBmndInYqqw6EvRYLn2VMYQgYuPQYA5UZ7TyHzwvoBiMpUw5nFzYhS2A/Xcmq/ON5Mu8RY3LGRAVBOvQ== +"@elastic/eui@14.4.0": + version "14.4.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-14.4.0.tgz#ac09a476798dcdb1005616cccc149eda23ea2a90" + integrity sha512-dR7lYwUaIRXZjlUrJBq8GcGLPh6QfM3waQxUFI8lOnMVayJe3OOMNADCn8Oty6wNYIOrBWzZbW6w4bzInWF6oA== dependencies: "@types/lodash" "^4.14.116" "@types/numeral" "^0.0.25" From e6ace31c0e719067b689b9c83e850fc68210c015 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Wed, 2 Oct 2019 13:21:39 -0400 Subject: [PATCH 46/53] [Lens] Make horizontal bar chart a first-class chart (#47062) --- .../xy_visualization.test.ts.snap | 3 -- .../xy_visualization_plugin/state_helpers.ts | 26 +++++++++ .../xy_visualization_plugin/to_expression.ts | 1 - .../public/xy_visualization_plugin/types.ts | 27 ++++++++-- .../xy_config_panel.test.tsx | 51 +++++++----------- .../xy_config_panel.tsx | 51 ++++-------------- .../xy_expression.test.tsx | 6 +-- .../xy_visualization_plugin/xy_expression.tsx | 20 +++---- .../xy_suggestions.test.ts | 13 +---- .../xy_visualization_plugin/xy_suggestions.ts | 30 +++-------- .../xy_visualization.test.ts | 49 +++++++++++++++-- .../xy_visualization.tsx | 17 ++++-- .../es_archives/lens/reporting/data.json.gz | Bin 4356 -> 4359 bytes 13 files changed, 161 insertions(+), 133 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index 12902f548e45b7..76af8328673add 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -5,9 +5,6 @@ Object { "chain": Array [ Object { "arguments": Object { - "isHorizontal": Array [ - false, - ], "layers": Array [ Object { "chain": Array [ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts new file mode 100644 index 00000000000000..eb7fd688bab5a4 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts @@ -0,0 +1,26 @@ +/* + * 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 { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { SeriesType, visualizationTypes } from './types'; + +export function isHorizontalSeries(seriesType: SeriesType) { + return seriesType === 'bar_horizontal' || seriesType === 'bar_horizontal_stacked'; +} + +export function isHorizontalChart(layers: Array<{ seriesType: SeriesType }>) { + return layers.every(l => isHorizontalSeries(l.seriesType)); +} + +export function getIconForSeries(type: SeriesType): EuiIconType { + const definition = visualizationTypes.find(t => t.id === type); + + if (!definition) { + throw new Error(`Unknown series type ${type}`); + } + + return (definition.icon as EuiIconType) || 'empty'; +} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index ff5f7eb08f2db0..f0e932d14f281b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -131,7 +131,6 @@ export const buildExpression = ( arguments: { xTitle: [xTitle], yTitle: [yTitle], - isHorizontal: [state.isHorizontal], legend: [ { type: 'expression', diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index 742cc36be4ea66..28f72f60c3a2de 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -175,7 +175,14 @@ export const layerConfig: ExpressionFunction< }, }; -export type SeriesType = 'bar' | 'line' | 'area' | 'bar_stacked' | 'area_stacked'; +export type SeriesType = + | 'bar' + | 'bar_horizontal' + | 'line' + | 'area' + | 'bar_stacked' + | 'bar_horizontal_stacked' + | 'area_stacked'; export interface LayerConfig { hide?: boolean; @@ -199,7 +206,6 @@ export interface XYArgs { yTitle: string; legend: LegendConfig; layers: LayerArgs[]; - isHorizontal: boolean; } // Persisted parts of the state @@ -207,7 +213,6 @@ export interface XYState { preferredSeriesType: SeriesType; legend: LegendConfig; layers: LayerConfig[]; - isHorizontal: boolean; } export type State = XYState; @@ -221,13 +226,27 @@ export const visualizationTypes: VisualizationType[] = [ defaultMessage: 'Bar', }), }, + { + id: 'bar_horizontal', + icon: 'visBarHorizontal', + label: i18n.translate('xpack.lens.xyVisualization.barHorizontalLabel', { + defaultMessage: 'Horizontal Bar', + }), + }, { id: 'bar_stacked', icon: 'visBarVertical', - label: i18n.translate('xpack.lens.xyVisualization.stackedBarLabel', { + label: i18n.translate('xpack.lens.xyVisualization.stackedBar', { defaultMessage: 'Stacked Bar', }), }, + { + id: 'bar_horizontal_stacked', + icon: 'visBarHorizontal', + label: i18n.translate('xpack.lens.xyVisualization.stackedBarHorizontalLabel', { + defaultMessage: 'Stacked Horizontal Bar', + }), + }, { id: 'line', icon: 'visLine', diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index ad08b8949f3b91..5cdf1031a22b04 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FormEvent } from 'react'; +import React from 'react'; import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; @@ -15,7 +15,6 @@ import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; import { generateId } from '../id_generator'; import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; -import { act } from 'react-test-renderer'; jest.mock('../id_generator'); @@ -28,7 +27,6 @@ describe('XYConfigPanel', () => { return { legend: { isVisible: true, position: Position.Right }, preferredSeriesType: 'bar', - isHorizontal: false, layers: [ { seriesType: 'bar', @@ -64,50 +62,48 @@ describe('XYConfigPanel', () => { }; }); - test.skip('toggles axis position when going from horizontal bar to any other type', () => {}); test.skip('allows toggling of legend visibility', () => {}); test.skip('allows changing legend position', () => {}); test.skip('allows toggling the y axis gridlines', () => {}); test.skip('allows toggling the x axis gridlines', () => {}); - test('puts the horizontal toggle in a popover', () => { + test('enables stacked chart types even when there is no split series', () => { const state = testState(); - const setState = jest.fn(); const component = mount( ); - component - .find(`[data-test-subj="lnsXY_chart_settings"]`) + openComponentPopover(component, 'first'); + + const options = component + .find('[data-test-subj="lnsXY_seriesType"]') .first() - .simulate('click'); + .prop('options') as EuiButtonGroupProps['options']; - act(() => { - component - .find('[data-test-subj="lnsXY_chart_horizontal"]') - .first() - .prop('onChange')!({} as FormEvent); - }); + expect(options!.map(({ id }) => id)).toEqual([ + 'bar', + 'bar_stacked', + 'line', + 'area', + 'area_stacked', + ]); - expect(setState).toHaveBeenCalledWith({ - ...state, - isHorizontal: true, - }); + expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); - test('enables stacked chart types even when there is no split series', () => { + test('shows only horizontal bar options when in horizontal mode', () => { const state = testState(); const component = mount( ); @@ -118,14 +114,7 @@ describe('XYConfigPanel', () => { .first() .prop('options') as EuiButtonGroupProps['options']; - expect(options!.map(({ id }) => id)).toEqual([ - 'bar', - 'bar_stacked', - 'line', - 'area', - 'area_stacked', - ]); - + expect(options!.map(({ id }) => id)).toEqual(['bar_horizontal', 'bar_horizontal_stacked']); expect(options!.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 7170a41a168800..e268c099ddc24d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -17,7 +17,6 @@ import { EuiPanel, EuiButtonIcon, EuiPopover, - EuiSwitch, EuiSpacer, EuiButtonEmpty, EuiPopoverFooter, @@ -27,6 +26,7 @@ import { VisualizationProps, OperationMetadata } from '../types'; import { NativeRenderer } from '../native_renderer'; import { MultiColumnEditor } from '../multi_column_editor'; import { generateId } from '../id_generator'; +import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; @@ -55,10 +55,12 @@ function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { function LayerSettings({ layer, + horizontalOnly, setSeriesType, removeLayer, }: { layer: LayerConfig; + horizontalOnly: boolean; setSeriesType: (seriesType: SeriesType) => void; removeLayer: () => void; }) { @@ -96,10 +98,12 @@ function LayerSettings({ name="chartType" className="eui-displayInlineBlock" data-test-subj="lnsXY_seriesType" - options={visualizationTypes.map(t => ({ - ...t, - iconType: t.icon || 'empty', - }))} + options={visualizationTypes + .filter(t => isHorizontalSeries(t.id as SeriesType) === horizontalOnly) + .map(t => ({ + ...t, + iconType: t.icon || 'empty', + }))} idSelected={layer.seriesType} onChange={seriesType => setSeriesType(seriesType as SeriesType)} isIconOnly @@ -124,44 +128,10 @@ function LayerSettings({ export function XYConfigPanel(props: VisualizationProps) { const { state, setState, frame } = props; - const [isChartOptionsOpen, setIsChartOptionsOpen] = useState(false); + const horizontalOnly = isHorizontalChart(state.layers); return ( - setIsChartOptionsOpen(false)} - button={ - setIsChartOptionsOpen(!isChartOptionsOpen)} - aria-label={i18n.translate('xpack.lens.xyChart.chartSettings', { - defaultMessage: 'Chart Settings', - })} - title={i18n.translate('xpack.lens.xyChart.chartSettings', { - defaultMessage: 'Chart Settings', - })} - /> - } - > - { - setState({ - ...state, - isHorizontal: !state.isHorizontal, - }); - }} - data-test-subj="lnsXY_chart_horizontal" - /> - - {state.layers.map((layer, index) => ( ) { setState(updateLayer(state, { ...layer, seriesType }, index)) } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index 0ac286c7bb83c7..8770ee5b5e1c9b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -35,7 +35,6 @@ function sampleArgs() { const args: XYArgs = { xTitle: '', yTitle: '', - isHorizontal: false, legend: { isVisible: false, position: Position.Top, @@ -161,7 +160,7 @@ describe('xy_expression', () => { const component = shallow( @@ -208,8 +207,7 @@ describe('xy_expression', () => { data={data} args={{ ...args, - isHorizontal: true, - layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }], + layers: [{ ...args.layers[0], seriesType: 'bar_horizontal_stacked' }], }} formatFactory={getFormatSpy} timeZone="UTC" diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index e559cdd514bc66..43452ff4327677 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -27,6 +27,7 @@ import { IInterpreterRenderFunction } from '../../../../../../src/legacy/core_pl import { LensMultiTable } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; +import { isHorizontalChart } from './state_helpers'; export interface XYChartProps { data: LensMultiTable; @@ -75,10 +76,6 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs help: 'Layers of visual series', multi: true, }, - isHorizontal: { - types: ['boolean'], - help: 'Render horizontally', - }, }, context: { types: ['lens_multitable'], @@ -140,7 +137,7 @@ export function XYChartReportable(props: XYChartRenderProps) { } export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderProps) { - const { legend, layers, isHorizontal } = args; + const { legend, layers } = args; if (Object.values(data.tables).every(table => table.rows.length === 0)) { const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar'; @@ -176,18 +173,20 @@ export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderPr } } + const shouldRotate = isHorizontalChart(layers); + return ( - ) : seriesType === 'bar' || seriesType === 'bar_stacked' ? ( + ) : seriesType === 'bar' || + seriesType === 'bar_stacked' || + seriesType === 'bar_horizontal' || + seriesType === 'bar_horizontal_stacked' ? ( ) : ( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index ed44c741233161..a205fe433106aa 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -199,7 +199,6 @@ describe('xy_suggestions', () => { changeType: 'reduced', }, state: { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ @@ -235,7 +234,6 @@ describe('xy_suggestions', () => { test('only makes a seriesType suggestion for unchanged table without split', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ @@ -270,7 +268,6 @@ describe('xy_suggestions', () => { test('suggests seriesType and stacking when there is a split', () => { const currentState: XYState = { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ @@ -311,7 +308,6 @@ describe('xy_suggestions', () => { test('suggests a flipped chart for unchanged table and existing bar chart on ordinal x axis', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ @@ -335,16 +331,13 @@ describe('xy_suggestions', () => { }); expect(rest).toHaveLength(0); - expect(suggestion.state).toEqual({ - ...currentState, - isHorizontal: true, - }); + expect(suggestion.state.preferredSeriesType).toEqual('bar_horizontal'); + expect(suggestion.state.layers.every(l => l.seriesType === 'bar_horizontal')).toBeTruthy(); expect(suggestion.title).toEqual('Flip'); }); test('suggests stacking for unchanged table that has a split', () => { const currentState: XYState = { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ @@ -379,7 +372,6 @@ describe('xy_suggestions', () => { test('keeps column to dimension mappings on extended tables', () => { const currentState: XYState = { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ @@ -418,7 +410,6 @@ describe('xy_suggestions', () => { test('overwrites column to dimension mappings if a date dimension is added', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { - isHorizontal: false, legend: { isVisible: true, position: 'bottom' }, preferredSeriesType: 'bar', layers: [ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index 2f28e20ebd274a..7c7e9caddd31b7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { partition } from 'lodash'; import { Position } from '@elastic/charts'; -import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { SuggestionRequest, VisualizationSuggestion, @@ -17,6 +16,7 @@ import { } from '../types'; import { State, SeriesType, XYState } from './types'; import { generateId } from '../id_generator'; +import { getIconForSeries } from './state_helpers'; const columnSortOrder = { date: 0, @@ -26,21 +26,6 @@ const columnSortOrder = { number: 4, }; -function getIconForSeries(type: SeriesType): EuiIconType { - switch (type) { - case 'area': - case 'area_stacked': - return 'visArea'; - case 'bar': - case 'bar_stacked': - return 'visBarVertical'; - case 'line': - return 'visLine'; - default: - throw new Error('unknown series type'); - } -} - /** * Generate suggestions for the xy chart. * @@ -163,10 +148,8 @@ function getSuggestionsForLayer( ): VisualizationSuggestion | Array> { const title = getSuggestionTitle(yValues, xValue, tableLabel); const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue, changeType); - const isHorizontal = currentState ? currentState.isHorizontal : false; const options = { - isHorizontal, currentState, seriesType, layerId, @@ -186,14 +169,18 @@ function getSuggestionsForLayer( const sameStateSuggestions: Array> = []; // if current state is using the same data, suggest same chart with different presentational configuration - if (seriesType !== 'line' && xValue.operation.scale === 'ordinal') { // flip between horizontal/vertical for ordinal scales sameStateSuggestions.push( buildSuggestion({ ...options, title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }), - isHorizontal: !options.isHorizontal, + seriesType: + seriesType === 'bar_horizontal' + ? 'bar' + : seriesType === 'bar_horizontal_stacked' + ? 'bar_stacked' + : 'bar_horizontal', }) ); } else { @@ -328,7 +315,6 @@ function getSuggestionTitle( } function buildSuggestion({ - isHorizontal, currentState, seriesType, layerId, @@ -339,7 +325,6 @@ function buildSuggestion({ xValue, }: { currentState: XYState | undefined; - isHorizontal: boolean; seriesType: SeriesType; title: string; yValues: TableSuggestionColumn[]; @@ -358,7 +343,6 @@ function buildSuggestion({ }; const state: State = { - isHorizontal, legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, preferredSeriesType: seriesType, layers: [ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 8bc7b0c9116f70..5cd0791ae3da9e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -7,7 +7,7 @@ import { xyVisualization } from './xy_visualization'; import { Position } from '@elastic/charts'; import { Operation } from '../types'; -import { State } from './types'; +import { State, SeriesType } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; import { generateId } from '../id_generator'; import { Ast } from '@kbn/interpreter/target/common'; @@ -16,7 +16,6 @@ jest.mock('../id_generator'); function exampleState(): State { return { - isHorizontal: false, legend: { position: Position.Bottom, isVisible: true }, preferredSeriesType: 'bar', layers: [ @@ -32,6 +31,51 @@ function exampleState(): State { } describe('xy_visualization', () => { + describe('getDescription', () => { + function mixedState(...types: SeriesType[]) { + const state = exampleState(); + return { + ...state, + layers: types.map((t, i) => ({ + ...state.layers[0], + layerId: `layer_${i}`, + seriesType: t, + })), + }; + } + + it('should show mixed xy chart when multilple series types', () => { + const desc = xyVisualization.getDescription(mixedState('bar', 'line')); + + expect(desc.label).toEqual('Mixed XY Chart'); + }); + + it('should show mixed horizontal bar chart when multiple horizontal bar types', () => { + const desc = xyVisualization.getDescription( + mixedState('bar_horizontal', 'bar_horizontal_stacked') + ); + + expect(desc.label).toEqual('Mixed Horizontal Bar Chart'); + }); + + it('should show bar chart when bar only', () => { + const desc = xyVisualization.getDescription(mixedState('bar_horizontal', 'bar_horizontal')); + + expect(desc.label).toEqual('Horizontal Bar Chart'); + }); + + it('should show the chart description if not mixed', () => { + expect(xyVisualization.getDescription(mixedState('area')).label).toEqual('Area Chart'); + expect(xyVisualization.getDescription(mixedState('line')).label).toEqual('Line Chart'); + expect(xyVisualization.getDescription(mixedState('area_stacked')).label).toEqual( + 'Stacked Area Chart' + ); + expect(xyVisualization.getDescription(mixedState('bar_horizontal_stacked')).label).toEqual( + 'Stacked Horizontal Bar Chart' + ); + }); + }); + describe('#initialize', () => { it('loads default state', () => { (generateId as jest.Mock) @@ -48,7 +92,6 @@ describe('xy_visualization', () => { expect(initialState).toMatchInlineSnapshot(` Object { - "isHorizontal": false, "layers": Array [ Object { "accessors": Array [ diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 69cb93bb1903d0..29c5e5d5e42975 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -16,6 +16,7 @@ import { Visualization } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; import { generateId } from '../id_generator'; +import { isHorizontalChart } from './state_helpers'; const defaultIcon = 'visBarVertical'; const defaultSeriesType = 'bar_stacked'; @@ -25,7 +26,7 @@ function getDescription(state?: State) { return { icon: defaultIcon, label: i18n.translate('xpack.lens.xyVisualization.xyLabel', { - defaultMessage: 'XY Chart', + defaultMessage: 'XY', }), }; } @@ -42,8 +43,12 @@ function getDescription(state?: State) { label: seriesTypes.length === 1 ? visualizationType.label + : isHorizontalChart(state.layers) + ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { + defaultMessage: 'Mixed Horizontal Bar', + }) : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { - defaultMessage: 'Mixed XY Chart', + defaultMessage: 'Mixed XY', }), }; } @@ -55,9 +60,14 @@ export const xyVisualization: Visualization = { getDescription(state) { const { icon, label } = getDescription(state); + const chartLabel = i18n.translate('xpack.lens.xyVisualization.chartLabel', { + defaultMessage: '{label} Chart', + values: { label }, + }); + return { icon: icon || defaultIcon, - label, + label: chartLabel, }; }, @@ -75,7 +85,6 @@ export const xyVisualization: Visualization = { return ( state || { title: 'Empty XY Chart', - isHorizontal: false, legend: { isVisible: true, position: Position.Right }, preferredSeriesType: defaultSeriesType, layers: [ diff --git a/x-pack/test/functional/es_archives/lens/reporting/data.json.gz b/x-pack/test/functional/es_archives/lens/reporting/data.json.gz index b59717330488afadc06fd319e810fdcfe6363a60..93ceaf3d8f6f5daa843f9fd8fb1e43a0eff23805 100644 GIT binary patch literal 4359 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 XeTrPHwRcO{Z3JU_-i12~5AS~fbnSYJ delta 4260 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 G0ssJeX)LM$ From f8810d12ac100b88fdf1aa1c49c88192d4ff02d0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 2 Oct 2019 11:46:55 -0600 Subject: [PATCH 47/53] [SIEM] Start of deprecated lifecycle refactor (#46293) --- .../timeline/data_providers.spec.ts | 6 +- .../timeline/flyout_button.spec.ts | 2 +- .../autocomplete_field/suggestion_item.tsx | 13 +- .../drag_drop_context_wrapper.tsx | 60 ++- .../drag_and_drop/draggable_wrapper.tsx | 42 +- .../components/edit_data_provider/index.tsx | 196 ++++---- .../components/embeddables/embedded_map.tsx | 2 +- .../event_details/stateful_event_details.tsx | 37 +- .../events_viewer/events_viewer.test.tsx | 11 +- .../components/events_viewer/index.test.tsx | 10 + .../public/components/events_viewer/index.tsx | 2 +- .../fields_browser/field_browser.tsx | 163 ++++--- .../components/fields_browser/index.tsx | 251 +++++------ .../public/components/flyout/pane/index.tsx | 58 ++- .../public/components/help_menu/help_menu.tsx | 52 ++- .../components/lazy_accordion/index.tsx | 77 ++-- .../__snapshots__/index.test.tsx.snap | 104 ----- .../components/load_more_table/index.mock.tsx | 118 ----- .../components/load_more_table/index.test.tsx | 360 --------------- .../components/load_more_table/index.tsx | 320 ------------- .../load_more_table/translations.ts | 23 - .../permissions/ml_capabilities_provider.tsx | 2 +- .../get_anomalies_host_table_columns.test.tsx | 2 +- .../get_anomalies_host_table_columns.tsx | 2 +- ...t_anomalies_network_table_columns.test.tsx | 2 +- .../get_anomalies_network_table_columns.tsx | 2 +- .../components/navigation/index.test.tsx | 4 +- .../public/components/navigation/index.tsx | 124 ++--- .../navigation/tab_navigation/index.test.tsx | 10 +- .../navigation/tab_navigation/index.tsx | 103 ++--- .../siem/public/components/notes/index.tsx | 28 +- .../components/notes/note_cards/index.tsx | 55 +-- .../delete_timeline_modal.test.tsx | 22 +- .../delete_timeline_modal.tsx | 6 +- .../delete_timeline_modal/index.test.tsx | 30 -- .../delete_timeline_modal/index.tsx | 53 +-- .../components/open_timeline/index.test.tsx | 190 ++++---- .../public/components/open_timeline/index.tsx | 424 ++++++++---------- .../open_timeline_modal/index.test.tsx | 23 +- .../open_timeline_modal/index.tsx | 124 ++--- .../components/page/add_to_kql/index.tsx | 28 +- .../page/hosts/hosts_table/index.tsx | 124 +++-- .../page/network/domains_table/columns.tsx | 2 +- .../network/network_dns_table/columns.tsx | 2 +- .../network_top_n_flow_table/columns.tsx | 2 +- .../network_top_n_flow_table/index.tsx | 83 ++-- .../page/network/tls_table/columns.tsx | 2 +- .../page/network/users_table/columns.tsx | 2 +- .../components/paginated_table/index.tsx | 12 +- .../public/components/resize_handle/index.tsx | 147 +++--- .../components/super_date_picker/index.tsx | 264 +++++------ .../body/column_headers/header/index.tsx | 77 ++-- .../body/data_driven_columns/index.tsx | 10 +- .../timeline/body/events/stateful_event.tsx | 192 ++++---- .../timeline/body/stateful_body.tsx | 138 +++--- .../data_providers/provider_item_badge.tsx | 92 ++-- .../components/timeline/footer/index.test.tsx | 60 ++- .../components/timeline/footer/index.tsx | 190 +++----- .../timeline/footer/last_updated.tsx | 71 ++- .../siem/public/components/timeline/index.tsx | 276 ++++++------ .../components/timeline/properties/index.tsx | 150 +++---- .../timeline/search_or_filter/index.tsx | 31 +- .../components/url_state/use_url_state.tsx | 2 +- .../components/with_hover_actions/index.tsx | 40 +- .../siem/public/containers/hosts/filter.tsx | 115 +++-- .../containers/hosts/first_last_seen/index.ts | 6 +- .../containers/kuery_autocompletion/index.tsx | 117 +++-- .../siem/public/containers/network/filter.tsx | 124 +++-- .../siem/public/containers/source/index.tsx | 81 ++-- .../public/containers/timeline/all/index.tsx | 90 ++-- .../containers/timeline/details/index.tsx | 22 +- .../public/pages/timelines/timelines_page.tsx | 34 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 74 files changed, 2091 insertions(+), 3616 deletions(-) delete mode 100644 x-pack/legacy/plugins/siem/public/components/load_more_table/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/siem/public/components/load_more_table/index.mock.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/load_more_table/index.test.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/load_more_table/index.tsx delete mode 100644 x-pack/legacy/plugins/siem/public/components/load_more_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 7c9a0edebe53e9..236d5a53481b7f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -61,7 +61,7 @@ describe('timeline data providers', () => { cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', 'background', - 'rgba(125, 226, 209, 0.1) none repeat scroll 0% 0% / auto padding-box border-box' + 'rgba(1, 125, 115, 0.1) none repeat scroll 0% 0% / auto padding-box border-box' ); }); @@ -81,7 +81,7 @@ describe('timeline data providers', () => { cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).should( 'have.css', 'background', - 'rgba(125, 226, 209, 0.2) none repeat scroll 0% 0% / auto padding-box border-box' + 'rgba(1, 125, 115, 0.2) none repeat scroll 0% 0% / auto padding-box border-box' ); }); @@ -101,7 +101,7 @@ describe('timeline data providers', () => { cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', 'border', - '3.1875px dashed rgb(125, 226, 209)' + '3.1875px dashed rgb(1, 125, 115)' ); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts index 811c529b8bec59..c1c35e497d0815 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/flyout_button.spec.ts @@ -41,7 +41,7 @@ describe('timeline flyout button', () => { cy.get(TIMELINE_NOT_READY_TO_DROP_BUTTON).should( 'have.css', 'background', - 'rgba(125, 226, 209, 0.1) none repeat scroll 0% 0% / auto padding-box border-box' + 'rgba(1, 125, 115, 0.1) none repeat scroll 0% 0% / auto padding-box border-box' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx index 997a19b0e8a2ec..aaf7be2f7f5a6d 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx @@ -18,13 +18,8 @@ interface SuggestionItemProps { suggestion: AutocompleteSuggestion; } -export class SuggestionItem extends React.PureComponent { - public static defaultProps: Partial = { - isSelected: false, - }; - - public render() { - const { isSelected, onClick, onMouseEnter, suggestion } = this.props; +export const SuggestionItem = React.memo( + ({ isSelected = false, onClick, onMouseEnter, suggestion }) => { return ( { ); } -} +); + +SuggestionItem.displayName = 'SuggestionItem'; const SuggestionItemContainer = euiStyled.div<{ isSelected?: boolean; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index aab83ec7908fe0..11b604571378b1 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -6,7 +6,7 @@ import { defaultTo, noop } from 'lodash/fp'; import * as React from 'react'; -import { DragDropContext, DropResult, ResponderProvided, DragStart } from 'react-beautiful-dnd'; +import { DragDropContext, DropResult, DragStart } from 'react-beautiful-dnd'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; @@ -57,43 +57,39 @@ const onDragEndHandler = ({ /** * DragDropContextWrapperComponent handles all drag end events */ -export class DragDropContextWrapperComponent extends React.Component { - public shouldComponentUpdate = ({ children, dataProviders }: Props) => - children === this.props.children && dataProviders !== this.props.dataProviders // prevent re-renders when data providers are added or removed, but all other props are the same - ? false - : true; - - public render() { - const { children } = this.props; - +export const DragDropContextWrapperComponent = React.memo( + ({ browserFields, children, dataProviders, dispatch }) => { + function onDragEnd(result: DropResult) { + enableScrolling(); + + if (dataProviders != null) { + onDragEndHandler({ + browserFields, + result, + dataProviders, + dispatch, + }); + } + + if (!draggableIsField(result)) { + document.body.classList.remove(IS_DRAGGING_CLASS_NAME); + } + } return ( - + {children} ); + }, + (prevProps, nextProps) => { + return ( + prevProps.children === nextProps.children && + prevProps.dataProviders === nextProps.dataProviders + ); // prevent re-renders when data providers are added or removed, but all other props are the same } +); - private onDragEnd: (result: DropResult, provided: ResponderProvided) => void = ( - result: DropResult - ) => { - const { browserFields, dataProviders, dispatch } = this.props; - - enableScrolling(); - - if (dataProviders != null) { - onDragEndHandler({ - browserFields, - result, - dataProviders, - dispatch, - }); - } - - if (!draggableIsField(result)) { - document.body.classList.remove(IS_DRAGGING_CLASS_NAME); - } - }; -} +DragDropContextWrapperComponent.displayName = 'DragDropContextWrapperComponent'; const emptyDataProviders: dragAndDropModel.IdToDataProvider = {}; // stable reference diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index 0755ef0e5592cf..8a12a5035fc3a8 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import * as React from 'react'; +import React, { useEffect } from 'react'; import { Draggable, DraggableProvided, @@ -161,28 +161,15 @@ type Props = OwnProps & DispatchProps; * Wraps a draggable component to handle registration / unregistration of the * data provider associated with the item being dropped */ -class DraggableWrapperComponent extends React.Component { - public shouldComponentUpdate = ({ dataProvider, render, truncate }: Props) => - isEqual(dataProvider, this.props.dataProvider) && - render !== this.props.render && - truncate === this.props.truncate - ? false - : true; - - public componentDidMount() { - const { dataProvider, registerProvider } = this.props; - - registerProvider!({ provider: dataProvider }); - } - - public componentWillUnmount() { - const { dataProvider, unRegisterProvider } = this.props; - - unRegisterProvider!({ id: dataProvider.id }); - } - public render() { - const { dataProvider, render, truncate } = this.props; +const DraggableWrapperComponent = React.memo( + ({ dataProvider, registerProvider, render, truncate, unRegisterProvider }) => { + useEffect(() => { + registerProvider!({ provider: dataProvider }); + return () => { + unRegisterProvider!({ id: dataProvider.id }); + }; + }, []); return ( @@ -223,8 +210,17 @@ class DraggableWrapperComponent extends React.Component { ); + }, + (prevProps, nextProps) => { + return ( + isEqual(prevProps.dataProvider, nextProps.dataProvider) && + prevProps.render !== nextProps.render && + prevProps.truncate === nextProps.truncate + ); } -} +); + +DraggableWrapperComponent.displayName = 'DraggableWrapperComponent'; export const DraggableWrapper = connect( null, diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx index 10b4340b6a88d2..dc7f2185c26b72 100644 --- a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx @@ -9,15 +9,15 @@ import { EuiButton, EuiComboBox, EuiComboBoxOptionProps, + EuiFieldText, EuiFlexGroup, EuiFlexItem, - EuiFieldText, EuiFormRow, EuiPanel, EuiSpacer, EuiToolTip, } from '@elastic/eui'; -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import styled, { injectGlobal } from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -37,8 +37,8 @@ import * as i18n from './translations'; const EDIT_DATA_PROVIDER_WIDTH = 400; const FIELD_COMBO_BOX_WIDTH = 195; const OPERATOR_COMBO_BOX_WIDTH = 160; -const VALUE_INPUT_CLASS_NAME = 'edit-data-provider-value'; const SAVE_CLASS_NAME = 'edit-data-provider-save'; +const VALUE_INPUT_CLASS_NAME = 'edit-data-provider-value'; export const HeaderContainer = styled.div` width: ${EDIT_DATA_PROVIDER_WIDTH}; @@ -68,12 +68,6 @@ interface Props { value: string | number; } -interface State { - updatedField: EuiComboBoxOptionProps[]; - updatedOperator: EuiComboBoxOptionProps[]; - updatedValue: string | number; -} - const sanatizeValue = (value: string | number): string => Array.isArray(value) ? `${value[0]}` : `${value}`; // fun fact: value should never be an array @@ -88,37 +82,80 @@ export const getInitialOperatorLabel = ( } }; -export class StatefulEditDataProvider extends React.PureComponent { - constructor(props: Props) { - super(props); +export const StatefulEditDataProvider = React.memo( + ({ + andProviderId, + browserFields, + field, + isExcluded, + onDataProviderEdited, + operator, + providerId, + timelineId, + value, + }) => { + const [updatedField, setUpdatedField] = useState([{ label: field }]); + const [updatedOperator, setUpdatedOperator] = useState( + getInitialOperatorLabel(isExcluded, operator) + ); + const [updatedValue, setUpdatedValue] = useState(value); - const { field, isExcluded, operator, value } = props; + /** Focuses the Value input if it is visible, falling back to the Save button if it's not */ + function focusInput() { + const elements = document.getElementsByClassName(VALUE_INPUT_CLASS_NAME); - this.state = { - updatedField: [{ label: field }], - updatedOperator: getInitialOperatorLabel(isExcluded, operator), - updatedValue: value, - }; - } + if (elements.length > 0) { + (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` + } else { + const saveElements = document.getElementsByClassName(SAVE_CLASS_NAME); - public componentDidMount() { - this.disableScrolling(); - this.focusInput(); - } + if (saveElements.length > 0) { + (saveElements[0] as HTMLElement).focus(); + } + } + } - public componentWillUnmount() { - this.enableScrolling(); - } + function onFieldSelected(selectedField: EuiComboBoxOptionProps[]) { + setUpdatedField(selectedField); + + focusInput(); + } + + function onOperatorSelected(operatorSelected: EuiComboBoxOptionProps[]) { + setUpdatedOperator(operatorSelected); + + focusInput(); + } + + function onValueChange(e: React.ChangeEvent) { + setUpdatedValue(e.target.value); + } + + function disableScrolling() { + const x = + window.pageXOffset !== undefined + ? window.pageXOffset + : (document.documentElement || document.body.parentNode || document.body).scrollLeft; - public render() { - const { - andProviderId, - browserFields, - onDataProviderEdited, - providerId, - timelineId, - } = this.props; - const { updatedField, updatedOperator, updatedValue } = this.state; + const y = + window.pageYOffset !== undefined + ? window.pageYOffset + : (document.documentElement || document.body.parentNode || document.body).scrollTop; + + window.onscroll = () => window.scrollTo(x, y); + } + + function enableScrolling() { + window.onscroll = () => noop; + } + + useEffect(() => { + disableScrolling(); + focusInput(); + return () => { + enableScrolling(); + }; + }, []); return ( @@ -127,18 +164,14 @@ export class StatefulEditDataProvider extends React.PureComponent - 0 ? this.state.updatedField[0].label : null - } - > + 0 ? updatedField[0].label : null}> @@ -151,10 +184,10 @@ export class StatefulEditDataProvider extends React.PureComponent @@ -167,17 +200,17 @@ export class StatefulEditDataProvider extends React.PureComponent - {this.state.updatedOperator.length > 0 && - this.state.updatedOperator[0].label !== i18n.EXISTS && - this.state.updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( @@ -196,6 +229,13 @@ export class StatefulEditDataProvider extends React.PureComponent color="primary" data-test-subj="save" fill={true} + isDisabled={ + !selectionsAreValid({ + browserFields, + selectedField: updatedField, + selectedOperator: updatedOperator, + }) + } onClick={() => { onDataProviderEdited({ andProviderId, @@ -207,13 +247,6 @@ export class StatefulEditDataProvider extends React.PureComponent value: updatedValue, }); }} - isDisabled={ - !selectionsAreValid({ - browserFields: this.props.browserFields, - selectedField: updatedField, - selectedOperator: updatedOperator, - }) - } size="s" > {i18n.SAVE} @@ -225,53 +258,6 @@ export class StatefulEditDataProvider extends React.PureComponent ); } +); - /** Focuses the Value input if it is visible, falling back to the Save button if it's not */ - private focusInput = () => { - const elements = document.getElementsByClassName(VALUE_INPUT_CLASS_NAME); - - if (elements.length > 0) { - (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` - } else { - const saveElements = document.getElementsByClassName(SAVE_CLASS_NAME); - - if (saveElements.length > 0) { - (saveElements[0] as HTMLElement).focus(); - } - } - }; - - private onFieldSelected = (selectedField: EuiComboBoxOptionProps[]) => { - this.setState({ updatedField: selectedField }); - - this.focusInput(); - }; - - private onOperatorSelected = (updatedOperator: EuiComboBoxOptionProps[]) => { - this.setState({ updatedOperator }); - - this.focusInput(); - }; - - private onValueChange = (e: React.ChangeEvent) => { - this.setState({ - updatedValue: e.target.value, - }); - }; - - private disableScrolling = () => { - const x = - window.pageXOffset !== undefined - ? window.pageXOffset - : (document.documentElement || document.body.parentNode || document.body).scrollLeft; - - const y = - window.pageYOffset !== undefined - ? window.pageYOffset - : (document.documentElement || document.body.parentNode || document.body).scrollTop; - - window.onscroll = () => window.scrollTo(x, y); - }; - - private enableScrolling = () => (window.onscroll = () => noop); -} +StatefulEditDataProvider.displayName = 'StatefulEditDataProvider'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index 86696503dbda38..18040a35a52807 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -43,7 +43,7 @@ export interface EmbeddedMapProps { } export const EmbeddedMap = React.memo( - ({ applyFilterQueryFromKueryExpression, queryExpression, startDate, endDate, setQuery }) => { + ({ applyFilterQueryFromKueryExpression, endDate, queryExpression, setQuery, startDate }) => { const [embeddable, setEmbeddable] = React.useState(null); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(false); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx index ec76d8f90c3de9..cb677368298782 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React, { useState } from 'react'; import { BrowserFields } from '../../containers/source'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; @@ -23,43 +23,24 @@ interface Props { toggleColumn: (column: ColumnHeader) => void; } -interface State { - view: View; -} - -export class StatefulEventDetails extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { view: 'table-view' }; - } +export const StatefulEventDetails = React.memo( + ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { + const [view, setView] = useState('table-view'); - public onViewSelected = (view: View): void => { - this.setState({ view }); - }; - - public render() { - const { - browserFields, - columnHeaders, - data, - id, - onUpdateColumns, - timelineId, - toggleColumn, - } = this.props; return ( setView(newView)} timelineId={timelineId} toggleColumn={toggleColumn} + view={view} /> ); } -} +); + +StatefulEventDetails.displayName = 'StatefulEventDetails'; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx index 03fb37760bc356..d85231b564da8a 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx @@ -20,8 +20,17 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); const from = 1566943856794; const to = 1566857456791; - +// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 +/* eslint-disable no-console */ +const originalError = console.error; describe('EventsViewer', () => { + beforeAll(() => { + console.error = jest.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); test('it renders the "Showing..." subtitle with the expected event count', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx index bef5e66faecd16..dc0e1288f40f83 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx @@ -20,7 +20,17 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); const from = 1566943856794; const to = 1566857456791; +// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 +/* eslint-disable no-console */ +const originalError = console.error; describe('StatefulEventsViewer', () => { + beforeAll(() => { + console.error = jest.fn(); + }); + + afterAll(() => { + console.error = originalError; + }); test('it renders the events viewer', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 52b724525f5a94..d572d6dd4913b7 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -87,7 +87,7 @@ const StatefulEventsViewerComponent = React.memo( updateItemsPerPage, upsertColumn, }) => { - const [showInspect, setShowInspect] = useState(false); + const [showInspect, setShowInspect] = useState(false); useEffect(() => { if (createTimeline != null) { diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx index 17785ff582a3c2..fb47672512de56 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx @@ -5,8 +5,8 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiOutsideClickDetector } from '@elastic/eui'; +import React, { useEffect } from 'react'; import { noop } from 'lodash/fp'; -import * as React from 'react'; import styled, { css } from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -22,7 +22,7 @@ import { getFieldBrowserSearchInputClassName, PANES_FLEX_GROUP_WIDTH, } from './helpers'; -import { FieldBrowserProps, OnFieldSelected, OnHideFieldBrowser } from './types'; +import { FieldBrowserProps, OnHideFieldBrowser } from './types'; const FieldsBrowserContainer = styled.div<{ width: number }>` ${({ theme, width }) => css` @@ -102,34 +102,80 @@ type Props = Pick< * This component has no internal state, but it uses lifecycle methods to * set focus to the search input, scroll to the selected category, etc */ -export class FieldsBrowser extends React.PureComponent { - public componentDidMount() { - this.scrollViews(); - this.focusInput(); - } +export const FieldsBrowser = React.memo( + ({ + browserFields, + columnHeaders, + filteredBrowserFields, + isEventViewer, + isSearching, + onCategorySelected, + onFieldSelected, + onHideFieldBrowser, + onSearchInputChange, + onOutsideClick, + onUpdateColumns, + searchInput, + selectedCategoryId, + timelineId, + toggleColumn, + width, + }) => { + /** Focuses the input that filters the field browser */ + function focusInput() { + const elements = document.getElementsByClassName( + getFieldBrowserSearchInputClassName(timelineId) + ); - public componentDidUpdate() { - this.scrollViews(); - this.focusInput(); // always re-focus the input to enable additional filtering - } + if (elements.length > 0) { + (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` + } + } + + /** Invoked when the user types in the input to filter the field browser */ + function onInputChange(event: React.ChangeEvent) { + onSearchInputChange(event.target.value); + } + + function selectFieldAndHide(fieldId: string) { + if (onFieldSelected != null) { + onFieldSelected(fieldId); + } - public render() { - const { - columnHeaders, - browserFields, - filteredBrowserFields, - searchInput, - isEventViewer, - isSearching, - onCategorySelected, - onFieldSelected, - onOutsideClick, - onUpdateColumns, - selectedCategoryId, - timelineId, - toggleColumn, - width, - } = this.props; + onHideFieldBrowser(); + } + + function scrollViews() { + if (selectedCategoryId !== '') { + const categoryPaneTitles = document.getElementsByClassName( + getCategoryPaneCategoryClassName({ + categoryId: selectedCategoryId, + timelineId, + }) + ); + + if (categoryPaneTitles.length > 0) { + categoryPaneTitles[0].scrollIntoView(); + } + + const fieldPaneTitles = document.getElementsByClassName( + getFieldBrowserCategoryTitleClassName({ + categoryId: selectedCategoryId, + timelineId, + }) + ); + + if (fieldPaneTitles.length > 0) { + fieldPaneTitles[0].scrollIntoView(); + } + } + + focusInput(); // always re-focus the input to enable additional filtering + } + + useEffect(() => { + scrollViews(); + }, [selectedCategoryId, timelineId]); return ( { isEventViewer={isEventViewer} isSearching={isSearching} onOutsideClick={onOutsideClick} - onSearchInputChange={this.onInputChange} + onSearchInputChange={onInputChange} onUpdateColumns={onUpdateColumns} searchInput={searchInput} timelineId={timelineId} @@ -170,7 +216,7 @@ export class FieldsBrowser extends React.PureComponent { data-test-subj="fields-pane" filteredBrowserFields={filteredBrowserFields} onCategorySelected={onCategorySelected} - onFieldSelected={this.selectFieldAndHide} + onFieldSelected={selectFieldAndHide} onUpdateColumns={onUpdateColumns} searchInput={searchInput} selectedCategoryId={selectedCategoryId} @@ -184,59 +230,4 @@ export class FieldsBrowser extends React.PureComponent { ); } - - /** Focuses the input that filters the field browser */ - private focusInput = () => { - const elements = document.getElementsByClassName( - getFieldBrowserSearchInputClassName(this.props.timelineId) - ); - - if (elements.length > 0) { - (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` - } - }; - - /** Invoked when the user types in the input to filter the field browser */ - private onInputChange = (event: React.ChangeEvent) => - this.props.onSearchInputChange(event.target.value); - - private selectFieldAndHide: OnFieldSelected = (fieldId: string) => { - const { onFieldSelected, onHideFieldBrowser } = this.props; - - if (onFieldSelected != null) { - onFieldSelected(fieldId); - } - - onHideFieldBrowser(); - }; - - private scrollViews = () => { - const { selectedCategoryId, timelineId } = this.props; - - if (this.props.selectedCategoryId !== '') { - const categoryPaneTitles = document.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ); - - if (categoryPaneTitles.length > 0) { - categoryPaneTitles[0].scrollIntoView(); - } - - const fieldPaneTitles = document.getElementsByClassName( - getFieldBrowserCategoryTitleClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ); - - if (fieldPaneTitles.length > 0) { - fieldPaneTitles[0].scrollIntoView(); - } - } - - this.focusInput(); // always re-focus the input to enable additional filtering - }; -} +); diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx index 69720c76cab803..7d21e1f44d04b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx @@ -6,7 +6,7 @@ import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import * as React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -15,7 +15,6 @@ import { BrowserFields } from '../../containers/source'; import { timelineActions } from '../../store/actions'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; -import { OnUpdateColumns } from '../timeline/events'; import { FieldsBrowser } from './field_browser'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; import * as i18n from './translations'; @@ -26,19 +25,6 @@ const fieldsButtonClassName = 'fields-button'; /** wait this many ms after the user completes typing before applying the filter input */ const INPUT_TIMEOUT = 250; -interface State { - /** all field names shown in the field browser must contain this string (when specified) */ - filterInput: string; - /** all fields in this collection have field names that match the filterInput */ - filteredBrowserFields: BrowserFields | null; - /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ - isSearching: boolean; - /** this category will be displayed in the right-hand pane of the field browser */ - selectedCategoryId: string; - /** show the field browser */ - show: boolean; -} - const FieldsBrowserButtonContainer = styled.div` position: relative; `; @@ -60,52 +46,110 @@ interface DispatchProps { /** * Manages the state of the field browser */ -export class StatefulFieldsBrowserComponent extends React.PureComponent< - FieldBrowserProps & DispatchProps, - State -> { - /** tracks the latest timeout id from `setTimeout`*/ - private inputTimeoutId: number = 0; - - constructor(props: FieldBrowserProps) { - super(props); - - this.state = { - filterInput: '', - filteredBrowserFields: null, - isSearching: false, - selectedCategoryId: DEFAULT_CATEGORY_NAME, - show: false, - }; - } +export const StatefulFieldsBrowserComponent = React.memo( + ({ + columnHeaders, + browserFields, + height, + isEventViewer = false, + onFieldSelected, + onUpdateColumns, + timelineId, + toggleColumn, + width, + }) => { + /** tracks the latest timeout id from `setTimeout`*/ + const inputTimeoutId = useRef(0); + + /** all field names shown in the field browser must contain this string (when specified) */ + const [filterInput, setFilterInput] = useState(''); + /** all fields in this collection have field names that match the filterInput */ + const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); + /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ + const [isSearching, setIsSearching] = useState(false); + /** this category will be displayed in the right-hand pane of the field browser */ + const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + /** show the field browser */ + const [show, setShow] = useState(false); + useEffect(() => { + return () => { + if (inputTimeoutId.current !== 0) { + // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: + clearTimeout(inputTimeoutId.current); + inputTimeoutId.current = 0; + } + }; + }, []); + + /** Shows / hides the field browser */ + function toggleShow() { + setShow(!show); + } + + /** Invoked when the user types in the filter input */ + function updateFilter(newFilterInput: string) { + setFilterInput(newFilterInput); + setIsSearching(true); + + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: filterInput, + }); + + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + + const newSelectedCategoryId = + filterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(newFilteredBrowserFields) + .sort() + .reduce( + (selected, category) => + newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + newFilteredBrowserFields[category].fields!.length > + newFilteredBrowserFields[selected].fields!.length + ? category + : selected, + Object.keys(newFilteredBrowserFields)[0] + ); + setSelectedCategoryId(newSelectedCategoryId); + }, INPUT_TIMEOUT); + } - public componentWillUnmount() { - if (this.inputTimeoutId !== 0) { - // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: - clearTimeout(this.inputTimeoutId); - this.inputTimeoutId = 0; + /** + * Invoked when the user clicks a category name in the left-hand side of + * the field browser + */ + function updateSelectedCategoryId(categoryId: string) { + setSelectedCategoryId(categoryId); } - } - public render() { - const { - columnHeaders, - browserFields, - height, - isEventViewer = false, - onFieldSelected, - timelineId, - toggleColumn, - width, - } = this.props; - const { - filterInput, - filteredBrowserFields, - isSearching, - selectedCategoryId, - show, - } = this.state; + /** + * Invoked when the user clicks on the context menu to view a category's + * columns in the timeline, this function dispatches the action that + * causes the timeline display those columns. + */ + function updateColumnsAndSelectCategoryId(columns: ColumnHeader[]) { + onUpdateColumns(columns); // show the category columns in the timeline + } + /** Invoked when the field browser should be hidden */ + function hideFieldBrowser() { + setFilterInput(''); + setFilterInput(''); + setFilteredBrowserFields(null); + setIsSearching(false); + setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setShow(false); + } // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = show ? mergeBrowserFieldsWithDefaultCategory(browserFields) @@ -121,14 +165,14 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent< className={fieldsButtonClassName} data-test-subj="show-field-browser-gear" iconType="list" - onClick={this.toggleShow} + onClick={toggleShow} /> ) : ( {i18n.FIELDS} @@ -148,12 +192,12 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent< height={height} isEventViewer={isEventViewer} isSearching={isSearching} - onCategorySelected={this.updateSelectedCategoryId} + onCategorySelected={updateSelectedCategoryId} onFieldSelected={onFieldSelected} - onHideFieldBrowser={this.hideFieldBrowser} - onOutsideClick={show ? this.hideFieldBrowser : noop} - onSearchInputChange={this.updateFilter} - onUpdateColumns={this.updateColumnsAndSelectCategoryId} + onHideFieldBrowser={hideFieldBrowser} + onOutsideClick={show ? hideFieldBrowser : noop} + onSearchInputChange={updateFilter} + onUpdateColumns={updateColumnsAndSelectCategoryId} searchInput={filterInput} selectedCategoryId={selectedCategoryId} timelineId={timelineId} @@ -165,84 +209,9 @@ export class StatefulFieldsBrowserComponent extends React.PureComponent< ); } +); - /** Shows / hides the field browser */ - private toggleShow = () => { - this.setState(({ show }) => ({ - show: !show, - })); - }; - - /** Invoked when the user types in the filter input */ - private updateFilter = (filterInput: string): void => { - this.setState({ - filterInput, - isSearching: true, - }); - - if (this.inputTimeoutId !== 0) { - clearTimeout(this.inputTimeoutId); // ⚠️ mutation: cancel any previous timers - } - - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - this.inputTimeoutId = window.setTimeout(() => { - const filteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(this.props.browserFields), - substring: this.state.filterInput, - }); - - this.setState(currentState => ({ - filteredBrowserFields, - isSearching: false, - selectedCategoryId: - currentState.filterInput === '' || Object.keys(filteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(filteredBrowserFields) - .sort() - .reduce( - (selected, category) => - filteredBrowserFields[category].fields != null && - filteredBrowserFields[selected].fields != null && - filteredBrowserFields[category].fields!.length > - filteredBrowserFields[selected].fields!.length - ? category - : selected, - Object.keys(filteredBrowserFields)[0] - ), - })); - }, INPUT_TIMEOUT); - }; - - /** - * Invoked when the user clicks a category name in the left-hand side of - * the field browser - */ - private updateSelectedCategoryId = (categoryId: string): void => { - this.setState({ - selectedCategoryId: categoryId, - }); - }; - - /** - * Invoked when the user clicks on the context menu to view a category's - * columns in the timeline, this function dispatches the action that - * causes the timeline display those columns. - */ - private updateColumnsAndSelectCategoryId: OnUpdateColumns = (columns: ColumnHeader[]): void => { - this.props.onUpdateColumns(columns); // show the category columns in the timeline - }; - - /** Invoked when the field browser should be hidden */ - private hideFieldBrowser = () => { - this.setState({ - filterInput: '', - filteredBrowserFields: null, - isSearching: false, - selectedCategoryId: DEFAULT_CATEGORY_NAME, - show: false, - }); - }; -} +StatefulFieldsBrowserComponent.displayName = 'StatefulFieldsBrowserComponent'; export const StatefulFieldsBrowser = connect( null, diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 15ce42c6a16b64..ceaff289f776cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -111,18 +111,30 @@ const FlyoutHeaderWithCloseButton = React.memo<{ FlyoutHeaderWithCloseButton.displayName = 'FlyoutHeaderWithCloseButton'; -class FlyoutPaneComponent extends React.PureComponent { - public render() { - const { - children, - flyoutHeight, - headerHeight, - onClose, - timelineId, - usersViewing, - width, - } = this.props; - +const FlyoutPaneComponent = React.memo( + ({ + applyDeltaToWidth, + children, + flyoutHeight, + headerHeight, + onClose, + timelineId, + usersViewing, + width, + }) => { + const renderFlyout = () => <>; + + const onResize: OnResize = ({ delta, id }) => { + const bodyClientWidthPixels = document.body.clientWidth; + + applyDeltaToWidth({ + bodyClientWidthPixels, + delta, + id, + maxWidthPercent, + minWidthPixels, + }); + }; return ( { } id={timelineId} - onResize={this.onResize} - render={this.renderFlyout} + onResize={onResize} + render={renderFlyout} /> { ); } +); - private renderFlyout = () => <>; - - private onResize: OnResize = ({ delta, id }) => { - const { applyDeltaToWidth } = this.props; - - const bodyClientWidthPixels = document.body.clientWidth; - - applyDeltaToWidth({ - bodyClientWidthPixels, - delta, - id, - maxWidthPercent, - minWidthPixels, - }); - }; -} +FlyoutPaneComponent.displayName = 'FlyoutPaneComponent'; export const Pane = connect( null, diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx index 90af0d56c15820..b59753e8add6a7 100644 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx +++ b/x-pack/legacy/plugins/siem/public/components/help_menu/help_menu.tsx @@ -16,30 +16,28 @@ export const Icon = styled(EuiIcon)` Icon.displayName = 'Icon'; -export class HelpMenuComponent extends React.PureComponent { - public render() { - return ( - <> - - - - - -
- - -
-
- - - - - - - - ); - } -} +export const HelpMenuComponent = React.memo(() => ( + <> + + + + + +
+ + +
+
+ + + + + + + +)); + +HelpMenuComponent.displayName = 'HelpMenuComponent'; diff --git a/x-pack/legacy/plugins/siem/public/components/lazy_accordion/index.tsx b/x-pack/legacy/plugins/siem/public/components/lazy_accordion/index.tsx index 5ed9a3b623c1ce..da2e7334756e4e 100644 --- a/x-pack/legacy/plugins/siem/public/components/lazy_accordion/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/lazy_accordion/index.tsx @@ -5,7 +5,7 @@ */ import { EuiAccordion, EuiAccordionProps } from '@elastic/eui'; -import * as React from 'react'; +import React, { useState } from 'react'; type Props = Pick> & { forceExpand?: boolean; @@ -14,10 +14,6 @@ type Props = Pick React.ReactNode; }; -interface State { - expanded: boolean; -} - /** * An accordion that doesn't render it's content unless it's expanded. * This component was created because `EuiAccordion`'s eager rendering of @@ -33,29 +29,36 @@ interface State { * TODO: animate the expansion and collapse of content rendered "below" * the real `EuiAccordion`. */ -export class LazyAccordion extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { - expanded: false, +export const LazyAccordion = React.memo( + ({ + buttonContent, + buttonContentClassName, + extraAction, + forceExpand, + id, + onCollapse, + onExpand, + paddingSize, + renderExpandedContent, + }) => { + const [expanded, setExpanded] = useState(false); + const onCollapsedClick = () => { + setExpanded(true); + if (onExpand != null) { + onExpand(); + } }; - } - public render() { - const { - id, - buttonContentClassName, - buttonContent, - forceExpand, - extraAction, - renderExpandedContent, - paddingSize, - } = this.props; + const onExpandedClick = () => { + setExpanded(false); + if (onCollapse != null) { + onCollapse(); + } + }; return ( <> - {forceExpand || this.state.expanded ? ( + {forceExpand || expanded ? ( <> { extraAction={extraAction} id={id} initialIsOpen={true} - onClick={this.onExpandedClick} + onClick={onExpandedClick} paddingSize={paddingSize} > <> - {renderExpandedContent(this.state.expanded)} + {renderExpandedContent(expanded)} ) : ( { data-test-subj="lazy-accordion-placeholder" extraAction={extraAction} id={id} - onClick={this.onCollapsedClick} + onClick={onCollapsedClick} paddingSize={paddingSize} /> )} ); } +); - private onCollapsedClick = () => { - const { onExpand } = this.props; - - this.setState({ expanded: true }); - - if (onExpand != null) { - onExpand(); - } - }; - - private onExpandedClick = () => { - const { onCollapse } = this.props; - - this.setState({ expanded: false }); - - if (onCollapse != null) { - onCollapse(); - } - }; -} +LazyAccordion.displayName = 'LazyAccordion'; diff --git a/x-pack/legacy/plugins/siem/public/components/load_more_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/load_more_table/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 4bf3f647502e2d..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/load_more_table/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,104 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Load More Table Component rendering it renders the default load more table 1`] = ` - - My test supplement. -

- } - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={ - Array [ - Object { - "numberOfRow": 2, - "text": "2 rows", - }, - Object { - "numberOfRow": 5, - "text": "5 rows", - }, - Object { - "numberOfRow": 10, - "text": "10 rows", - }, - Object { - "numberOfRow": 20, - "text": "20 rows", - }, - Object { - "numberOfRow": 50, - "text": "50 rows", - }, - ] - } - limit={1} - loadMore={[Function]} - loading={false} - pageOfItems={ - Array [ - Object { - "cursor": Object { - "value": "98966fa2013c396155c460d35c0902be", - }, - "host": Object { - "_id": "cPsuhGcB0WOhS6qyTKC0", - "firstSeen": "2018-12-06T15:40:53.319Z", - "name": "elrond.elstc.co", - "os": "Ubuntu", - "version": "18.04.1 LTS (Bionic Beaver)", - }, - }, - Object { - "cursor": Object { - "value": "aa7ca589f1b8220002f2fc61c64cfbf1", - }, - "host": Object { - "_id": "KwQDiWcB0WOhS6qyXmrW", - "firstSeen": "2018-12-07T14:12:38.560Z", - "name": "siem-kibana", - "os": "Debian GNU/Linux", - "version": "9 (stretch)", - }, - }, - ] - } - updateLimitPagination={[Function]} -/> -`; diff --git a/x-pack/legacy/plugins/siem/public/components/load_more_table/index.mock.tsx b/x-pack/legacy/plugins/siem/public/components/load_more_table/index.mock.tsx deleted file mode 100644 index 02ec00a78bc91c..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/load_more_table/index.mock.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 { getOrEmptyTagFromValue } from '../empty_value'; - -import { Columns, ItemsPerRow } from './index'; - -export const mockData = { - Hosts: { - totalCount: 4, - edges: [ - { - host: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - name: 'elrond.elstc.co', - os: 'Ubuntu', - version: '18.04.1 LTS (Bionic Beaver)', - firstSeen: '2018-12-06T15:40:53.319Z', - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, - }, - { - host: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - name: 'siem-kibana', - os: 'Debian GNU/Linux', - version: '9 (stretch)', - firstSeen: '2018-12-07T14:12:38.560Z', - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - endCursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - hasNextPage: true, - }, - }, -}; - -export const getHostsColumns = (): [ - Columns, - Columns, - Columns, - Columns -] => [ - { - field: 'node.host.name', - name: 'Host', - truncateText: false, - hideForMobile: false, - render: (name: string) => getOrEmptyTagFromValue(name), - }, - { - field: 'node.host.firstSeen', - name: 'First seen', - truncateText: false, - hideForMobile: false, - render: (firstSeen: string) => getOrEmptyTagFromValue(firstSeen), - }, - { - field: 'node.host.os', - name: 'OS', - truncateText: false, - hideForMobile: false, - render: (os: string) => getOrEmptyTagFromValue(os), - }, - { - field: 'node.host.version', - name: 'Version', - truncateText: false, - hideForMobile: false, - render: (version: string) => getOrEmptyTagFromValue(version), - }, -]; - -export const sortedHosts: [ - Columns, - Columns, - Columns, - Columns -] = getHostsColumns().map(h => ({ ...h, sortable: true })) as [ - Columns, - Columns, - Columns, - Columns -]; - -export const rowItems: ItemsPerRow[] = [ - { - text: '2 rows', - numberOfRow: 2, - }, - { - text: '5 rows', - numberOfRow: 5, - }, - { - text: '10 rows', - numberOfRow: 10, - }, - { - text: '20 rows', - numberOfRow: 20, - }, - { - text: '50 rows', - numberOfRow: 50, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/components/load_more_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/load_more_table/index.test.tsx deleted file mode 100644 index 3c42d3d2acfe32..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/load_more_table/index.test.tsx +++ /dev/null @@ -1,360 +0,0 @@ -/* - * 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 { mount, shallow } from 'enzyme'; -import toJson from 'enzyme-to-json'; -import * as React from 'react'; - -import { Direction } from '../../graphql/types'; - -import { LoadMoreTable } from './index'; -import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { ThemeProvider } from 'styled-components'; - -describe('Load More Table Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const loadMore = jest.fn(); - const updateLimitPagination = jest.fn(); - describe('rendering', () => { - test('it renders the default load more table', () => { - const wrapper = shallow( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect(toJson(wrapper)).toMatchSnapshot(); - }); - - test('it renders the loading panel at the beginning ', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={[]} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect( - wrapper.find('[data-test-subj="initialLoadingPanelLoadMoreTable"]').exists() - ).toBeTruthy(); - }); - - test('it renders the over loading panel after data has been in the table ', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect(wrapper.find('[data-test-subj="loadingPanelLoadMoreTable"]').exists()).toBeTruthy(); - }); - - test('it renders the loadMore button if need to fetch more', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect( - wrapper - .find('[data-test-subj="loadingMoreButton"]') - .first() - .text() - ).toContain('Load more'); - }); - - test('it renders the Loading... in the more load button when fetching new data', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect( - wrapper.find('[data-test-subj="initialLoadingPanelLoadMoreTable"]').exists() - ).toBeFalsy(); - expect( - wrapper - .find('[data-test-subj="loadingMoreButton"]') - .first() - .text() - ).toContain('Loading…'); - }); - - test('it does NOT render the loadMore button because there is nothing else to fetch', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect(wrapper.find('[data-test-subj="loadingMoreButton"]').exists()).toBeFalsy(); - }); - - test('it render popover to select new limit in table', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); - }); - - test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={[]} - limit={2} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - - test('It should render a sort icon if sorting is defined', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadMore={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); - }); - }); - - describe('Events', () => { - test('should call loadmore when clicking on the button load more', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - wrapper - .find('[data-test-subj="loadingMoreButton"]') - .first() - .simulate('click'); - - expect(loadMore).toBeCalled(); - }); - - test('Should call updateLimitPagination when you pick a new limit', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadMore={() => loadMore(mockData.Hosts.pageInfo.endCursor)} - pageOfItems={mockData.Hosts.edges} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateLimitPagination).toBeCalled(); - }); - - test('Should call onChange when you choose a new sort in the table', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadMore={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - updateLimitPagination={newlimit => updateLimitPagination({ limit: newlimit })} - /> -
- ); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - expect(mockOnChange).toBeCalled(); - expect(mockOnChange.mock.calls[0]).toEqual([ - { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, - ]); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/load_more_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/load_more_table/index.tsx deleted file mode 100644 index 0663246039cb8d..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/load_more_table/index.tsx +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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 { - EuiBasicTable, - EuiButton, - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingContent, - EuiPopover, -} from '@elastic/eui'; -import { isEmpty, noop } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import { Direction } from '../../graphql/types'; -import { HeaderPanel } from '../header_panel'; -import { Loader } from '../loader'; - -import * as i18n from './translations'; -import { Panel } from '../panel'; - -const DEFAULT_DATA_TEST_SUBJ = 'load-more-table'; - -export interface ItemsPerRow { - text: string; - numberOfRow: number; -} - -export interface SortingBasicTable { - field: string; - direction: Direction; - allowNeutralSort?: boolean; -} - -export interface Criteria { - page?: { index: number; size: number }; - sort?: SortingBasicTable; -} - -// Using telescoping templates to remove 'any' that was polluting downstream column type checks -interface BasicTableProps { - columns: - | [Columns] - | [Columns, Columns] - | [Columns, Columns, Columns] - | [Columns, Columns, Columns, Columns] - | [Columns, Columns, Columns, Columns, Columns] - | [Columns, Columns, Columns, Columns, Columns, Columns] - | [Columns, Columns, Columns, Columns, Columns, Columns, Columns] - | [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns - ] - | [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns - ]; - hasNextPage: boolean; - dataTestSubj?: string; - headerCount: number; - headerSupplement?: React.ReactElement; - headerTitle: string | React.ReactElement; - headerTooltip?: string; - headerUnit: string | React.ReactElement; - id?: string; - itemsPerRow?: ItemsPerRow[]; - limit: number; - loading: boolean; - loadMore: () => void; - onChange?: (criteria: Criteria) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pageOfItems: any[]; - sorting?: SortingBasicTable; - updateLimitPagination: (limit: number) => void; -} - -interface BasicTableState { - loadingInitial: boolean; - isPopoverOpen: boolean; - showInspect: boolean; -} - -type Func = (arg: T) => string | number; - -export interface Columns { - field?: string; - align?: string; - name: string | React.ReactNode; - isMobileHeader?: boolean; - sortable?: boolean | Func; - truncateText?: boolean; - hideForMobile?: boolean; - render?: (item: T, node: U) => React.ReactNode; - width?: string; -} - -export class LoadMoreTable extends React.PureComponent< - BasicTableProps, - BasicTableState -> { - public readonly state = { - loadingInitial: this.props.headerCount === -1, - isPopoverOpen: false, - showInspect: false, - }; - - static getDerivedStateFromProps( - props: BasicTableProps, - state: BasicTableState - ) { - if (state.loadingInitial && props.headerCount >= 0) { - return { - ...state, - loadingInitial: false, - }; - } - return null; - } - - public render() { - const { - columns, - dataTestSubj = DEFAULT_DATA_TEST_SUBJ, - hasNextPage, - headerCount, - headerSupplement, - headerTitle, - headerTooltip, - headerUnit, - id, - itemsPerRow, - limit, - loading, - onChange = noop, - pageOfItems, - sorting = null, - updateLimitPagination, - } = this.props; - const { loadingInitial } = this.state; - - const button = ( - - {`${i18n.ROWS}: ${limit}`} - - ); - - const rowItems = - itemsPerRow && - itemsPerRow.map(item => ( - { - this.closePopover(); - updateLimitPagination(item.numberOfRow); - }} - > - {item.text} - - )); - - return ( - - = 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` - } - title={headerTitle} - tooltip={headerTooltip} - > - {!loadingInitial && headerSupplement} - - - {loadingInitial ? ( - - ) : ( - <> - - - {hasNextPage && ( - - - {!isEmpty(itemsPerRow) && ( - - - - )} - - - - - {loading ? `${i18n.LOADING}` : i18n.LOAD_MORE} - - - - )} - - {loading && } - - )} - - ); - } - - private mouseEnter = () => { - this.setState(prevState => ({ - ...prevState, - showInspect: true, - })); - }; - - private mouseLeave = () => { - this.setState(prevState => ({ - ...prevState, - showInspect: false, - })); - }; - - private onButtonClick = () => { - this.setState(prevState => ({ - ...prevState, - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - private closePopover = () => { - this.setState(prevState => ({ - ...prevState, - isPopoverOpen: false, - })); - }; -} - -const BasicTable = styled(EuiBasicTable)` - tbody { - th, - td { - vertical-align: top; - } - - .euiTableCellContent { - display: block; - } - } -`; - -BasicTable.displayName = 'BasicTable'; - -const FooterAction = styled(EuiFlexGroup).attrs({ - alignItems: 'center', - responsive: false, -})` - margin-top: ${props => props.theme.eui.euiSizeXS}; -`; - -FooterAction.displayName = 'FooterAction'; diff --git a/x-pack/legacy/plugins/siem/public/components/load_more_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/load_more_table/translations.ts deleted file mode 100644 index ec093f97216247..00000000000000 --- a/x-pack/legacy/plugins/siem/public/components/load_more_table/translations.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export const LOADING = i18n.translate('xpack.siem.loadMoreTable.loadingButtonLabel', { - defaultMessage: 'Loading…', -}); - -export const LOAD_MORE = i18n.translate('xpack.siem.loadMoreTable.loadMoreButtonLabel', { - defaultMessage: 'Load more', -}); - -export const SHOWING = i18n.translate('xpack.siem.loadMoreTable.showingSubtitle', { - defaultMessage: 'Showing', -}); - -export const ROWS = i18n.translate('xpack.siem.loadMoreTable.rowsButtonLabel', { - defaultMessage: 'Rows per page', -}); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx index b2470bc0f5abd4..0956e93829e5ab 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx @@ -21,7 +21,7 @@ export const MlCapabilitiesContext = React.createContext(emptyMl MlCapabilitiesContext.displayName = 'MlCapabilitiesContext'; export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ children }) => { - const [capabilities, setCapabilities] = useState(emptyMlCapabilities); + const [capabilities, setCapabilities] = useState(emptyMlCapabilities); const [, dispatchToaster] = useStateToaster(); const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 3a1fcbb653efec..04fed8e4fff3f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -8,7 +8,7 @@ import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_ import { HostsType } from '../../../store/hosts/model'; import * as i18n from './translations'; import { AnomaliesByHost, Anomaly } from '../types'; -import { Columns } from '../../load_more_table'; +import { Columns } from '../../paginated_table'; import { TestProviders } from '../../../mock'; import { mount } from 'enzyme'; import React from 'react'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx index f0cf2e5a6e662d..6650449dd8200d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import moment from 'moment'; -import { Columns } from '../../load_more_table'; +import { Columns } from '../../paginated_table'; import { AnomaliesByHost, Anomaly, NarrowDateRange } from '../types'; import { getRowItemDraggable } from '../../tables/helpers'; import { EntityDraggable } from '../entity_draggable'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx index d063ed023bca6a..768c7af8f4b2c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -8,7 +8,7 @@ import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_ import { NetworkType } from '../../../store/network/model'; import * as i18n from './translations'; import { AnomaliesByNetwork, Anomaly } from '../types'; -import { Columns } from '../../load_more_table'; +import { Columns } from '../../paginated_table'; import { mount } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx index fb43175686e3d7..1e1628fb077dd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import moment from 'moment'; -import { Columns } from '../../load_more_table'; +import { Columns } from '../../paginated_table'; import { Anomaly, NarrowDateRange, AnomaliesByNetwork } from '../types'; import { getRowItemDraggable } from '../../tables/helpers'; import { EntityDraggable } from '../entity_draggable'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index 25ebb8ad89ecd1..96cb85b246a49b 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import * as React from 'react'; import { CONSTANTS } from '../url_state/constants'; @@ -63,7 +63,7 @@ describe('SIEM Navigation', () => { }, [CONSTANTS.timelineId]: '', }; - const wrapper = shallow(); + const wrapper = mount(); test('it calls setBreadcrumbs with correct path on mount', () => { expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, { detailName: undefined, diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index 6c5cac1464e79c..06f7a2ffb05661 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import React from 'react'; +import React, { useEffect } from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; @@ -27,78 +27,23 @@ import { TabNavigation } from './tab_navigation'; import { TabNavigationProps } from './tab_navigation/types'; import { SiemNavigationComponentProps } from './types'; -export class SiemNavigationComponent extends React.Component { - public shouldComponentUpdate(nextProps: Readonly): boolean { - if ( - this.props.pathName === nextProps.pathName && - this.props.search === nextProps.search && - isEqual(this.props.hosts, nextProps.hosts) && - isEqual(this.props.hostDetails, nextProps.hostDetails) && - isEqual(this.props.network, nextProps.network) && - isEqual(this.props.navTabs, nextProps.navTabs) && - isEqual(this.props.timerange, nextProps.timerange) && - isEqual(this.props.timelineId, nextProps.timelineId) - ) { - return false; - } - return true; - } - - public componentWillMount(): void { - const { - detailName, - hosts, - hostDetails, - navTabs, - network, - pageName, - pathName, - search, - tabName, - timerange, - timelineId, - } = this.props; - if (pathName) { - setBreadcrumbs({ - detailName, - hosts, - hostDetails, - navTabs, - network, - pageName, - pathName, - search, - tabName, - timerange, - timelineId, - }); - } - } - - public componentWillReceiveProps(nextProps: Readonly): void { - if ( - this.props.pathName !== nextProps.pathName || - this.props.search !== nextProps.search || - !isEqual(this.props.hosts, nextProps.hosts) || - !isEqual(this.props.hostDetails, nextProps.hostDetails) || - !isEqual(this.props.network, nextProps.network) || - !isEqual(this.props.navTabs, nextProps.navTabs) || - !isEqual(this.props.timerange, nextProps.timerange) || - !isEqual(this.props.timelineId, nextProps.timelineId) - ) { - const { - detailName, - hosts, - hostDetails, - navTabs, - network, - pageName, - pathName, - search, - tabName, - timelineId, - timerange, - } = nextProps; +export const SiemNavigationComponent = React.memo( + ({ + detailName, + display, + hostDetails, + hosts, + navTabs, + network, + pageName, + pathName, + search, + showBorder, + tabName, + timelineId, + timerange, + }) => { + useEffect(() => { if (pathName) { setBreadcrumbs({ detailName, @@ -114,23 +59,8 @@ export class SiemNavigationComponent extends React.Component ); + }, + (prevProps, nextProps) => { + return ( + prevProps.pathName === nextProps.pathName && + prevProps.search === nextProps.search && + isEqual(prevProps.hosts, nextProps.hosts) && + isEqual(prevProps.hostDetails, nextProps.hostDetails) && + isEqual(prevProps.network, nextProps.network) && + isEqual(prevProps.navTabs, nextProps.navTabs) && + isEqual(prevProps.timerange, nextProps.timerange) && + isEqual(prevProps.timelineId, nextProps.timelineId) + ); } -} +); + +SiemNavigationComponent.displayName = 'SiemNavigationComponent'; const makeMapStateToProps = () => { const getInputsSelector = inputsSelectors.inputsSelector(); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx index b6ec9e5ee0e029..61a0ec9c06c2d0 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { mount, shallow } from 'enzyme'; import * as React from 'react'; import { TabNavigation } from './'; @@ -74,8 +74,8 @@ describe('Tab Navigation', () => { expect(hostsTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { - const wrapper = shallow(); - const networkTab = () => wrapper.find('[data-test-subj="navigation-network"]'); + const wrapper = mount(); + const networkTab = () => wrapper.find('[data-test-subj="navigation-network"]').first(); expect(networkTab().prop('isSelected')).toBeFalsy(); wrapper.setProps({ pageName: 'network', @@ -151,9 +151,9 @@ describe('Tab Navigation', () => { expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { - const wrapper = shallow(); + const wrapper = mount(); const tableNavigationTab = () => - wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`); + wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); wrapper.setProps({ pageName: SiemPageName.hosts, diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 9a409b9f53d8ce..c62335ea1c06db 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { EuiTab, EuiTabs, EuiLink } from '@elastic/eui'; -import { get, getOr } from 'lodash/fp'; +import { getOr } from 'lodash/fp'; -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import classnames from 'classnames'; import { trackUiAction as track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/track_usage'; -import { HostsTableType } from '../../../store/hosts/model'; import { getSearch } from '../helpers'; import { TabNavigationProps } from './types'; @@ -36,71 +35,51 @@ const TabContainer = styled.div` TabContainer.displayName = 'TabContainer'; -interface TabNavigationState { - selectedTabId: string; -} - -export class TabNavigation extends React.PureComponent { - constructor(props: TabNavigationProps) { - super(props); - const selectedTabId = this.mapLocationToTab(props.pageName, props.tabName); - this.state = { selectedTabId }; - } - public componentWillReceiveProps(nextProps: TabNavigationProps): void { - const selectedTabId = this.mapLocationToTab(nextProps.pageName, nextProps.tabName); - - if (this.state.selectedTabId !== selectedTabId) { - this.setState(prevState => ({ - ...prevState, - selectedTabId, - })); - } - } - public render() { - const { display = 'condensed' } = this.props; - return ( - - {this.renderTabs()} - - ); - } - - public mapLocationToTab = (pageName: string, tabName?: HostsTableType): string => { - const { navTabs } = this.props; +export const TabNavigation = React.memo(props => { + const { display = 'condensed', navTabs, pageName, showBorder, tabName } = props; + const mapLocationToTab = (): string => { return getOr( '', 'id', Object.values(navTabs).find(item => tabName === item.id || pageName === item.id) ); }; + const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab()); + useEffect(() => { + const currentTabSelected = mapLocationToTab(); - private renderTabs = (): JSX.Element[] => { - const { navTabs } = this.props; - return Object.keys(navTabs).map(tabName => { - const tab = get(tabName, navTabs); - return ( - + Object.values(navTabs).map(tab => ( + + - { + track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${tab.id}`); + }} > - { - track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${tab.id}`); - }} - > - {tab.name} - - - - ); - }); - }; -} + {tab.name} + + + + )); + return ( + + {renderTabs()} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/notes/index.tsx b/x-pack/legacy/plugins/siem/public/components/notes/index.tsx index 8eaf368058631b..29f7686ade88b5 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/index.tsx @@ -5,7 +5,7 @@ */ import { EuiInMemoryTable, EuiModalBody, EuiModalHeader, EuiPanel, EuiSpacer } from '@elastic/eui'; -import * as React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { Note } from '../../lib/note'; @@ -23,10 +23,6 @@ interface Props { updateNote: UpdateNote; } -interface State { - newNote: string; -} - const NotesPanel = styled(EuiPanel)` height: ${NOTES_PANEL_HEIGHT}px; width: ${NOTES_PANEL_WIDTH}px; @@ -47,15 +43,9 @@ const InMemoryTable = styled(EuiInMemoryTable)` InMemoryTable.displayName = 'InMemoryTable'; /** A view for entering and reviewing notes */ -export class Notes extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { newNote: '' }; - } - - public render() { - const { associateNote, getNotesByIds, getNewNoteId, noteIds, updateNote } = this.props; +export const Notes = React.memo( + ({ associateNote, getNotesByIds, getNewNoteId, noteIds, updateNote }) => { + const [newNote, setNewNote] = useState(''); return ( @@ -67,8 +57,8 @@ export class Notes extends React.PureComponent { @@ -84,8 +74,6 @@ export class Notes extends React.PureComponent { ); } +); - private updateNewNote = (newNote: string): void => { - this.setState({ newNote }); - }; -} +Notes.displayName = 'Notes'; diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx b/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx index 51992e00313a4a..aa9415aadeda16 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiPanel } from '@elastic/eui'; -import * as React from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { Note } from '../../../lib/note'; @@ -53,27 +53,23 @@ interface Props { updateNote: UpdateNote; } -interface State { - newNote: string; -} - /** A view for entering and reviewing notes */ -export class NoteCards extends React.PureComponent { - constructor(props: Props) { - super(props); - - this.state = { newNote: '' }; - } - - public render() { - const { - getNotesByIds, - getNewNoteId, - noteIds, - showAddNote, - toggleShowAddNote, - updateNote, - } = this.props; +export const NoteCards = React.memo( + ({ + associateNote, + getNotesByIds, + getNewNoteId, + noteIds, + showAddNote, + toggleShowAddNote, + updateNote, + }) => { + const [newNote, setNewNote] = useState(''); + + const associateNoteAndToggleShow = (noteId: string) => { + associateNote(noteId); + toggleShowAddNote(); + }; return ( @@ -90,11 +86,11 @@ export class NoteCards extends React.PureComponent { {showAddNote ? ( @@ -102,13 +98,6 @@ export class NoteCards extends React.PureComponent { ); } +); - private associateNoteAndToggleShow = (noteId: string) => { - this.props.associateNote(noteId); - this.props.toggleShowAddNote(); - }; - - private updateNewNote = (newNote: string): void => { - this.setState({ newNote }); - }; -} +NoteCards.displayName = 'NoteCards'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx index e91feed536f93c..917ec3f1bf0b86 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx @@ -17,7 +17,7 @@ describe('DeleteTimelineModal', () => { ); @@ -34,7 +34,7 @@ describe('DeleteTimelineModal', () => { ); @@ -48,7 +48,7 @@ describe('DeleteTimelineModal', () => { test('it displays `Untitled Timeline` in the title when title is undefined', () => { const wrapper = mountWithIntl( - + ); expect( @@ -61,7 +61,7 @@ describe('DeleteTimelineModal', () => { test('it displays `Untitled Timeline` in the title when title is null', () => { const wrapper = mountWithIntl( - + ); expect( @@ -74,7 +74,7 @@ describe('DeleteTimelineModal', () => { test('it displays `Untitled Timeline` in the title when title is just whitespace', () => { const wrapper = mountWithIntl( - + ); expect( @@ -90,7 +90,7 @@ describe('DeleteTimelineModal', () => { ); @@ -102,14 +102,14 @@ describe('DeleteTimelineModal', () => { ).toEqual(i18n.DELETE_WARNING); }); - test('it invokes toggleShowModal when the Cancel button is clicked', () => { - const toggleShowModal = jest.fn(); + test('it invokes closeModal when the Cancel button is clicked', () => { + const closeModal = jest.fn(); const wrapper = mountWithIntl( ); @@ -118,7 +118,7 @@ describe('DeleteTimelineModal', () => { .first() .simulate('click'); - expect(toggleShowModal).toBeCalled(); + expect(closeModal).toBeCalled(); }); test('it invokes onDelete when the Delete button is clicked', () => { @@ -128,7 +128,7 @@ describe('DeleteTimelineModal', () => { ); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx index 9c416419066e66..e9e438a8c5e2e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx @@ -14,7 +14,7 @@ import * as i18n from '../translations'; interface Props { title?: string | null; onDelete: () => void; - toggleShowModal: () => void; + closeModal: () => void; } export const DELETE_TIMELINE_MODAL_WIDTH = 600; // px @@ -22,7 +22,7 @@ export const DELETE_TIMELINE_MODAL_WIDTH = 600; // px /** * Renders a modal that confirms deletion of a timeline */ -export const DeleteTimelineModal = pure(({ title, toggleShowModal, onDelete }) => ( +export const DeleteTimelineModal = pure(({ title, closeModal, onDelete }) => ( (({ title, toggleShowModal, onDele }} /> } - onCancel={toggleShowModal} + onCancel={closeModal} onConfirm={onDelete} cancelButtonText={i18n.CANCEL} confirmButtonText={i18n.DELETE} diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx index 1700e86f57c844..561eac000bbf76 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx @@ -5,7 +5,6 @@ */ import { EuiButtonIconProps } from '@elastic/eui'; -import { get } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import * as React from 'react'; @@ -79,35 +78,6 @@ describe('DeleteTimelineModal', () => { expect(props.isDisabled).toBe(false); }); - test('it defaults showModal to false until the trash button is clicked', () => { - const wrapper = mountWithIntl( - - ); - - expect(get('showModal', wrapper.state())).toBe(false); - }); - - test('it sets showModal to true when the trash button is clicked', () => { - const wrapper = mountWithIntl( - - ); - - wrapper - .find('[data-test-subj="delete-timeline"]') - .first() - .simulate('click'); - - expect(get('showModal', wrapper.state())).toBe(true); - }); - test('it does NOT render the modal when showModal is false', () => { const wrapper = mountWithIntl( { - constructor(props: Props) { - super(props); +export const DeleteTimelineModalButton = React.memo( + ({ deleteTimelines, savedObjectId, title }) => { + const [showModal, setShowModal] = useState(false); - this.state = { showModal: false }; - } + const openModal = () => setShowModal(true); + const closeModal = () => setShowModal(false); - public render() { - const { deleteTimelines, savedObjectId, title } = this.props; + const onDelete = () => { + if (deleteTimelines != null && savedObjectId != null) { + deleteTimelines([savedObjectId]); + } + closeModal(); + }; return ( <> @@ -44,19 +43,19 @@ export class DeleteTimelineModalButton extends React.PureComponent iconSize="s" iconType="trash" isDisabled={deleteTimelines == null || savedObjectId == null || savedObjectId === ''} - onClick={this.toggleShowModal} + onClick={openModal} size="s" /> - {this.state.showModal ? ( + {showModal ? ( - + @@ -64,20 +63,6 @@ export class DeleteTimelineModalButton extends React.PureComponent ); } +); - private toggleShowModal = () => { - this.setState(state => ({ - showModal: !state.showModal, - })); - }; - - private onDelete = () => { - const { deleteTimelines, savedObjectId } = this.props; - - if (deleteTimelines != null && savedObjectId != null) { - deleteTimelines([savedObjectId]); - } - - this.toggleShowModal(); - }; -} +DeleteTimelineModalButton.displayName = 'DeleteTimelineModalButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx index 62de2ea30542a0..7a0caf14af302e 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx @@ -5,8 +5,7 @@ */ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, ReactWrapper } from 'enzyme'; -import { get } from 'lodash/fp'; +import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; import * as React from 'react'; import { ThemeProvider } from 'styled-components'; @@ -22,21 +21,11 @@ import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; jest.mock('../../lib/settings/use_kibana_ui_setting'); -const getStateChildComponent = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - wrapper: ReactWrapper, React.Component<{}, {}, any>> -): // eslint-disable-next-line @typescript-eslint/no-explicit-any -React.Component<{}, {}, any> => - wrapper - .find('[data-test-subj="stateful-timeline"]') - .last() - .instance(); - describe('StatefulOpenTimeline', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const title = 'All Timelines / Open Timelines'; - test('it has the expected initial state', async () => { + test('it has the expected initial state', () => { const wrapper = mount( @@ -53,15 +42,18 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - wrapper.update(); + const componentProps = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .props(); - expect(getStateChildComponent(wrapper).state).toEqual({ + expect(componentProps).toEqual({ + ...componentProps, itemIdToExpandedNotesRowMap: {}, onlyFavorites: false, pageIndex: 0, pageSize: 10, - search: '', + query: '', selectedItems: [], sortDirection: 'desc', sortField: 'updated', @@ -69,7 +61,7 @@ describe('StatefulOpenTimeline', () => { }); describe('#onQueryChange', () => { - test('it updates the query state with the expected trimmed value when the user enters a query', async () => { + test('it updates the query state with the expected trimmed value when the user enters a query', () => { const wrapper = mount( @@ -85,26 +77,15 @@ describe('StatefulOpenTimeline', () => { ); - - await wait(); - wrapper.update(); - wrapper .find('[data-test-subj="search-bar"] input') .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - wrapper.update(); - - expect(getStateChildComponent(wrapper).state).toEqual({ - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: false, - pageIndex: 0, - pageSize: 10, - search: 'abcd', - selectedItems: [], - sortDirection: 'desc', - sortField: 'updated', - }); + expect( + wrapper + .find('[data-test-subj="search-row"]') + .first() + .prop('query') + ).toEqual('abcd'); }); test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { @@ -129,8 +110,6 @@ describe('StatefulOpenTimeline', () => { .find('[data-test-subj="search-bar"] input') .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - wrapper.update(); - expect( wrapper .find('[data-test-subj="query-message"]') @@ -161,8 +140,6 @@ describe('StatefulOpenTimeline', () => { .find('[data-test-subj="search-bar"] input') .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - wrapper.update(); - expect( wrapper .find('[data-test-subj="selectable-query-text"]') @@ -226,7 +203,6 @@ describe('StatefulOpenTimeline', () => { .find('.euiCheckbox__input') .first() .simulate('change', { target: { checked: true } }); - wrapper.update(); wrapper .find('[data-test-subj="favorite-selected"]') @@ -273,7 +249,6 @@ describe('StatefulOpenTimeline', () => { .find('.euiCheckbox__input') .first() .simulate('change', { target: { checked: true } }); - wrapper.update(); wrapper .find('[data-test-subj="delete-selected"]') @@ -319,14 +294,17 @@ describe('StatefulOpenTimeline', () => { .first() .simulate('change', { target: { checked: true } }); - wrapper.update(); + const selectedItems: [] = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); - expect(get('selectedItems', getStateChildComponent(wrapper).state).length).toEqual(13); // 13 because we did mock 13 timelines in the query + expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query }); }); describe('#onTableChange', () => { - test('it updates the sort state when the user clicks on a column to sort it', async () => { + test('it updates the sort state when the user clicks on a column to sort it', () => { const wrapper = mount( @@ -343,32 +321,29 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper.update(); + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('desc'); wrapper .find('thead tr th button') .at(0) .simulate('click'); - wrapper.update(); - - expect(getStateChildComponent(wrapper).state).toEqual({ - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: false, - pageIndex: 0, - pageSize: 10, - search: '', - selectedItems: [], - sortDirection: 'asc', - sortField: 'updated', - }); + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('asc'); }); }); describe('#onToggleOnlyFavorites', () => { - test('it updates the onlyFavorites state when the user clicks the Only Favorites button', async () => { + test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { const wrapper = mount( @@ -385,25 +360,24 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(false); wrapper .find('[data-test-subj="only-favorites-toggle"]') .first() .simulate('click'); - wrapper.update(); - - expect(getStateChildComponent(wrapper).state).toEqual({ - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: true, - pageIndex: 0, - pageSize: 10, - search: '', - selectedItems: [], - sortDirection: 'desc', - sortField: 'updated', - }); + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(true); }); }); @@ -426,38 +400,38 @@ describe('StatefulOpenTimeline', () => { ); await wait(); - wrapper.update(); + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({}); + wrapper .find('[data-test-subj="expand-notes"]') .first() .simulate('click'); - wrapper.update(); - expect(getStateChildComponent(wrapper).state).toEqual({ - itemIdToExpandedNotesRowMap: { - '10849df0-7b44-11e9-a608-ab3d811609': ( - ({ ...note, savedObjectId: note.noteId }) - ) - : [] - } - /> - ), - }, - onlyFavorites: false, - pageIndex: 0, - pageSize: 10, - search: '', - selectedItems: [], - sortDirection: 'desc', - sortField: 'updated', + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({ + '10849df0-7b44-11e9-a608-ab3d811609': ( + ({ ...note, savedObjectId: note.noteId }) + ) + : [] + } + /> + ), }); }); @@ -487,8 +461,6 @@ describe('StatefulOpenTimeline', () => { .first() .simulate('click'); - wrapper.update(); - expect( wrapper .find('[data-test-subj="note-previews-container"]') @@ -543,25 +515,23 @@ describe('StatefulOpenTimeline', () => { ); - + const getSelectedItem = (): [] => + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); await wait(); - - wrapper.update(); - + expect(getSelectedItem().length).toEqual(0); wrapper .find('.euiCheckbox__input') .first() .simulate('change', { target: { checked: true } }); - wrapper.update(); - + expect(getSelectedItem().length).toEqual(13); wrapper .find('[data-test-subj="delete-selected"]') .first() .simulate('click'); - - wrapper.update(); - - expect(get('selectedItems', getStateChildComponent(wrapper).state).length).toEqual(0); + expect(getSelectedItem().length).toEqual(0); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx index c686228ed31e8f..d101d1f4d39f41 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx @@ -5,7 +5,7 @@ */ import ApolloClient from 'apollo-client'; -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; @@ -43,25 +43,6 @@ import { import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; import { ColumnHeader } from '../timeline/body/column_headers/column_header'; -export interface OpenTimelineState { - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - itemIdToExpandedNotesRowMap: Record; - /** Only query for favorite timelines when true */ - onlyFavorites: boolean; - /** The requested page of results */ - pageIndex: number; - /** The requested size of each page of search results */ - pageSize: number; - /** The current search criteria */ - search: string; - /** The currently-selected timelines in the table */ - selectedItems: OpenTimelineResult[]; - /** The requested sort direction of the query results */ - sortDirection: 'asc' | 'desc'; - /** The requested field to sort on */ - sortField: string; -} - interface OwnProps { apolloClient: ApolloClient; /** Displays open timeline in modal */ @@ -85,70 +66,208 @@ export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): str ); /** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ -export class StatefulOpenTimelineComponent extends React.PureComponent< - OpenTimelineOwnProps, - OpenTimelineState -> { - constructor(props: OpenTimelineOwnProps) { - super(props); +export const StatefulOpenTimelineComponent = React.memo( + ({ + defaultPageSize, + isModal = false, + title, + apolloClient, + closeModalTimeline, + updateTimeline, + updateIsLoading, + timeline, + createNewTimeline, + }) => { + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< + Record + >({}); + /** Only query for favorite timelines when true */ + const [onlyFavorites, setOnlyFavorites] = useState(false); + /** The requested page of results */ + const [pageIndex, setPageIndex] = useState(0); + /** The requested size of each page of search results */ + const [pageSize, setPageSize] = useState(defaultPageSize); + /** The current search criteria */ + const [search, setSearch] = useState(''); + /** The currently-selected timelines in the table */ + const [selectedItems, setSelectedItems] = useState([]); + /** The requested sort direction of the query results */ + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); + /** The requested field to sort on */ + const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + + /** Invoked when the user presses enters to submit the text in the search input */ + const onQueryChange: OnQueryChange = (query: EuiSearchBarQuery) => { + setSearch(query.queryText.trim()); + }; + + /** Focuses the input that filters the field browser */ + const focusInput = () => { + const elements = document.querySelector(`.${OPEN_TIMELINE_CLASS_NAME} input`); - this.state = { - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: false, - search: '', - pageIndex: 0, - pageSize: props.defaultPageSize, - sortField: DEFAULT_SORT_FIELD, - sortDirection: DEFAULT_SORT_DIRECTION, - selectedItems: [], + if (elements != null) { + elements.focus(); + } }; - } - public componentDidMount() { - this.focusInput(); - } + /* This feature will be implemented in the near future, so we are keeping it to know what to do */ + + /** Invoked when the user clicks the action to add the selected timelines to favorites */ + // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { + // const { addTimelinesToFavorites } = this.props; + // const { selectedItems } = this.state; + // if (addTimelinesToFavorites != null) { + // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); + // TODO: it's not possible to clear the selection state of the newly-favorited + // items, because we can't pass the selection state as props to the table. + // See: https://github.com/elastic/eui/issues/1077 + // TODO: the query must re-execute to show the results of the mutation + // } + // }; + + const onDeleteOneTimeline: OnDeleteOneTimeline = (timelineIds: string[]) => { + deleteTimelines(timelineIds, { + search, + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + sort: { + sortField: sortField as SortFieldTimeline, + sortOrder: sortDirection as Direction, + }, + onlyUserFavorite: onlyFavorites, + }); + }; + + /** Invoked when the user clicks the action to delete the selected timelines */ + const onDeleteSelected: OnDeleteSelected = () => { + deleteTimelines(getSelectedTimelineIds(selectedItems), { + search, + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + sort: { + sortField: sortField as SortFieldTimeline, + sortOrder: sortDirection as Direction, + }, + onlyUserFavorite: onlyFavorites, + }); + + // NOTE: we clear the selection state below, but if the server fails to + // delete a timeline, it will remain selected in the table: + resetSelectionState(); + + // TODO: the query must re-execute to show the results of the deletion + }; + + /** Invoked when the user selects (or de-selects) timelines */ + const onSelectionChange: OnSelectionChange = (newSelectedItems: OpenTimelineResult[]) => { + setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 + }; + + /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ + const onTableChange: OnTableChange = ({ page, sort }: OnTableChangeParams) => { + const { index, size } = page; + const { field, direction } = sort; + setPageIndex(index); + setPageSize(size); + setSortDirection(direction); + setSortField(field); + }; + + /** Invoked when the user toggles the option to only view favorite timelines */ + const onToggleOnlyFavorites: OnToggleOnlyFavorites = () => { + setOnlyFavorites(!onlyFavorites); + }; + + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + const onToggleShowNotes: OnToggleShowNotes = ( + newItemIdToExpandedNotesRowMap: Record + ) => { + setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); + }; + + /** Resets the selection state such that all timelines are unselected */ + const resetSelectionState = () => { + setSelectedItems([]); + }; + + const openTimeline: OnOpenTimeline = ({ + duplicate, + timelineId, + }: { + duplicate: boolean; + timelineId: string; + }) => { + if (isModal && closeModalTimeline != null) { + closeModalTimeline(); + } + + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading, + updateTimeline, + }); + }; + + const deleteTimelines: DeleteTimelines = ( + timelineIds: string[], + variables?: AllTimelinesVariables + ) => { + if (timelineIds.includes(timeline.savedObjectId || '')) { + createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + } + apolloClient.mutate({ + mutation: deleteTimelineMutation, + fetchPolicy: 'no-cache', + variables: { id: timelineIds }, + refetchQueries: [ + { + query: allTimelinesQuery, + variables, + }, + ], + }); + }; + useEffect(() => { + focusInput(); + }, []); - public render() { - const { defaultPageSize, isModal = false, title } = this.props; - const { - itemIdToExpandedNotesRowMap, - onlyFavorites, - pageIndex, - pageSize, - search: query, - selectedItems, - sortDirection, - sortField, - } = this.state; return ( {({ timelines, loading, totalCount }) => { return !isModal ? ( ) : ( ); } - - /** Invoked when the user presses enters to submit the text in the search input */ - private onQueryChange: OnQueryChange = (query: EuiSearchBarQuery) => { - this.setState({ - search: query.queryText.trim(), - }); - }; - - /** Focuses the input that filters the field browser */ - private focusInput = () => { - const elements = document.querySelector(`.${OPEN_TIMELINE_CLASS_NAME} input`); - - if (elements != null) { - elements.focus(); - } - }; - - /* This feature will be implemented in the near future, so we are keeping it to know what to do */ - - /** Invoked when the user clicks the action to add the selected timelines to favorites */ - // private onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { - // const { addTimelinesToFavorites } = this.props; - // const { selectedItems } = this.state; - // if (addTimelinesToFavorites != null) { - // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); - // TODO: it's not possible to clear the selection state of the newly-favorited - // items, because we can't pass the selection state as props to the table. - // See: https://github.com/elastic/eui/issues/1077 - // TODO: the query must re-execute to show the results of the mutation - // } - // }; - - private onDeleteOneTimeline: OnDeleteOneTimeline = (timelineIds: string[]) => { - const { onlyFavorites, pageIndex, pageSize, search, sortDirection, sortField } = this.state; - - this.deleteTimelines(timelineIds, { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - }; - - /** Invoked when the user clicks the action to delete the selected timelines */ - private onDeleteSelected: OnDeleteSelected = () => { - const { selectedItems, onlyFavorites } = this.state; - - this.deleteTimelines(getSelectedTimelineIds(selectedItems), { - search: this.state.search, - pageInfo: { - pageIndex: this.state.pageIndex + 1, - pageSize: this.state.pageSize, - }, - sort: { - sortField: this.state.sortField as SortFieldTimeline, - sortOrder: this.state.sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - - // NOTE: we clear the selection state below, but if the server fails to - // delete a timeline, it will remain selected in the table: - this.resetSelectionState(); - - // TODO: the query must re-execute to show the results of the deletion - }; - - /** Invoked when the user selects (or de-selects) timelines */ - private onSelectionChange: OnSelectionChange = (selectedItems: OpenTimelineResult[]) => { - this.setState({ selectedItems }); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 - }; - - /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ - private onTableChange: OnTableChange = ({ page, sort }: OnTableChangeParams) => { - const { index: pageIndex, size: pageSize } = page; - const { field: sortField, direction: sortDirection } = sort; - - this.setState({ - pageIndex, - pageSize, - sortDirection, - sortField, - }); - }; - - /** Invoked when the user toggles the option to only view favorite timelines */ - private onToggleOnlyFavorites: OnToggleOnlyFavorites = () => { - this.setState(state => ({ - onlyFavorites: !state.onlyFavorites, - })); - }; - - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - private onToggleShowNotes: OnToggleShowNotes = ( - itemIdToExpandedNotesRowMap: Record - ) => { - this.setState(() => ({ - itemIdToExpandedNotesRowMap, - })); - }; - - /** Resets the selection state such that all timelines are unselected */ - private resetSelectionState = () => { - this.setState({ - selectedItems: [], - }); - }; - - private openTimeline: OnOpenTimeline = ({ - duplicate, - timelineId, - }: { - duplicate: boolean; - timelineId: string; - }) => { - const { - apolloClient, - closeModalTimeline, - isModal, - updateTimeline, - updateIsLoading, - } = this.props; - - if (isModal && closeModalTimeline != null) { - closeModalTimeline(); - } - - queryTimelineById({ - apolloClient, - duplicate, - timelineId, - updateIsLoading, - updateTimeline, - }); - }; - - private deleteTimelines: DeleteTimelines = ( - timelineIds: string[], - variables?: AllTimelinesVariables - ) => { - if (timelineIds.includes(this.props.timeline.savedObjectId || '')) { - this.props.createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); - } - this.props.apolloClient.mutate< - DeleteTimelineMutation.Mutation, - DeleteTimelineMutation.Variables - >({ - mutation: deleteTimelineMutation, - fetchPolicy: 'no-cache', - variables: { id: timelineIds }, - refetchQueries: [ - { - query: allTimelinesQuery, - variables, - }, - ], - }); - }; -} +); const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx index bcafed20a50ffd..146afa85e10a76 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -5,8 +5,7 @@ */ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { get } from 'lodash/fp'; -import { mount, ReactWrapper } from 'enzyme'; +import { mount } from 'enzyme'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; @@ -20,12 +19,6 @@ import { OpenTimelineModalButton } from '.'; jest.mock('../../../lib/settings/use_kibana_ui_setting'); -const getStateChildComponent = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - wrapper: ReactWrapper, React.Component<{}, {}, any>> -): // eslint-disable-next-line @typescript-eslint/no-explicit-any -React.Component<{}, {}, any> => wrapper.find('[data-test-subj="state-child-component"]').instance(); - describe('OpenTimelineModalButton', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); @@ -56,10 +49,7 @@ describe('OpenTimelineModalButton', () => { - + @@ -69,7 +59,7 @@ describe('OpenTimelineModalButton', () => { wrapper.update(); - expect(get('showModal', getStateChildComponent(wrapper).state)).toEqual(false); + expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(0); }); test('it sets showModal to true when the button is clicked', async () => { @@ -151,10 +141,7 @@ describe('OpenTimelineModalButton', () => { - + @@ -169,7 +156,7 @@ describe('OpenTimelineModalButton', () => { wrapper.update(); - expect(get('showModal', getStateChildComponent(wrapper).state)).toEqual(true); + expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); }); test('it invokes the optional onToggle function provided as a prop when the open timeline button is clicked', async () => { diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx index 79fa747aee0817..41907e07d5c1bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx @@ -5,8 +5,7 @@ */ import { EuiButtonEmpty, EuiModal, EuiOverlayMask } from '@elastic/eui'; -import * as React from 'react'; -import styled from 'styled-components'; +import React, { useState } from 'react'; import { ApolloConsumer } from 'react-apollo'; import * as i18n from '../translations'; @@ -20,90 +19,61 @@ export interface OpenTimelineModalButtonProps { onToggle?: () => void; } -export interface OpenTimelineModalButtonState { - showModal: boolean; -} - const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px -// TODO: this container can be removed when -// the following EUI PR is available (in Kibana): -// https://github.com/elastic/eui/pull/1902/files#diff-d662c14c5dcd7e4b41028bf60b9bc77b -const ModalContainer = styled.div` - .euiModalBody { - display: flex; - flex-direction: column; - } -`; - -ModalContainer.displayName = 'ModalContainer'; - /** * Renders a button that when clicked, displays the `Open Timelines` modal */ -export class OpenTimelineModalButton extends React.PureComponent< - OpenTimelineModalButtonProps, - OpenTimelineModalButtonState -> { - constructor(props: OpenTimelineModalButtonProps) { - super(props); +export const OpenTimelineModalButton = React.memo(({ onToggle }) => { + const [showModal, setShowModal] = useState(false); - this.state = { showModal: false }; + /** shows or hides the `Open Timeline` modal */ + function toggleShowModal() { + if (onToggle != null) { + onToggle(); + } + setShowModal(!showModal); } - public render() { - return ( - - {client => ( - <> - - {i18n.OPEN_TIMELINE} - - - {this.state.showModal && ( - - - - - - - - )} - - )} - - ); + function closeModalTimeline() { + toggleShowModal(); } + return ( + + {client => ( + <> + + {i18n.OPEN_TIMELINE} + - /** shows or hides the `Open Timeline` modal */ - private toggleShowModal = () => { - if (this.props.onToggle != null) { - this.props.onToggle(); - } - - this.setState(state => ({ - showModal: !state.showModal, - })); - }; + {showModal && ( + + + + + + )} + + )} + + ); +}); - private closeModalTimeline = () => { - this.toggleShowModal(); - }; -} +OpenTimelineModalButton.displayName = 'OpenTimelineModalButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.tsx index 693dcf7516bc45..5c0b449916a1f9 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.tsx @@ -7,7 +7,6 @@ import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; @@ -25,15 +24,21 @@ interface Props { filterQueryDraft: KueryFilterQuery; } -class AddToKqlComponent extends React.PureComponent { - public render() { - const { children } = this.props; +const AddToKqlComponent = React.memo( + ({ children, expression, filterQueryDraft, applyFilterQueryFromKueryExpression }) => { + const addToKql = () => { + applyFilterQueryFromKueryExpression( + filterQueryDraft && !isEmpty(filterQueryDraft.expression) + ? `${filterQueryDraft.expression} and ${expression}` + : expression + ); + }; return ( - + } @@ -41,16 +46,9 @@ class AddToKqlComponent extends React.PureComponent { /> ); } +); - private addToKql = () => { - const { expression, filterQueryDraft, applyFilterQueryFromKueryExpression } = this.props; - applyFilterQueryFromKueryExpression( - filterQueryDraft && !isEmpty(filterQueryDraft.expression) - ? `${filterQueryDraft.expression} and ${expression}` - : expression - ); - }; -} +AddToKqlComponent.displayName = 'AddToKqlComponent'; const HoverActionsContainer = styled(EuiPanel)` align-items: center; @@ -75,7 +73,7 @@ interface AddToKqlProps { type: networkModel.NetworkType | hostsModel.HostsType; } -export const AddToKql = pure( +export const AddToKql = React.memo( ({ children, expression, type, componentFilterType, indexPattern }) => { switch (componentFilterType) { case 'hosts': diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx index 65c8e9a6546866..d4b3b5e8759892 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import memoizeOne from 'memoize-one'; -import React from 'react'; +import React, { useMemo } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import { StaticIndexPattern } from 'ui/index_patterns'; @@ -38,8 +37,8 @@ interface OwnProps { data: HostsEdges[]; fakeTotalCount: number; id: string; - isInspect: boolean; indexPattern: StaticIndexPattern; + isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; showMorePagesIndicator: boolean; @@ -49,15 +48,15 @@ interface OwnProps { interface HostsTableReduxProps { activePage: number; + direction: Direction; limit: number; sortField: HostsFields; - direction: Direction; } interface HostsTableDispatchProps { updateHostsSort: ActionCreator<{ - sort: HostsSortField; hostsType: hostsModel.HostsType; + sort: HostsSortField; }>; updateTableActivePage: ActionCreator<{ activePage: number; @@ -65,8 +64,8 @@ interface HostsTableDispatchProps { tableType: hostsModel.HostsTableType; }>; updateTableLimit: ActionCreator<{ - limit: number; hostsType: hostsModel.HostsType; + limit: number; tableType: hostsModel.HostsTableType; }>; } @@ -90,47 +89,58 @@ const rowItems: ItemsPerRow[] = [ numberOfRow: 10, }, ]; +const getSorting = ( + trigger: string, + sortField: HostsFields, + direction: Direction +): SortingBasicTable => ({ field: getNodeField(sortField), direction }); + +const HostsTableComponent = React.memo( + ({ + activePage, + data, + direction, + fakeTotalCount, + id, + indexPattern, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + sortField, + totalCount, + type, + updateHostsSort, + updateTableActivePage, + updateTableLimit, + }) => { + const onChange = (criteria: Criteria) => { + if (criteria.sort != null) { + const sort: HostsSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction, + }; + if (sort.direction !== direction || sort.field !== sortField) { + updateHostsSort({ + sort, + hostsType: type, + }); + } + } + }; -class HostsTableComponent extends React.PureComponent { - private memoizedColumns: ( - type: hostsModel.HostsType, - indexPattern: StaticIndexPattern - ) => HostsTableColumns; - private memoizedSorting: ( - trigger: string, - sortField: HostsFields, - direction: Direction - ) => SortingBasicTable; - - constructor(props: HostsTableProps) { - super(props); - this.memoizedColumns = memoizeOne(this.getMemoizeHostsColumns); - this.memoizedSorting = memoizeOne(this.getSorting); - } + const hostsColumns = useMemo(() => getHostsColumns(type, indexPattern), [type, indexPattern]); - public render() { - const { - activePage, - data, - direction, - fakeTotalCount, - id, - isInspect, - indexPattern, - limit, - loading, - loadPage, - showMorePagesIndicator, - totalCount, + const sorting = useMemo(() => getSorting(`${sortField}-${direction}`, sortField, direction), [ sortField, - type, - updateTableActivePage, - updateTableLimit, - } = this.props; + direction, + ]); + return ( { limit={limit} loading={loading} loadPage={newActivePage => loadPage(newActivePage)} - onChange={this.onChange} + onChange={onChange} pageOfItems={data} showMorePagesIndicator={showMorePagesIndicator} - sorting={this.memoizedSorting(`${sortField}-${direction}`, sortField, direction)} + sorting={sorting} totalCount={fakeTotalCount} updateLimitPagination={newLimit => updateTableLimit({ @@ -163,33 +173,9 @@ class HostsTableComponent extends React.PureComponent { /> ); } +); - private getSorting = ( - trigger: string, - sortField: HostsFields, - direction: Direction - ): SortingBasicTable => ({ field: getNodeField(sortField), direction }); - - private getMemoizeHostsColumns = ( - type: hostsModel.HostsType, - indexPattern: StaticIndexPattern - ): HostsTableColumns => getHostsColumns(type, indexPattern); - - private onChange = (criteria: Criteria) => { - if (criteria.sort != null) { - const sort: HostsSortField = { - field: getSortField(criteria.sort.field), - direction: criteria.sort.direction, - }; - if (sort.direction !== this.props.direction || sort.field !== this.props.sortField) { - this.props.updateHostsSort({ - sort, - hostsType: this.props.type, - }); - } - } - }; -} +HostsTableComponent.displayName = 'HostsTableComponent'; const getSortField = (field: string): HostsFields => { switch (field) { diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx index 24820b637d388e..cf5da3fbebba64 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx @@ -25,7 +25,7 @@ import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_ import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; import { defaultToEmptyTag, getEmptyTagValue } from '../../../empty_value'; import { PreferenceFormattedDate } from '../../../formatted_date'; -import { Columns } from '../../../load_more_table'; +import { Columns } from '../../../paginated_table'; import { LocalizedDateTooltip } from '../../../localized_date_tooltip'; import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; import { PreferenceFormattedBytes } from '../../../formatted_bytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx index 1fdea3f2b03322..353699c5158bc4 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx @@ -12,7 +12,7 @@ import { networkModel } from '../../../../store'; import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; import { defaultToEmptyTag, getEmptyTagValue } from '../../../empty_value'; -import { Columns } from '../../../load_more_table'; +import { Columns } from '../../../paginated_table'; import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; import { PreferenceFormattedBytes } from '../../../formatted_bytes'; import { Provider } from '../../../timeline/data_providers/provider'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx index 38eda9810740c2..97fa601a49af17 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx @@ -21,7 +21,7 @@ import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_ import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../empty_value'; import { IPDetailsLink } from '../../../links'; -import { Columns } from '../../../load_more_table'; +import { Columns } from '../../../paginated_table'; import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; import { Provider } from '../../../timeline/data_providers/provider'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx index 5abbdab9c980fe..714d3f7a8131c6 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx @@ -79,26 +79,45 @@ const rowItems: ItemsPerRow[] = [ export const NetworkTopNFlowTableId = 'networkTopSourceFlow-top-talkers'; -class NetworkTopNFlowTableComponent extends React.PureComponent { - public render() { - const { - activePage, - data, - fakeTotalCount, - flowTargeted, - id, - indexPattern, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - topNFlowSort, - totalCount, - type, - updateTopNFlowLimit, - updateTableActivePage, - } = this.props; +const NetworkTopNFlowTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + flowTargeted, + id, + indexPattern, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + topNFlowSort, + totalCount, + type, + updateTopNFlowLimit, + updateTopNFlowSort, + updateTableActivePage, + }) => { + const onChange = (criteria: Criteria, tableType: networkModel.TopNTableType) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const field = last(splitField); + const newSortDirection = + field !== topNFlowSort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click + const newTopNFlowSort: NetworkTopNFlowSortField = { + field: field as NetworkTopNFlowFields, + direction: newSortDirection, + }; + if (!isEqual(newTopNFlowSort, topNFlowSort)) { + updateTopNFlowSort({ + topNFlowSort: newTopNFlowSort, + networkType: type, + tableType, + }); + } + } + }; let tableType: networkModel.TopNTableType; let headerTitle: string; @@ -136,7 +155,7 @@ class NetworkTopNFlowTableComponent extends React.PureComponent loadPage(newActivePage)} - onChange={criteria => this.onChange(criteria, tableType)} + onChange={criteria => onChange(criteria, tableType)} pageOfItems={data} showMorePagesIndicator={showMorePagesIndicator} sorting={{ field, direction: topNFlowSort.direction }} @@ -153,27 +172,9 @@ class NetworkTopNFlowTableComponent extends React.PureComponent ); } +); - private onChange = (criteria: Criteria, tableType: networkModel.TopNTableType) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const field = last(splitField); - const newSortDirection = - field !== this.props.topNFlowSort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopNFlowSort: NetworkTopNFlowSortField = { - field: field as NetworkTopNFlowFields, - direction: newSortDirection, - }; - if (!isEqual(newTopNFlowSort, this.props.topNFlowSort)) { - this.props.updateTopNFlowSort({ - topNFlowSort: newTopNFlowSort, - networkType: this.props.type, - tableType, - }); - } - } - }; -} +NetworkTopNFlowTableComponent.displayName = 'NetworkTopNFlowTableComponent'; const mapStateToProps = (state: State, ownProps: OwnProps) => networkSelectors.topNFlowSelector(ownProps.flowTargeted); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx index 7578c5decc8519..aea8ee9e6b9e14 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; import { TlsNode } from '../../../../graphql/types'; -import { Columns } from '../../../load_more_table'; +import { Columns } from '../../../paginated_table'; import { getRowItemDraggables, getRowItemDraggable } from '../../../tables/helpers'; import { LocalizedDateTooltip } from '../../../localized_date_tooltip'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx index b17ec74fa05401..2c51fb8f94561a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx @@ -6,7 +6,7 @@ import { FlowTarget, UsersItem } from '../../../../graphql/types'; import { defaultToEmptyTag } from '../../../empty_value'; -import { Columns } from '../../../load_more_table'; +import { Columns } from '../../../paginated_table'; import * as i18n from './translations'; import { getRowItemDraggables, getRowItemDraggable } from '../../../tables/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index b5678a36c1eedf..257ee03c944bfb 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -100,15 +100,17 @@ export interface BasicTableProps { updateActivePage: (activePage: number) => void; updateLimitPagination: (limit: number) => void; } +type Func = (arg: T) => string | number; -export interface Columns { +export interface Columns { + align?: string; field?: string; - name: string | React.ReactNode; + hideForMobile?: boolean; isMobileHeader?: boolean; - sortable?: boolean; + name: string | React.ReactNode; + render?: (item: T, node: U) => React.ReactNode; + sortable?: boolean | Func; truncateText?: boolean; - hideForMobile?: boolean; - render?: (item: T) => void; width?: string; } diff --git a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx index 40df2c134047f2..0a6203056fd20f 100644 --- a/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/resize_handle/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; +import React, { useEffect, useRef } from 'react'; import { fromEvent, Observable, Subscription } from 'rxjs'; import { concatMap, takeUntil } from 'rxjs/operators'; import styled, { css } from 'styled-components'; @@ -41,10 +41,6 @@ interface Props extends ResizeHandleContainerProps { render: (isResizing: boolean) => React.ReactNode; } -interface State { - isResizing: boolean; -} - const ResizeHandleContainer = styled.div` ${({ bottom, height, left, positionAbsolute, right, theme, top }) => css` bottom: ${positionAbsolute && bottom}; @@ -67,88 +63,75 @@ export const removeGlobalResizeCursorStyleFromBody = () => { document.body.classList.remove(globalResizeCursorClassName); }; -export class Resizeable extends React.PureComponent { - private drag$: Observable | null; - private dragEventTargets: Array<{ htmlElement: HTMLElement; prevCursor: string }>; - private dragSubscription: Subscription | null; - private prevX: number = 0; - private ref: React.RefObject; - private upSubscription: Subscription | null; - - constructor(props: Props) { - super(props); - - // NOTE: the ref and observable below are NOT stored in component `State` - this.ref = React.createRef(); - this.drag$ = null; - this.dragSubscription = null; - this.upSubscription = null; - this.dragEventTargets = []; - - this.state = { - isResizing: false, +export const Resizeable = React.memo( + ({ bottom, handle, height, id, left, onResize, positionAbsolute, render, right, top }) => { + const drag$ = useRef | null>(null); + const dragEventTargets = useRef>([]); + const dragSubscription = useRef(null); + const prevX = useRef(0); + const ref = useRef>(React.createRef()); + const upSubscription = useRef(null); + const isResizingRef = useRef(false); + + const calculateDelta = (e: MouseEvent) => { + const deltaX = calculateDeltaX({ prevX: prevX.current, screenX: e.screenX }); + prevX.current = e.screenX; + return deltaX; }; - } - - public componentDidMount() { - const { id, onResize } = this.props; - - const move$ = fromEvent(document, 'mousemove'); - const down$ = fromEvent(this.ref.current!, 'mousedown'); - const up$ = fromEvent(document, 'mouseup'); - - this.drag$ = down$.pipe(concatMap(() => move$.pipe(takeUntil(up$)))); - this.dragSubscription = this.drag$.subscribe(event => { - // We do a feature detection of event.movementX here and if it is missing - // we calculate the delta manually. Browsers IE-11 and Safari will call calculateDelta - const delta = - event.movementX == null || isSafari ? this.calculateDelta(event) : event.movementX; - if (!this.state.isResizing) { - this.setState({ isResizing: true }); - } - onResize({ id, delta }); - if (event.target != null && event.target instanceof HTMLElement) { - const htmlElement: HTMLElement = event.target; - this.dragEventTargets = [ - ...this.dragEventTargets, - { htmlElement, prevCursor: htmlElement.style.cursor }, - ]; - htmlElement.style.cursor = resizeCursorStyle; - } - }); - - this.upSubscription = up$.subscribe(() => { - if (this.state.isResizing) { - this.dragEventTargets.reverse().forEach(eventTarget => { - eventTarget.htmlElement.style.cursor = eventTarget.prevCursor; + useEffect(() => { + const move$ = fromEvent(document, 'mousemove'); + const down$ = fromEvent(ref.current.current!, 'mousedown'); + const up$ = fromEvent(document, 'mouseup'); + + drag$.current = down$.pipe(concatMap(() => move$.pipe(takeUntil(up$)))); + dragSubscription.current = + drag$.current && + drag$.current.subscribe(event => { + // We do a feature detection of event.movementX here and if it is missing + // we calculate the delta manually. Browsers IE-11 and Safari will call calculateDelta + const delta = + event.movementX == null || isSafari ? calculateDelta(event) : event.movementX; + if (!isResizingRef.current) { + isResizingRef.current = true; + } + onResize({ id, delta }); + if (event.target != null && event.target instanceof HTMLElement) { + const htmlElement: HTMLElement = event.target; + dragEventTargets.current = [ + ...dragEventTargets.current, + { htmlElement, prevCursor: htmlElement.style.cursor }, + ]; + htmlElement.style.cursor = resizeCursorStyle; + } }); - this.dragEventTargets = []; - this.setState({ isResizing: false }); - } - }); - } - public componentWillUnmount() { - if (this.dragSubscription != null) { - this.dragSubscription.unsubscribe(); - } - - if (this.upSubscription != null) { - this.upSubscription.unsubscribe(); - } - } - - public render() { - const { bottom, handle, height, left, positionAbsolute, render, right, top } = this.props; + upSubscription.current = up$.subscribe(() => { + if (isResizingRef.current) { + dragEventTargets.current.reverse().forEach(eventTarget => { + eventTarget.htmlElement.style.cursor = eventTarget.prevCursor; + }); + dragEventTargets.current = []; + isResizingRef.current = false; + } + }); + return () => { + if (dragSubscription.current != null) { + dragSubscription.current.unsubscribe(); + } + if (upSubscription.current != null) { + upSubscription.current.unsubscribe(); + } + }; + }, []); return ( <> - {render(this.state.isResizing)} + {render(isResizingRef.current)} { ); } +); - private calculateDelta = (e: MouseEvent) => { - const deltaX = calculateDeltaX({ prevX: this.prevX, screenX: e.screenX }); - - this.prevX = e.screenX; - - return deltaX; - }; -} +Resizeable.displayName = 'Resizeable'; diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index 722cf9db731f72..fa695d76f9f3e5 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -7,13 +7,13 @@ import dateMath from '@elastic/datemath'; import { EuiSuperDatePicker, - EuiSuperDatePickerProps, OnRefreshChangeProps, + EuiSuperDatePickerRecentRange, OnRefreshProps, OnTimeChangeProps, } from '@elastic/eui'; import { getOr, take } from 'lodash/fp'; -import React, { Component } from 'react'; +import React, { useState } from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; @@ -36,33 +36,17 @@ import { InputsRange, Policy } from '../../store/inputs/model'; const MAX_RECENTLY_USED_RANGES = 9; -type MyEuiSuperDatePickerProps = Pick< - EuiSuperDatePickerProps, - | 'end' - | 'isPaused' - | 'onTimeChange' - | 'onRefreshChange' - | 'onRefresh' - | 'recentlyUsedRanges' - | 'refreshInterval' - | 'showUpdateButton' - | 'start' -> & { - isLoading?: boolean; -}; -const MyEuiSuperDatePicker: React.SFC = EuiSuperDatePicker; - interface SuperDatePickerStateRedux { duration: number; - policy: Policy['kind']; - kind: string; - fromStr: string; - toStr: string; - start: number; end: number; + fromStr: string; isLoading: boolean; - queries: inputsModel.GlobalGraphqlQuery[]; + kind: string; kqlQuery: inputsModel.GlobalKqlQuery; + policy: Policy['kind']; + queries: inputsModel.GlobalGraphqlQuery[]; + start: number; + toStr: string; } interface UpdateReduxTime extends OnTimeChangeProps { @@ -85,145 +69,137 @@ type DispatchUpdateReduxTime = ({ }: UpdateReduxTime) => ReturnUpdateReduxTime; interface SuperDatePickerDispatchProps { + setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => void; startAutoReload: ({ id }: { id: InputsModelId }) => void; stopAutoReload: ({ id }: { id: InputsModelId }) => void; - setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => void; updateReduxTime: DispatchUpdateReduxTime; } interface OwnProps { - id: InputsModelId; disabled?: boolean; + id: InputsModelId; timelineId?: string; } -interface TimeArgs { - start: string; - end: string; -} - export type SuperDatePickerProps = OwnProps & SuperDatePickerDispatchProps & SuperDatePickerStateRedux; -export interface SuperDatePickerState { - isQuickSelection: boolean; - recentlyUsedRanges: TimeArgs[]; - showUpdateButton: boolean; -} +export const SuperDatePickerComponent = React.memo( + ({ + duration, + end, + fromStr, + id, + isLoading, + kind, + kqlQuery, + policy, + queries, + setDuration, + start, + startAutoReload, + stopAutoReload, + timelineId, + toStr, + updateReduxTime, + }) => { + const [isQuickSelection, setIsQuickSelection] = useState(true); + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState( + [] + ); + const onRefresh = ({ start: newStart, end: newEnd }: OnRefreshProps): void => { + const { kqlHasBeenUpdated } = updateReduxTime({ + end: newEnd, + id, + isInvalid: false, + isQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const currentStart = formatDate(newStart); + const currentEnd = isQuickSelection + ? formatDate(newEnd, { roundUp: true }) + : formatDate(newEnd); + if ( + !kqlHasBeenUpdated && + (!isQuickSelection || (start === currentStart && end === currentEnd)) + ) { + refetchQuery(queries); + } + }; + + const onRefreshChange = ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { + if (duration !== refreshInterval) { + setDuration({ id, duration: refreshInterval }); + } -export const SuperDatePickerComponent = class extends Component< - SuperDatePickerProps, - SuperDatePickerState -> { - constructor(props: SuperDatePickerProps) { - super(props); + if (isPaused && policy === 'interval') { + stopAutoReload({ id }); + } else if (!isPaused && policy === 'manual') { + startAutoReload({ id }); + } - this.state = { - isQuickSelection: true, - recentlyUsedRanges: [], - showUpdateButton: true, + if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { + refetchQuery(queries); + } + }; + + const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { + newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); }; - } - public render() { - const { duration, end, start, kind, fromStr, policy, toStr, isLoading } = this.props; + const onTimeChange = ({ + start: newStart, + end: newEnd, + isQuickSelection: newIsQuickSelection, + isInvalid, + }: OnTimeChangeProps) => { + if (!isInvalid) { + updateReduxTime({ + end: newEnd, + id, + isInvalid, + isQuickSelection: newIsQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const newRecentlyUsedRanges = [ + { start: newStart, end: newEnd }, + ...take( + MAX_RECENTLY_USED_RANGES, + recentlyUsedRanges.filter( + recentlyUsedRange => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + ), + ]; + + setRecentlyUsedRanges(newRecentlyUsedRanges); + setIsQuickSelection(newIsQuickSelection); + } + }; const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); return ( - ); } - private onRefresh = ({ start, end }: OnRefreshProps): void => { - const { kqlHasBeenUpdated } = this.props.updateReduxTime({ - end, - id: this.props.id, - isInvalid: false, - isQuickSelection: this.state.isQuickSelection, - kql: this.props.kqlQuery, - start, - timelineId: this.props.timelineId, - }); - const currentStart = formatDate(start); - const currentEnd = this.state.isQuickSelection - ? formatDate(end, { roundUp: true }) - : formatDate(end); - if ( - !kqlHasBeenUpdated && - (!this.state.isQuickSelection || - (this.props.start === currentStart && this.props.end === currentEnd)) - ) { - this.refetchQuery(this.props.queries); - } - }; - - private onRefreshChange = ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { - const { id, duration, policy, stopAutoReload, startAutoReload } = this.props; - if (duration !== refreshInterval) { - this.props.setDuration({ id, duration: refreshInterval }); - } - - if (isPaused && policy === 'interval') { - stopAutoReload({ id }); - } else if (!isPaused && policy === 'manual') { - startAutoReload({ id }); - } - - if ( - !isPaused && - (!this.state.isQuickSelection || (this.state.isQuickSelection && this.props.toStr !== 'now')) - ) { - this.refetchQuery(this.props.queries); - } - }; - - private refetchQuery = (queries: inputsModel.GlobalGraphqlQuery[]) => { - queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - private onTimeChange = ({ start, end, isQuickSelection, isInvalid }: OnTimeChangeProps) => { - if (!isInvalid) { - this.props.updateReduxTime({ - end, - id: this.props.id, - isInvalid, - isQuickSelection, - kql: this.props.kqlQuery, - start, - timelineId: this.props.timelineId, - }); - this.setState((prevState: SuperDatePickerState) => { - const recentlyUsedRanges = [ - { start, end }, - ...take( - MAX_RECENTLY_USED_RANGES, - prevState.recentlyUsedRanges.filter( - recentlyUsedRange => - !(recentlyUsedRange.start === start && recentlyUsedRange.end === end) - ) - ), - ]; - - return { - recentlyUsedRanges, - isQuickSelection, - }; - }); - } - }; -}; +); const formatDate = ( date: string, @@ -292,33 +268,35 @@ const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ }; export const makeMapStateToProps = () => { - const getPolicySelector = policySelector(); const getDurationSelector = durationSelector(); - const getKindSelector = kindSelector(); - const getStartSelector = startSelector(); const getEndSelector = endSelector(); const getFromStrSelector = fromStrSelector(); - const getToStrSelector = toStrSelector(); const getIsLoadingSelector = isLoadingSelector(); - const getQueriesSelector = queriesSelector(); + const getKindSelector = kindSelector(); const getKqlQuerySelector = kqlQuerySelector(); + const getPolicySelector = policySelector(); + const getQueriesSelector = queriesSelector(); + const getStartSelector = startSelector(); + const getToStrSelector = toStrSelector(); return (state: State, { id }: OwnProps) => { const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); return { - policy: getPolicySelector(inputsRange), duration: getDurationSelector(inputsRange), - kind: getKindSelector(inputsRange), - start: getStartSelector(inputsRange), end: getEndSelector(inputsRange), fromStr: getFromStrSelector(inputsRange), - toStr: getToStrSelector(inputsRange), isLoading: getIsLoadingSelector(inputsRange), - queries: getQueriesSelector(inputsRange), + kind: getKindSelector(inputsRange), kqlQuery: getKqlQuerySelector(inputsRange), + policy: getPolicySelector(inputsRange), + queries: getQueriesSelector(inputsRange), + start: getStartSelector(inputsRange), + toStr: getToStrSelector(inputsRange), }; }; }; +SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; + const mapDispatchToProps = (dispatch: Dispatch) => ({ startAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.startAutoReload({ id })), diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx index 62f58e1b585d9e..1e603b0c157793 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx @@ -91,53 +91,56 @@ interface Props { } /** Renders a header */ -export class Header extends React.PureComponent { - public render() { - const { header } = this.props; +export const Header = React.memo( + ({ + header, + onColumnRemoved, + onColumnResized, + onColumnSorted, + onFilterChange = noop, + setIsResizing, + sort, + }) => { + const onClick = () => { + onColumnSorted!({ + columnId: header.id, + sortDirection: getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, + }), + }); + }; + + const onResize: OnResize = ({ delta, id }) => { + onColumnResized({ columnId: id, delta }); + }; + + const renderActions = (isResizing: boolean) => { + setIsResizing(isResizing); + return ( + <> + + + + + + + ); + }; return ( } id={header.id} - onResize={this.onResize} + onResize={onResize} positionAbsolute - render={this.renderActions} + render={renderActions} right="-1px" top={0} /> ); } +); - private renderActions = (isResizing: boolean) => { - const { header, onColumnRemoved, onFilterChange = noop, setIsResizing, sort } = this.props; - - setIsResizing(isResizing); - - return ( - <> - - - - - - - ); - }; - - private onClick = () => { - const { header, onColumnSorted, sort } = this.props; - - onColumnSorted!({ - columnId: header.id, - sortDirection: getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }), - }); - }; - - private onResize: OnResize = ({ delta, id }) => { - this.props.onColumnResized({ columnId: id, delta }); - }; -} +Header.displayName = 'Header'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx index 44d0480bc5f28e..2b2401519eb322 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx @@ -22,10 +22,8 @@ interface Props { timelineId: string; } -export class DataDrivenColumns extends React.PureComponent { - public render() { - const { _id, columnHeaders, columnRenderers, data, timelineId } = this.props; - +export const DataDrivenColumns = React.memo( + ({ _id, columnHeaders, columnRenderers, data, timelineId }) => { // Passing the styles directly to the component because the width is // being calculated and is recommended by Styled Components for performance // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 @@ -51,7 +49,9 @@ export class DataDrivenColumns extends React.PureComponent { ); } -} +); + +DataDrivenColumns.displayName = 'DataDrivenColumns'; const getMappedNonEcsValue = ({ data, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx index 1e3f7303c2e1d6..766a75c05f17c6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; -import VisibilitySensor from 'react-visibility-sensor'; +import React, { useEffect, useRef, useState } from 'react'; import uuid from 'uuid'; +import VisibilitySensor from 'react-visibility-sensor'; import { BrowserFields } from '../../../../containers/source'; import { TimelineDetailsComponentQuery } from '../../../../containers/timeline/details'; @@ -35,24 +35,18 @@ interface Props { columnRenderers: ColumnRenderer[]; event: TimelineItem; eventIdToNoteIds: Readonly>; - isEventViewer?: boolean; getNotesByIds: (noteIds: string[]) => Note[]; + isEventViewer?: boolean; + maxDelay?: number; onColumnResized: OnColumnResized; onPinEvent: OnPinEvent; - onUpdateColumns: OnUpdateColumns; onUnPinEvent: OnUnPinEvent; + onUpdateColumns: OnUpdateColumns; pinnedEventIds: Readonly>; rowRenderers: RowRenderer[]; timelineId: string; toggleColumn: (column: ColumnHeader) => void; updateNote: UpdateNote; - maxDelay?: number; -} - -interface State { - expanded: { [eventId: string]: boolean }; - showNotes: { [eventId: string]: boolean }; - initialRender: boolean; } export const getNewNoteId = (): string => uuid.v4(); @@ -105,69 +99,86 @@ const Attributes = React.memo(({ children }) => { ); }); -export class StatefulEvent extends React.Component { - private _isMounted: boolean = false; +export const StatefulEvent = React.memo( + ({ + actionsColumnWidth, + addNoteToEvent, + browserFields, + columnHeaders, + columnRenderers, + event, + eventIdToNoteIds, + getNotesByIds, + isEventViewer = false, + maxDelay = 0, + onColumnResized, + onPinEvent, + onUnPinEvent, + onUpdateColumns, + pinnedEventIds, + rowRenderers, + timelineId, + toggleColumn, + updateNote, + }) => { + const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({}); + const [initialRender, setInitialRender] = useState(false); + const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); - public readonly state: State = { - expanded: {}, - showNotes: {}, - initialRender: false, - }; + const divElement = useRef(null); - public divElement: HTMLDivElement | null = null; + const onToggleShowNotes = (eventId: string): (() => void) => () => { + setShowNotes({ ...showNotes, [eventId]: !showNotes[eventId] }); + }; - /** - * Incrementally loads the events when it mounts by trying to - * see if it resides within a window frame and if it is it will - * indicate to React that it should render its self by setting - * its initialRender to true. - */ - public componentDidMount() { - this._isMounted = true; + const onToggleExpanded = (eventId: string): (() => void) => () => { + setExpanded({ + ...expanded, + [eventId]: !expanded[eventId], + }); + }; - requestIdleCallbackViaScheduler( - () => { - if (!this.state.initialRender && this._isMounted) { - this.setState({ initialRender: true }); - } - }, - { timeout: this.props.maxDelay ? this.props.maxDelay : 0 } - ); - } + const associateNote = ( + eventId: string, + addNoteToEventChild: AddNoteToEvent, + onPinEventChild: OnPinEvent + ): ((noteId: string) => void) => (noteId: string) => { + addNoteToEventChild({ eventId, noteId }); + if (!eventIsPinned({ eventId, pinnedEventIds })) { + onPinEventChild(eventId); // pin the event, because it has notes + } + }; - componentWillUnmount() { - this._isMounted = false; - } + /** + * Incrementally loads the events when it mounts by trying to + * see if it resides within a window frame and if it is it will + * indicate to React that it should render its self by setting + * its initialRender to true. + */ - public render() { - const { - actionsColumnWidth, - addNoteToEvent, - browserFields, - columnHeaders, - columnRenderers, - event, - eventIdToNoteIds, - getNotesByIds, - isEventViewer = false, - onColumnResized, - onPinEvent, - onUpdateColumns, - onUnPinEvent, - pinnedEventIds, - rowRenderers, - timelineId, - toggleColumn, - updateNote, - } = this.props; + useEffect(() => { + let _isMounted = true; + + requestIdleCallbackViaScheduler( + () => { + if (!initialRender && _isMounted) { + setInitialRender(true); + } + }, + { timeout: maxDelay } + ); + return () => { + _isMounted = false; + }; + }, []); // Number of current columns plus one for actions. const columnCount = columnHeaders.length + 1; // If we are not ready to render yet, just return null - // see componentDidMount() for when it schedules the first + // see useEffect() for when it schedules the first // time this stateful component should be rendered. - if (!this.state.initialRender) { + if (!initialRender) { return ; } @@ -184,7 +195,7 @@ export class StatefulEvent extends React.Component { sourceId="default" indexName={event._index!} eventId={event._id} - executeQuery={!!this.state.expanded[event._id]} + executeQuery={!!expanded[event._id]} > {({ detailsData, loading }) => ( { data-test-subj="event" innerRef={c => { if (c != null) { - this.divElement = c; + divElement.current = c; } }} > @@ -201,26 +212,26 @@ export class StatefulEvent extends React.Component { data: event.ecs, children: ( ), @@ -231,9 +242,9 @@ export class StatefulEvent extends React.Component { { } else { // Height place holder for visibility detection as well as re-rendering sections. const height = - this.divElement != null ? this.divElement.clientHeight + 'px' : DEFAULT_ROW_HEIGHT; + divElement.current != null + ? `${divElement.current.clientHeight}px` + : DEFAULT_ROW_HEIGHT; // height is being inlined directly in here because of performance with StyledComponents // involving quick and constant changes to the DOM. @@ -257,33 +270,6 @@ export class StatefulEvent extends React.Component { ); } +); - private onToggleShowNotes = (eventId: string): (() => void) => () => { - this.setState(state => ({ - showNotes: { - ...state.showNotes, - [eventId]: !state.showNotes[eventId], - }, - })); - }; - - private onToggleExpanded = (eventId: string): (() => void) => () => { - this.setState(state => ({ - expanded: { - ...state.expanded, - [eventId]: !state.expanded[eventId], - }, - })); - }; - - private associateNote = ( - eventId: string, - addNoteToEvent: AddNoteToEvent, - onPinEvent: OnPinEvent - ): ((noteId: string) => void) => (noteId: string) => { - addNoteToEvent({ eventId, noteId }); - if (!eventIsPinned({ eventId, pinnedEventIds: this.props.pinnedEventIds })) { - onPinEvent(eventId); // pin the event, because it has notes - } - }; -} +StatefulEvent.displayName = 'StatefulEvent'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx index 871a60d18404ad..d93446b2af95b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx @@ -84,8 +84,10 @@ type StatefulBodyComponentProps = OwnProps & ReduxProps & DispatchProps; export const emptyColumnHeaders: ColumnHeader[] = []; -class StatefulBodyComponent extends React.Component { - public shouldComponentUpdate({ +const StatefulBodyComponent = React.memo( + ({ + addNoteToEvent, + applyDeltaToColumnWidth, browserFields, columnHeaders, data, @@ -93,45 +95,46 @@ class StatefulBodyComponent extends React.Component getNotesByIds, height, id, - isEventViewer, + isEventViewer = false, + pinEvent, pinnedEventIds, range, + removeColumn, sort, - }: StatefulBodyComponentProps) { - return ( - browserFields !== this.props.browserFields || - columnHeaders !== this.props.columnHeaders || - data !== this.props.data || - eventIdToNoteIds !== this.props.eventIdToNoteIds || - getNotesByIds !== this.props.getNotesByIds || - height !== this.props.height || - id !== this.props.id || - isEventViewer !== this.props.isEventViewer || - pinnedEventIds !== this.props.pinnedEventIds || - range !== this.props.range || - sort !== this.props.sort - ); - } + toggleColumn, + unPinEvent, + updateColumns, + updateNote, + updateSort, + }) => { + const onAddNoteToEvent: AddNoteToEvent = ({ + eventId, + noteId, + }: { + eventId: string; + noteId: string; + }) => addNoteToEvent!({ id, eventId, noteId }); + + const onColumnSorted: OnColumnSorted = sorted => { + updateSort!({ id, sort: sorted }); + }; - public render() { - const { - browserFields, - columnHeaders, - data, - eventIdToNoteIds, - getNotesByIds, - height, - id, - isEventViewer = false, - pinnedEventIds, - range, - sort, - toggleColumn, - } = this.props; + const onColumnRemoved: OnColumnRemoved = columnId => removeColumn!({ id, columnId }); + + const onColumnResized: OnColumnResized = ({ columnId, delta }) => + applyDeltaToColumnWidth!({ id, columnId, delta }); + + const onPinEvent: OnPinEvent = eventId => pinEvent!({ id, eventId }); + + const onUnPinEvent: OnUnPinEvent = eventId => unPinEvent!({ id, eventId }); + + const onUpdateNote: UpdateNote = (note: Note) => updateNote!({ note }); + + const onUpdateColumns: OnUpdateColumns = columns => updateColumns!({ id, columns }); return ( height={height} id={id} isEventViewer={isEventViewer} - onColumnResized={this.onColumnResized} - onColumnRemoved={this.onColumnRemoved} - onColumnSorted={this.onColumnSorted} + onColumnRemoved={onColumnRemoved} + onColumnResized={onColumnResized} + onColumnSorted={onColumnSorted} onFilterChange={noop} // TODO: this is the callback for column filters, which is out scope for this phase of delivery - onPinEvent={this.onPinEvent} - onUpdateColumns={this.onUpdateColumns} - onUnPinEvent={this.onUnPinEvent} + onPinEvent={onPinEvent} + onUnPinEvent={onUnPinEvent} + onUpdateColumns={onUpdateColumns} pinnedEventIds={pinnedEventIds} range={range!} rowRenderers={rowRenderers} sort={sort} toggleColumn={toggleColumn} - updateNote={this.onUpdateNote} + updateNote={onUpdateNote} /> ); + }, + (prevProps, nextProps) => { + return ( + prevProps.browserFields === nextProps.browserFields && + prevProps.columnHeaders === nextProps.columnHeaders && + prevProps.data === nextProps.data && + prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && + prevProps.getNotesByIds === nextProps.getNotesByIds && + prevProps.height === nextProps.height && + prevProps.id === nextProps.id && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.pinnedEventIds === nextProps.pinnedEventIds && + prevProps.range === nextProps.range && + prevProps.sort === nextProps.sort + ); } +); - private onAddNoteToEvent: AddNoteToEvent = ({ - eventId, - noteId, - }: { - eventId: string; - noteId: string; - }) => this.props.addNoteToEvent!({ id: this.props.id, eventId, noteId }); - - private onColumnSorted: OnColumnSorted = sorted => { - this.props.updateSort!({ id: this.props.id, sort: sorted }); - }; - - private onColumnRemoved: OnColumnRemoved = columnId => - this.props.removeColumn!({ id: this.props.id, columnId }); - - private onColumnResized: OnColumnResized = ({ columnId, delta }) => - this.props.applyDeltaToColumnWidth!({ id: this.props.id, columnId, delta }); - - private onPinEvent: OnPinEvent = eventId => this.props.pinEvent!({ id: this.props.id, eventId }); - - private onUnPinEvent: OnUnPinEvent = eventId => - this.props.unPinEvent!({ id: this.props.id, eventId }); - - private onUpdateNote: UpdateNote = (note: Note) => this.props.updateNote!({ note }); - - private onUpdateColumns: OnUpdateColumns = columns => - this.props.updateColumns!({ id: this.props.id, columns }); -} +StatefulBodyComponent.displayName = 'StatefulBodyComponent'; const makeMapStateToProps = () => { const memoizedColumnHeaders: ( @@ -201,9 +193,9 @@ const makeMapStateToProps = () => { return { columnHeaders: memoizedColumnHeaders(columns, browserFields), - id, eventIdToNoteIds, getNotesByIds: getNotesByIds(state), + id, pinnedEventIds, }; }; @@ -215,12 +207,12 @@ export const StatefulBody = connect( { addNoteToEvent: timelineActions.addNoteToEvent, applyDeltaToColumnWidth: timelineActions.applyDeltaToColumnWidth, - unPinEvent: timelineActions.unPinEvent, - updateColumns: timelineActions.updateColumns, - updateSort: timelineActions.updateSort, pinEvent: timelineActions.pinEvent, removeColumn: timelineActions.removeColumn, removeProvider: timelineActions.removeProvider, + unPinEvent: timelineActions.unPinEvent, + updateColumns: timelineActions.updateColumns, updateNote: appActions.updateNote, + updateSort: timelineActions.updateSort, } )(StatefulBodyComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx index 29417bd0b578b4..98cf0a78b1d1f8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx @@ -5,7 +5,7 @@ */ import { noop } from 'lodash/fp'; -import React, { PureComponent } from 'react'; +import React, { useState } from 'react'; import { BrowserFields } from '../../../containers/source'; @@ -32,30 +32,42 @@ interface ProviderItemBadgeProps { val: string | number; } -interface OwnState { - isPopoverOpen: boolean; -} +export const ProviderItemBadge = React.memo( + ({ + andProviderId, + browserFields, + deleteProvider, + field, + kqlQuery, + isEnabled, + isExcluded, + onDataProviderEdited, + operator, + providerId, + timelineId, + toggleEnabledProvider, + toggleExcludedProvider, + val, + }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + function togglePopover() { + setIsPopoverOpen(!isPopoverOpen); + } -export class ProviderItemBadge extends PureComponent { - public readonly state = { - isPopoverOpen: false, - }; + function closePopover() { + setIsPopoverOpen(false); + } - public render() { - const { - andProviderId, - browserFields, - deleteProvider, - field, - kqlQuery, - isEnabled, - isExcluded, - onDataProviderEdited, - operator, - providerId, - timelineId, - val, - } = this.props; + function onToggleEnabledProvider() { + toggleEnabledProvider(); + closePopover(); + } + + function onToggleExcludedProvider() { + toggleExcludedProvider(); + closePopover(); + } return ( @@ -71,51 +83,31 @@ export class ProviderItemBadge extends PureComponent } - closePopover={this.closePopover} + closePopover={closePopover} deleteProvider={deleteProvider} field={field} kqlQuery={kqlQuery} isEnabled={isEnabled} isExcluded={isExcluded} isLoading={isLoading} - isOpen={this.state.isPopoverOpen} + isOpen={isPopoverOpen} onDataProviderEdited={onDataProviderEdited} operator={operator} providerId={providerId} timelineId={timelineId} - toggleEnabledProvider={this.toggleEnabledProvider} - toggleExcludedProvider={this.toggleExcludedProvider} + toggleEnabledProvider={onToggleEnabledProvider} + toggleExcludedProvider={onToggleExcludedProvider} value={val} /> )} ); } +); - private togglePopover = () => { - this.setState(prevState => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - private closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; - - private toggleEnabledProvider = () => { - this.props.toggleEnabledProvider(); - this.closePopover(); - }; - - private toggleExcludedProvider = () => { - this.props.toggleExcludedProvider(); - this.closePopover(); - }; -} +ProviderItemBadge.displayName = 'ProviderItemBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx index 79f85103077b70..6e8a0e8cfb17fc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx @@ -11,7 +11,7 @@ import * as React from 'react'; import { TestProviders } from '../../../mock/test_providers'; -import { Footer } from './index'; +import { Footer, PagingControl } from './index'; import { mockData } from './mock'; describe('Footer Timeline Component', () => { @@ -93,38 +93,36 @@ describe('Footer Timeline Component', () => { }); test('it renders the Loading... in the more load button when fetching new data', () => { - const wrapper = mount( - -