diff --git a/docs/development/core/server/kibana-plugin-server.authnothandled.md b/docs/development/core/server/kibana-plugin-server.authnothandled.md new file mode 100644 index 0000000000000..01e465c266319 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authnothandled.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthNotHandled](./kibana-plugin-server.authnothandled.md) + +## AuthNotHandled interface + + +Signature: + +```typescript +export interface AuthNotHandled +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-server.authnothandled.type.md) | AuthResultType.notHandled | | + diff --git a/docs/development/core/server/kibana-plugin-server.authnothandled.type.md b/docs/development/core/server/kibana-plugin-server.authnothandled.type.md new file mode 100644 index 0000000000000..81543de0ec61b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authnothandled.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthNotHandled](./kibana-plugin-server.authnothandled.md) > [type](./kibana-plugin-server.authnothandled.type.md) + +## AuthNotHandled.type property + +Signature: + +```typescript +type: AuthResultType.notHandled; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authredirected.md b/docs/development/core/server/kibana-plugin-server.authredirected.md new file mode 100644 index 0000000000000..3eb88d6c5a230 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirected.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirected](./kibana-plugin-server.authredirected.md) + +## AuthRedirected interface + + +Signature: + +```typescript +export interface AuthRedirected extends AuthRedirectedParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-server.authredirected.type.md) | AuthResultType.redirected | | + diff --git a/docs/development/core/server/kibana-plugin-server.authredirected.type.md b/docs/development/core/server/kibana-plugin-server.authredirected.type.md new file mode 100644 index 0000000000000..866ed358119e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirected.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirected](./kibana-plugin-server.authredirected.md) > [type](./kibana-plugin-server.authredirected.type.md) + +## AuthRedirected.type property + +Signature: + +```typescript +type: AuthResultType.redirected; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authredirectedparams.headers.md b/docs/development/core/server/kibana-plugin-server.authredirectedparams.headers.md new file mode 100644 index 0000000000000..c1cf8218e7509 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirectedparams.headers.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) > [headers](./kibana-plugin-server.authredirectedparams.headers.md) + +## AuthRedirectedParams.headers property + +Headers to attach for auth redirect. Must include "location" header + +Signature: + +```typescript +headers: { + location: string; + } & ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authredirectedparams.md b/docs/development/core/server/kibana-plugin-server.authredirectedparams.md new file mode 100644 index 0000000000000..3658f88fb6495 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authredirectedparams.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) + +## AuthRedirectedParams interface + +Result of auth redirection. + +Signature: + +```typescript +export interface AuthRedirectedParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.authredirectedparams.headers.md) | {
location: string;
} & ResponseHeaders | Headers to attach for auth redirect. Must include "location" header | + diff --git a/docs/development/core/server/kibana-plugin-server.authresult.md b/docs/development/core/server/kibana-plugin-server.authresult.md index 8739c4899bd02..f540173f34c7c 100644 --- a/docs/development/core/server/kibana-plugin-server.authresult.md +++ b/docs/development/core/server/kibana-plugin-server.authresult.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AuthResult = Authenticated; +export declare type AuthResult = Authenticated | AuthNotHandled | AuthRedirected; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultparams.md b/docs/development/core/server/kibana-plugin-server.authresultparams.md index 55b247f21f5a9..7a725cb340f5b 100644 --- a/docs/development/core/server/kibana-plugin-server.authresultparams.md +++ b/docs/development/core/server/kibana-plugin-server.authresultparams.md @@ -4,7 +4,7 @@ ## AuthResultParams interface -Result of an incoming request authentication. +Result of successful authentication. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.authresulttype.md b/docs/development/core/server/kibana-plugin-server.authresulttype.md index 61a98ee5e7b11..48c159a94c23d 100644 --- a/docs/development/core/server/kibana-plugin-server.authresulttype.md +++ b/docs/development/core/server/kibana-plugin-server.authresulttype.md @@ -16,4 +16,6 @@ export declare enum AuthResultType | Member | Value | Description | | --- | --- | --- | | authenticated | "authenticated" | | +| notHandled | "notHandled" | | +| redirected | "redirected" | | diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md index bc7003c5a68f3..a6a30dae894ad 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -17,4 +17,6 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | | [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | +| [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true | +| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (headers: {
location: string;
} & ResponseHeaders) => AuthResult | Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' | diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.nothandled.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.nothandled.md new file mode 100644 index 0000000000000..7de174b3c7bb6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.nothandled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [notHandled](./kibana-plugin-server.authtoolkit.nothandled.md) + +## AuthToolkit.notHandled property + +User has no credentials. Allows user to access a resource when authRequired: 'optional' Rejects a request when authRequired: true + +Signature: + +```typescript +notHandled: () => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md new file mode 100644 index 0000000000000..64d1d04a4abc0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.redirected.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthToolkit](./kibana-plugin-server.authtoolkit.md) > [redirected](./kibana-plugin-server.authtoolkit.redirected.md) + +## AuthToolkit.redirected property + +Redirect user to IdP when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' + +Signature: + +```typescript +redirected: (headers: { + location: string; + } & ResponseHeaders) => AuthResult; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.auth.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.auth.md new file mode 100644 index 0000000000000..536d6bd04d937 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.auth.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [auth](./kibana-plugin-server.kibanarequest.auth.md) + +## KibanaRequest.auth property + +Signature: + +```typescript +readonly auth: { + isAuthenticated: boolean; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index cb6745623e381..0d520783fd4cf 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -22,6 +22,7 @@ export declare class KibanaRequest{
isAuthenticated: boolean;
} | | | [body](./kibana-plugin-server.kibanarequest.body.md) | | Body | | | [events](./kibana-plugin-server.kibanarequest.events.md) | | KibanaRequestEvents | Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) | | [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | Readonly copy of incoming request headers. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index e843ffb265b82..c84585bf6cb65 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -53,7 +53,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AssistanceAPIResponse](./kibana-plugin-server.assistanceapiresponse.md) | | | [AssistantAPIClientParams](./kibana-plugin-server.assistantapiclientparams.md) | | | [Authenticated](./kibana-plugin-server.authenticated.md) | | -| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of an incoming request authentication. | +| [AuthNotHandled](./kibana-plugin-server.authnothandled.md) | | +| [AuthRedirected](./kibana-plugin-server.authredirected.md) | | +| [AuthRedirectedParams](./kibana-plugin-server.authredirectedparams.md) | Result of auth redirection. | +| [AuthResultParams](./kibana-plugin-server.authresultparams.md) | Result of successful authentication. | | [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [Capabilities](./kibana-plugin-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md index e4cbca9c97810..830abd4dde738 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.authrequired.md @@ -4,12 +4,12 @@ ## RouteConfigOptions.authRequired property -A flag shows that authentication for a route: `enabled` when true `disabled` when false +Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. -Enabled by default. +Defaults to `true` if an auth mechanism is registered. Signature: ```typescript -authRequired?: boolean; +authRequired?: boolean | 'optional'; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index 7fbab90cc2c8a..6664a28424a32 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -16,7 +16,7 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible.Defaults to true if an auth mechanism is registered. | | [body](./kibana-plugin-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | | [xsrfRequired](./kibana-plugin-server.routeconfigoptions.xsrfrequired.md) | Method extends 'get' ? never : boolean | Defines xsrf protection requirements for a route: - true. Requires an incoming POST/PUT/DELETE request to contain kbn-xsrf header. - false. Disables xsrf protection.Set to true by default | diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 72ff9162ffe6c..1531c1d22b01b 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -19,6 +19,7 @@ const { resolve } = require('path'); const webpack = require('webpack'); +const { stringifyRequest } = require('loader-utils'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const { REPO_ROOT, DLL_DIST_DIR } = require('../lib/constants'); // eslint-disable-next-line import/no-unresolved @@ -72,6 +73,38 @@ module.exports = async ({ config }) => { ], }); + // Enable SASS + config.module.rules.push({ + test: /\.scss$/, + exclude: /\.module.(s(a|c)ss)$/, + use: [ + { loader: 'style-loader' }, + { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'postcss-loader', + options: { + config: { + path: resolve(REPO_ROOT, 'src/optimize/'), + }, + }, + }, + { + loader: 'sass-loader', + options: { + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + )};\n`; + }, + sassOptions: { + includePaths: [resolve(REPO_ROOT, 'node_modules')], + }, + }, + }, + ], + }); + // Reference the built DLL file of static(ish) dependencies, which are removed // during kbn:bootstrap and rebuilt if missing. config.plugins.push( @@ -96,7 +129,7 @@ module.exports = async ({ config }) => { ); // Tell Webpack about the ts/x extensions - config.resolve.extensions.push('.ts', '.tsx'); + config.resolve.extensions.push('.ts', '.tsx', '.scss'); // Load custom Webpack config specified by a plugin. if (currentConfig.webpackHook) { diff --git a/src/cli_plugin/install/cleanup.js b/src/cli_plugin/install/cleanup.js index fa4bdcf4f6966..eaa25962ef0e4 100644 --- a/src/cli_plugin/install/cleanup.js +++ b/src/cli_plugin/install/cleanup.js @@ -27,7 +27,7 @@ export function cleanPrevious(settings, logger) { logger.log('Found previous install attempt. Deleting...'); try { - del.sync(settings.workingPath); + del.sync(settings.workingPath, { force: true }); } catch (e) { reject(e); } diff --git a/src/cli_plugin/install/install.js b/src/cli_plugin/install/install.js index 5a341e67dc128..92be2ac250320 100644 --- a/src/cli_plugin/install/install.js +++ b/src/cli_plugin/install/install.js @@ -46,7 +46,7 @@ export default async function install(settings, logger) { await extract(settings, logger); - del.sync(settings.tempArchiveFile); + del.sync(settings.tempArchiveFile, { force: true }); existingInstall(settings, logger); diff --git a/src/cli_plugin/remove/remove.js b/src/cli_plugin/remove/remove.js index 8432d0f44836b..353e592390ff4 100644 --- a/src/cli_plugin/remove/remove.js +++ b/src/cli_plugin/remove/remove.js @@ -37,7 +37,7 @@ export default function remove(settings, logger) { } logger.log(`Removing ${settings.plugin}...`); - del.sync(settings.pluginPath); + del.sync(settings.pluginPath, { force: true }); logger.log('Plugin removal complete'); } catch (err) { logger.error(`Unable to remove plugin because of error: "${err.message}"`); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 4dd6bedfa4f0c..c5e649f7d9d5c 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1178,10 +1178,10 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | `ui/index_patterns` | `data.indexPatterns` | still in progress | | `ui/registry/field_formats` | `data.fieldFormats` | | | `ui/registry/feature_catalogue` | `home.featureCatalogue.register` | Must add `home` as a dependency in your kibana.json. | -| `ui/registry/vis_types` | `visualizations.types` | -- | -| `ui/vis` | `visualizations.types` | -- | +| `ui/registry/vis_types` | `visualizations` | -- | +| `ui/vis` | `visualizations` | -- | | `ui/share` | `share` | `showShareContextMenu` is now called `toggleShareContextMenu`, `ShareContextMenuExtensionsRegistryProvider` is now called `register` | -| `ui/vis/vis_factory` | `visualizations.types` | -- | +| `ui/vis/vis_factory` | `visualizations` | -- | | `ui/vis/vis_filters` | `visualizations.filters` | -- | | `ui/utils/parse_es_interval` | `import { parseEsInterval } from '../data/public'` | `parseEsInterval`, `ParsedInterval`, `InvalidEsCalendarIntervalError`, `InvalidEsIntervalFormatError` items were moved to the `Data Plugin` as a static code | diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 05d718e1073df..d602422c14634 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -37,8 +37,7 @@ export interface CapabilitiesStart { */ export class CapabilitiesService { public async start({ appIds, http }: StartDeps): Promise { - const route = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/defaults' : ''; - const capabilities = await http.post(`/api/core/capabilities${route}`, { + const capabilities = await http.post('/api/core/capabilities', { body: JSON.stringify({ applications: appIds }), }); diff --git a/src/core/server/capabilities/capabilities_service.test.ts b/src/core/server/capabilities/capabilities_service.test.ts index aace0b9debf9c..7d2e7391aa8d4 100644 --- a/src/core/server/capabilities/capabilities_service.test.ts +++ b/src/core/server/capabilities/capabilities_service.test.ts @@ -41,8 +41,8 @@ describe('CapabilitiesService', () => { }); it('registers the capabilities routes', async () => { - expect(http.createRouter).toHaveBeenCalledWith('/api/core/capabilities'); - expect(router.post).toHaveBeenCalledTimes(2); + expect(http.createRouter).toHaveBeenCalledWith(''); + expect(router.post).toHaveBeenCalledTimes(1); expect(router.post).toHaveBeenCalledWith(expect.any(Object), expect.any(Function)); }); diff --git a/src/core/server/capabilities/routes/index.ts b/src/core/server/capabilities/routes/index.ts index ccaa4621d7003..74c485986a77b 100644 --- a/src/core/server/capabilities/routes/index.ts +++ b/src/core/server/capabilities/routes/index.ts @@ -22,6 +22,6 @@ import { InternalHttpServiceSetup } from '../../http'; import { registerCapabilitiesRoutes } from './resolve_capabilities'; export function registerRoutes(http: InternalHttpServiceSetup, resolver: CapabilitiesResolver) { - const router = http.createRouter('/api/core/capabilities'); + const router = http.createRouter(''); registerCapabilitiesRoutes(router, resolver); } diff --git a/src/core/server/capabilities/routes/resolve_capabilities.ts b/src/core/server/capabilities/routes/resolve_capabilities.ts index 5e1d49b4b1b7e..3fb1bb3d13d0b 100644 --- a/src/core/server/capabilities/routes/resolve_capabilities.ts +++ b/src/core/server/capabilities/routes/resolve_capabilities.ts @@ -22,30 +22,24 @@ import { IRouter } from '../../http'; import { CapabilitiesResolver } from '../resolve_capabilities'; export function registerCapabilitiesRoutes(router: IRouter, resolver: CapabilitiesResolver) { - // Capabilities are fetched on both authenticated and anonymous routes. - // However when `authRequired` is false, authentication is not performed - // and only default capabilities are returned (all disabled), even for authenticated users. - // So we need two endpoints to handle both scenarios. - [true, false].forEach(authRequired => { - router.post( - { - path: authRequired ? '' : '/defaults', - options: { - authRequired, - }, - validate: { - body: schema.object({ - applications: schema.arrayOf(schema.string()), - }), - }, + router.post( + { + path: '/api/core/capabilities', + options: { + authRequired: 'optional', }, - async (ctx, req, res) => { - const { applications } = req.body; - const capabilities = await resolver(req, applications); - return res.ok({ - body: capabilities, - }); - } - ); - }); + validate: { + body: schema.object({ + applications: schema.arrayOf(schema.string()), + }), + }, + }, + async (ctx, req, res) => { + const { applications } = req.body; + const capabilities = await resolver(req, applications); + return res.ok({ + body: capabilities, + }); + } + ); } diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 741c723ca9365..bbef0a105c089 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -36,6 +36,7 @@ import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; interface RequestFixtureOptions

{ + auth?: { isAuthenticated: boolean }; headers?: Record; params?: Record; body?: Record; @@ -65,11 +66,13 @@ function createKibanaRequestMock

({ routeAuthRequired, validation = {}, kibanaRouteState = { xsrfRequired: true }, + auth = { isAuthenticated: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); return KibanaRequest.from( createRawRequestMock({ + auth, headers, params, query, @@ -113,6 +116,9 @@ function createRawRequestMock(customization: DeepPartial = {}) { {}, { app: { xsrfRequired: true } as any, + auth: { + isAuthenticated: true, + }, headers: {}, path: '/', route: { settings: {} }, diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index cffdffab0d0cf..f898ed0ea1a99 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -26,8 +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 { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; - -import { IRouter, KibanaRouteState, isSafeMethod } from './router'; +import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -148,7 +147,7 @@ export class HttpServer { this.log.debug(`registering route handler for [${route.path}]`); // Hapi does not allow payload validation to be specified for 'head' or 'get' requests const validate = isSafeMethod(route.method) ? undefined : { payload: true }; - const { authRequired = true, tags, body = {} } = route.options; + const { authRequired, tags, body = {} } = route.options; const { accepts: allow, maxBytes, output, parse } = body; const kibanaRouteState: KibanaRouteState = { @@ -160,8 +159,7 @@ export class HttpServer { method: route.method, path: route.path, options: { - // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }` - auth: authRequired === true ? undefined : false, + auth: this.getAuthOption(authRequired), app: kibanaRouteState, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. @@ -196,6 +194,22 @@ export class HttpServer { this.server = undefined; } + private getAuthOption( + authRequired: RouteConfigOptions['authRequired'] = true + ): undefined | false | { mode: 'required' | 'optional' } { + if (this.authRegistered === false) return undefined; + + if (authRequired === true) { + return { mode: 'required' }; + } + if (authRequired === 'optional') { + return { mode: 'optional' }; + } + if (authRequired === false) { + return false; + } + } + private setupBasePathRewrite(config: HttpConfig, basePathService: BasePath) { if (config.basePath === undefined || !config.rewriteBasePath) { return; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 30032ff5da796..442bc93190d86 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -115,6 +115,8 @@ const createOnPostAuthToolkitMock = (): jest.Mocked => ({ const createAuthToolkitMock = (): jest.Mocked => ({ authenticated: jest.fn(), + notHandled: jest.fn(), + redirected: jest.fn(), }); const createOnPreResponseToolkitMock = (): jest.Mocked => ({ diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 8f4c02680f8a3..a75eb04fa0120 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -67,9 +67,12 @@ export { AuthenticationHandler, AuthHeaders, AuthResultParams, + AuthRedirected, + AuthRedirectedParams, AuthToolkit, AuthResult, Authenticated, + AuthNotHandled, AuthResultType, } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 425d8cac1893e..7b1630a7de0be 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -50,7 +50,7 @@ describe('http service', () => { await root.shutdown(); }); describe('#isAuthenticated()', () => { - it('returns true if has been authorized', async () => { + it('returns true if has been authenticated', async () => { const { http } = await root.setup(); const { registerAuth, createRouter, auth } = http; @@ -65,11 +65,11 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true }); }); - it('returns false if has not been authorized', async () => { + it('returns false if has not been authenticated', async () => { const { http } = await root.setup(); const { registerAuth, createRouter, auth } = http; - await registerAuth((req, res, toolkit) => toolkit.authenticated()); + registerAuth((req, res, toolkit) => toolkit.authenticated()); const router = createRouter(''); router.get( @@ -81,7 +81,7 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); }); - it('returns false if no authorization mechanism has been registered', async () => { + it('returns false if no authentication mechanism has been registered', async () => { const { http } = await root.setup(); const { createRouter, auth } = http; @@ -94,6 +94,37 @@ describe('http service', () => { await root.start(); await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); }); + + it('returns true if authenticated on a route with "optional" auth', async () => { + const { http } = await root.setup(); + const { createRouter, auth, registerAuth } = http; + + registerAuth((req, res, toolkit) => toolkit.authenticated()); + const router = createRouter(''); + router.get( + { path: '/is-auth', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) + ); + + await root.start(); + await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true }); + }); + + it('returns false if not authenticated on a route with "optional" auth', async () => { + const { http } = await root.setup(); + const { createRouter, auth, registerAuth } = http; + + registerAuth((req, res, toolkit) => toolkit.notHandled()); + + const router = createRouter(''); + router.get( + { path: '/is-auth', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) + ); + + await root.start(); + await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); + }); }); describe('#get()', () => { it('returns authenticated status and allow associate auth state with request', async () => { diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 6dc7ece1359df..0f0d54e88daca 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -57,7 +57,7 @@ interface StorageData { } describe('OnPreAuth', () => { - it('supports registering request inceptors', async () => { + it('supports registering a request interceptor', async () => { const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -415,6 +415,23 @@ describe('Auth', () => { .expect(200, { content: 'ok' }); }); + it('blocks access to a resource if credentials are not provided', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ body: { content: 'ok' } }) + ); + registerAuth((req, res, t) => t.notHandled()); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(401); + + expect(result.body.message).toBe('Unauthorized'); + }); + it('enables auth for a route by default if registerAuth has been called', async () => { const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -492,11 +509,9 @@ describe('Auth', () => { router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); const redirectTo = '/redirect-url'; - registerAuth((req, res) => - res.redirected({ - headers: { - location: redirectTo, - }, + registerAuth((req, res, t) => + t.redirected({ + location: redirectTo, }) ); await server.start(); @@ -507,6 +522,19 @@ describe('Auth', () => { expect(response.header.location).toBe(redirectTo); }); + it('throws if redirection url is not provided', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); + registerAuth((req, res, t) => t.redirected({} as any)); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(500); + }); + it(`doesn't expose internal error details`, async () => { const { registerAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -865,7 +893,7 @@ describe('Auth', () => { ] `); }); - // eslint-disable-next-line + it(`doesn't share request object between interceptors`, async () => { const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index bc1bbc881315a..85270174fbc04 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -45,6 +45,89 @@ afterEach(async () => { const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); describe('KibanaRequest', () => { + describe('auth', () => { + describe('isAuthenticated', () => { + it('returns false if no auth interceptor was registered', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: false, + }); + }); + it('returns false if not authenticated on a route with authRequired: "optional"', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.notHandled()); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: false, + }); + }); + it('returns false if redirected on a route with authRequired: "optional"', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.redirected({ location: '/any' })); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: false, + }); + }); + it('returns true if authenticated on a route with authRequired: "optional"', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.authenticated()); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: true, + }); + }); + it('returns true if authenticated', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + registerAuth((req, res, toolkit) => toolkit.authenticated()); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: { isAuthenticated: req.auth.isAuthenticated } }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + isAuthenticated: true, + }); + }); + }); + }); describe('events', () => { describe('aborted$', () => { it('emits once and completes when request aborted', async done => { diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index a1523781010d4..ee5b0c50acafb 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -46,6 +46,286 @@ afterEach(async () => { await server.stop(); }); +describe('Options', () => { + describe('authRequired', () => { + describe('optional', () => { + it('User has access to a route if auth mechanism not registered', async () => { + const { server: innerServer, createRouter, auth } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + + it('Authenticated user has access to a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated(); + }); + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: true, + requestIsAuthenticated: true, + }); + }); + + it('User with no credentials can access a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => toolkit.notHandled()); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + + it('User with invalid credentials cannot access a route', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => res.unauthorized()); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('does not redirect user and allows access to a resource', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => + toolkit.redirected({ + location: '/redirect-to', + }) + ); + + router.get( + { path: '/', validate: false, options: { authRequired: 'optional' } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + }); + + describe('true', () => { + it('User has access to a route if auth interceptor is not registered', async () => { + const { server: innerServer, createRouter, auth } = await server.setup(setupDeps); + const router = createRouter('/'); + + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + }); + + it('Authenticated user has access to a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => { + return toolkit.authenticated(); + }); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: true, + requestIsAuthenticated: true, + }); + }); + + it('User with no credentials cannot access a route', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => toolkit.notHandled()); + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('User with invalid credentials cannot access a route', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + + registerAuth((req, res, toolkit) => res.unauthorized()); + + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(401); + }); + + it('allows redirecting an user', async () => { + const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + const router = createRouter('/'); + const redirectUrl = '/redirect-to'; + + registerAuth((req, res, toolkit) => + toolkit.redirected({ + location: redirectUrl, + }) + ); + + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ body: 'ok' }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(302); + + expect(result.header.location).toBe(redirectUrl); + }); + }); + + describe('false', () => { + it('does not try to authenticate a user', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + const authHook = jest.fn(); + registerAuth(authHook); + router.get( + { path: '/', validate: false, options: { authRequired: false } }, + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); + + expect(authHook).toHaveBeenCalledTimes(0); + }); + }); + }); +}); + describe('Handler', () => { it("Doesn't expose error details if handler throws", async () => { const { server: innerServer, createRouter } = await server.setup(setupDeps); diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 036ab0211c2ff..2eaf7e0f6fbfe 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -25,11 +25,14 @@ import { lifecycleResponseFactory, LifecycleResponseFactory, isKibanaResponse, + ResponseHeaders, } from '../router'; /** @public */ export enum AuthResultType { authenticated = 'authenticated', + notHandled = 'notHandled', + redirected = 'redirected', } /** @public */ @@ -38,10 +41,20 @@ export interface Authenticated extends AuthResultParams { } /** @public */ -export type AuthResult = Authenticated; +export interface AuthNotHandled { + type: AuthResultType.notHandled; +} + +/** @public */ +export interface AuthRedirected extends AuthRedirectedParams { + type: AuthResultType.redirected; +} + +/** @public */ +export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected; const authResult = { - authenticated(data: Partial = {}): AuthResult { + authenticated(data: AuthResultParams = {}): AuthResult { return { type: AuthResultType.authenticated, state: data.state, @@ -49,8 +62,25 @@ const authResult = { responseHeaders: data.responseHeaders, }; }, + notHandled(): AuthResult { + return { + type: AuthResultType.notHandled, + }; + }, + redirected(headers: { location: string } & ResponseHeaders): AuthResult { + return { + type: AuthResultType.redirected, + headers, + }; + }, isAuthenticated(result: AuthResult): result is Authenticated { - return result && result.type === AuthResultType.authenticated; + return result?.type === AuthResultType.authenticated; + }, + isNotHandled(result: AuthResult): result is AuthNotHandled { + return result?.type === AuthResultType.notHandled; + }, + isRedirected(result: AuthResult): result is AuthRedirected { + return result?.type === AuthResultType.redirected; }, }; @@ -62,7 +92,7 @@ const authResult = { export type AuthHeaders = Record; /** - * Result of an incoming request authentication. + * Result of successful authentication. * @public */ export interface AuthResultParams { @@ -82,6 +112,18 @@ export interface AuthResultParams { responseHeaders?: AuthHeaders; } +/** + * Result of auth redirection. + * @public + */ +export interface AuthRedirectedParams { + /** + * Headers to attach for auth redirect. + * Must include "location" header + */ + headers: { location: string } & ResponseHeaders; +} + /** * @public * A tool set defining an outcome of Auth interceptor for incoming request. @@ -89,10 +131,23 @@ export interface AuthResultParams { export interface AuthToolkit { /** Authentication is successful with given credentials, allow request to pass through */ authenticated: (data?: AuthResultParams) => AuthResult; + /** + * User has no credentials. + * Allows user to access a resource when authRequired: 'optional' + * Rejects a request when authRequired: true + * */ + notHandled: () => AuthResult; + /** + * Redirects user to another location to complete authentication when authRequired: true + * Allows user to access a resource without redirection when authRequired: 'optional' + * */ + redirected: (headers: { location: string } & ResponseHeaders) => AuthResult; } const toolkit: AuthToolkit = { authenticated: authResult.authenticated, + notHandled: authResult.notHandled, + redirected: authResult.redirected, }; /** @@ -109,30 +164,51 @@ export type AuthenticationHandler = ( export function adoptToHapiAuthFormat( fn: AuthenticationHandler, log: Logger, - onSuccess: (req: Request, data: AuthResultParams) => void = () => undefined + onAuth: (request: Request, data: AuthResultParams) => void = () => undefined ) { return async function interceptAuth( request: Request, responseToolkit: ResponseToolkit ): Promise { const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); + const kibanaRequest = KibanaRequest.from(request, undefined, false); + try { - const result = await fn( - KibanaRequest.from(request, undefined, false), - lifecycleResponseFactory, - toolkit - ); + const result = await fn(kibanaRequest, lifecycleResponseFactory, toolkit); + if (isKibanaResponse(result)) { return hapiResponseAdapter.handle(result); } + if (authResult.isAuthenticated(result)) { - onSuccess(request, { + onAuth(request, { state: result.state, requestHeaders: result.requestHeaders, responseHeaders: result.responseHeaders, }); return responseToolkit.authenticated({ credentials: result.state || {} }); } + + if (authResult.isRedirected(result)) { + // we cannot redirect a user when resources with optional auth requested + if (kibanaRequest.route.options.authRequired === 'optional') { + return responseToolkit.continue; + } + + return hapiResponseAdapter.handle( + lifecycleResponseFactory.redirected({ + // hapi doesn't accept string[] as a valid header + headers: result.headers as any, + }) + ); + } + + if (authResult.isNotHandled(result)) { + if (kibanaRequest.route.options.authRequired === 'optional') { + return responseToolkit.continue; + } + return hapiResponseAdapter.handle(lifecycleResponseFactory.unauthorized()); + } throw new Error( `Unexpected result from Authenticate. Expected AuthResult or KibanaResponse, but given: ${result}.` ); diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts index 032027c234485..fb999dc60e39c 100644 --- a/src/core/server/http/router/request.test.ts +++ b/src/core/server/http/router/request.test.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { RouteOptions } from 'hapi'; import { KibanaRequest } from './request'; import { httpServerMock } from '../http_server.mocks'; import { schema } from '@kbn/config-schema'; @@ -117,6 +118,106 @@ describe('KibanaRequest', () => { }); }); + describe('route.options.authRequired property', () => { + it('handles required auth: undefined', () => { + const auth: RouteOptions['auth'] = undefined; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe(true); + }); + it('handles required auth: false', () => { + const auth: RouteOptions['auth'] = false; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe(false); + }); + it('handles required auth: { mode: "required" }', () => { + const auth: RouteOptions['auth'] = { mode: 'required' }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe(true); + }); + + it('handles required auth: { mode: "optional" }', () => { + const auth: RouteOptions['auth'] = { mode: 'optional' }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe('optional'); + }); + + it('handles required auth: { mode: "try" } as "optional"', () => { + const auth: RouteOptions['auth'] = { mode: 'try' }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + const kibanaRequest = KibanaRequest.from(request); + + expect(kibanaRequest.route.options.authRequired).toBe('optional'); + }); + + it('throws on auth: strategy name', () => { + const auth: RouteOptions['auth'] = 'session'; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + + expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot( + `"unexpected authentication options: \\"session\\" for route: /"` + ); + }); + + it('throws on auth: { mode: unexpected mode }', () => { + const auth: RouteOptions['auth'] = { mode: undefined }; + const request = httpServerMock.createRawRequest({ + route: { + settings: { + auth, + }, + }, + }); + + expect(() => KibanaRequest.from(request)).toThrowErrorMatchingInlineSnapshot( + `"unexpected authentication options: {} for route: /"` + ); + }); + }); + describe('RouteSchema type inferring', () => { it('should work with config-schema', () => { const body = Buffer.from('body!'); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index bb2db6367f701..f266677c1a172 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -143,6 +143,10 @@ export class KibanaRequest< public readonly socket: IKibanaSocket; /** Request events {@link KibanaRequestEvents} */ public readonly events: KibanaRequestEvents; + public readonly auth: { + /* true if the request has been successfully authenticated, otherwise false. */ + isAuthenticated: boolean; + }; /** @internal */ protected readonly [requestSymbol]: Request; @@ -172,6 +176,11 @@ export class KibanaRequest< this.route = deepFreeze(this.getRouteInfo(request)); this.socket = new KibanaSocket(request.raw.req.socket); this.events = this.getEvents(request); + + this.auth = { + // missing in fakeRequests, so we cast to false + isAuthenticated: Boolean(request.auth?.isAuthenticated), + }; } private getEvents(request: Request): KibanaRequestEvents { @@ -189,7 +198,7 @@ export class KibanaRequest< const { parse, maxBytes, allow, output } = request.route.settings.payload || {}; const options = ({ - authRequired: request.route.settings.auth !== false, + authRequired: this.getAuthRequired(request), // some places in LP call KibanaRequest.from(request) manually. remove fallback to true before v8 xsrfRequired: (request.route.settings.app as KibanaRouteState)?.xsrfRequired ?? true, tags: request.route.settings.tags || [], @@ -209,6 +218,31 @@ export class KibanaRequest< options, }; } + + private getAuthRequired(request: Request): boolean | 'optional' { + const authOptions = request.route.settings.auth; + if (typeof authOptions === 'object') { + // 'try' is used in the legacy platform + if (authOptions.mode === 'optional' || authOptions.mode === 'try') { + return 'optional'; + } + if (authOptions.mode === 'required') { + return true; + } + } + + // legacy platform routes + if (authOptions === undefined) { + return true; + } + + if (authOptions === false) return false; + throw new Error( + `unexpected authentication options: ${JSON.stringify(authOptions)} for route: ${ + this.url.href + }` + ); + } } /** diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index d1458ef4ad063..bb0a8616e7222 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -116,13 +116,15 @@ export interface RouteConfigOptionsBody { */ export interface RouteConfigOptions { /** - * A flag shows that authentication for a route: - * `enabled` when true - * `disabled` when false + * Defines authentication mode for a route: + * - true. A user has to have valid credentials to access a resource + * - false. A user can access a resource without any credentials. + * - 'optional'. A user can access a resource if has valid credentials or no credentials at all. + * Can be useful when we grant access to a resource but want to identify a user if possible. * - * Enabled by default. + * Defaults to `true` if an auth mechanism is registered. */ - authRequired?: boolean; + authRequired?: boolean | 'optional'; /** * Defines xsrf protection requirements for a route: diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 8e481171116fa..80eabe778ece3 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -100,9 +100,12 @@ export { AuthResultParams, AuthStatus, AuthToolkit, + AuthRedirected, + AuthRedirectedParams, AuthResult, AuthResultType, Authenticated, + AuthNotHandled, BasePath, IBasePath, CustomHttpResponseOptions, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts b/src/core/server/metrics/collectors/mocks.ts similarity index 72% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts rename to src/core/server/metrics/collectors/mocks.ts index d2d11c14a3e5f..d1eb15637779a 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/calculate_object_hash.d.ts +++ b/src/core/server/metrics/collectors/mocks.ts @@ -17,4 +17,19 @@ * under the License. */ -export function calculateObjectHash(obj: object): string; +import { MetricsCollector } from './types'; + +const createMock = () => { + const mocked: jest.Mocked> = { + collect: jest.fn(), + reset: jest.fn(), + }; + + mocked.collect.mockResolvedValue({}); + + return mocked; +}; + +export const collectorMock = { + create: createMock, +}; diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts index d3d9bb0be86fa..59bef9d8ddd2b 100644 --- a/src/core/server/metrics/collectors/os.ts +++ b/src/core/server/metrics/collectors/os.ts @@ -57,4 +57,6 @@ export class OsMetricsCollector implements MetricsCollector { return metrics; } + + public reset() {} } diff --git a/src/core/server/metrics/collectors/process.ts b/src/core/server/metrics/collectors/process.ts index aa68abaf74e41..a3b59a7cc8b7c 100644 --- a/src/core/server/metrics/collectors/process.ts +++ b/src/core/server/metrics/collectors/process.ts @@ -40,6 +40,8 @@ export class ProcessMetricsCollector implements MetricsCollector => { diff --git a/src/core/server/metrics/collectors/server.ts b/src/core/server/metrics/collectors/server.ts index e46ac2f653df6..84204d0466ff3 100644 --- a/src/core/server/metrics/collectors/server.ts +++ b/src/core/server/metrics/collectors/server.ts @@ -26,12 +26,12 @@ interface ServerResponseTime { } export class ServerMetricsCollector implements MetricsCollector { - private readonly requests: OpsServerMetrics['requests'] = { + private requests: OpsServerMetrics['requests'] = { disconnects: 0, total: 0, statusCodes: {}, }; - private readonly responseTimes: ServerResponseTime = { + private responseTimes: ServerResponseTime = { count: 0, total: 0, max: 0, @@ -77,4 +77,17 @@ export class ServerMetricsCollector implements MetricsCollector { + /** collect the data currently gathered by the collector */ collect(): Promise; + /** reset the internal state of the collector */ + reset(): void; } /** diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts index 6baf95894b9b4..dd5c256cf1600 100644 --- a/src/core/server/metrics/integration_tests/server_collector.test.ts +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -200,4 +200,80 @@ describe('ServerMetricsCollector', () => { metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(0); }); + + describe('#reset', () => { + it('reset the requests state', async () => { + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + await server.start(); + + await sendGet('/'); + await sendGet('/'); + await sendGet('/not-found'); + + let metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 3, + disconnects: 0, + statusCodes: { + '200': 2, + '404': 1, + }, + }); + + collector.reset(); + metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 0, + disconnects: 0, + statusCodes: {}, + }); + + await sendGet('/'); + await sendGet('/not-found'); + + metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 2, + disconnects: 0, + statusCodes: { + '200': 1, + '404': 1, + }, + }); + }); + + it('resets the response times', async () => { + router.get({ path: '/no-delay', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/500-ms', validate: false }, async (ctx, req, res) => { + await delay(500); + return res.ok({ body: '' }); + }); + + await server.start(); + + await Promise.all([sendGet('/no-delay'), sendGet('/500-ms')]); + let metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(250); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500); + + collector.reset(); + metrics = await collector.collect(); + expect(metrics.response_times.avg_in_millis).toBe(0); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(0); + + await Promise.all([sendGet('/500-ms'), sendGet('/500-ms')]); + metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(500); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500); + }); + }); }); diff --git a/src/core/server/metrics/metrics_service.test.mocks.ts b/src/core/server/metrics/metrics_service.test.mocks.ts index 8e91775283042..fe46e5693bf45 100644 --- a/src/core/server/metrics/metrics_service.test.mocks.ts +++ b/src/core/server/metrics/metrics_service.test.mocks.ts @@ -17,9 +17,10 @@ * under the License. */ -export const mockOpsCollector = { - collect: jest.fn(), -}; +import { collectorMock } from './collectors/mocks'; + +export const mockOpsCollector = collectorMock.create(); + jest.doMock('./ops_metrics_collector', () => ({ OpsMetricsCollector: jest.fn().mockImplementation(() => mockOpsCollector), })); diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index 10d6761adbe7d..f6334cc5d3c0f 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -57,37 +57,50 @@ describe('MetricsService', () => { expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval); }); - it('emits the metrics at start', async () => { + it('collects the metrics at every interval', async () => { mockOpsCollector.collect.mockResolvedValue(dummyMetrics); - const { getOpsMetrics$ } = await metricsService.setup({ - http: httpMock, - }); - + await metricsService.setup({ http: httpMock }); await metricsService.start(); expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); - expect( - await getOpsMetrics$() - .pipe(take(1)) - .toPromise() - ).toEqual(dummyMetrics); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3); }); - it('collects the metrics at every interval', async () => { + it('resets the collector after each collection', async () => { mockOpsCollector.collect.mockResolvedValue(dummyMetrics); - await metricsService.setup({ http: httpMock }); - + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); await metricsService.start(); + // `advanceTimersByTime` only ensure the interval handler is executed + // however the `reset` call is executed after the async call to `collect` + // meaning that we are going to miss the call if we don't wait for the + // actual observable emission that is performed after + const waitForNextEmission = () => + getOpsMetrics$() + .pipe(take(1)) + .toPromise(); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + expect(mockOpsCollector.reset).toHaveBeenCalledTimes(1); + let nextEmission = waitForNextEmission(); jest.advanceTimersByTime(testInterval); + await nextEmission; expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + expect(mockOpsCollector.reset).toHaveBeenCalledTimes(2); + nextEmission = waitForNextEmission(); jest.advanceTimersByTime(testInterval); + await nextEmission; expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3); + expect(mockOpsCollector.reset).toHaveBeenCalledTimes(3); }); it('throws when called before setup', async () => { diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index 1aed89a4aad60..0ea9d00792600 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -17,8 +17,8 @@ * under the License. */ -import { ReplaySubject } from 'rxjs'; -import { first, shareReplay } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; @@ -37,7 +37,7 @@ export class MetricsService private readonly logger: Logger; private metricsCollector?: OpsMetricsCollector; private collectInterval?: NodeJS.Timeout; - private metrics$ = new ReplaySubject(1); + private metrics$ = new Subject(); constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('metrics'); @@ -46,7 +46,7 @@ export class MetricsService public async setup({ http }: MetricsServiceSetupDeps): Promise { this.metricsCollector = new OpsMetricsCollector(http.server); - const metricsObservable = this.metrics$.pipe(shareReplay(1)); + const metricsObservable = this.metrics$.asObservable(); return { getOpsMetrics$: () => metricsObservable, @@ -74,6 +74,7 @@ export class MetricsService private async refreshMetrics() { this.logger.debug('Refreshing metrics'); const metrics = await this.metricsCollector!.collect(); + this.metricsCollector!.reset(); this.metrics$.next(metrics); } diff --git a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts index 8265796d57970..cf51f8a753729 100644 --- a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts +++ b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts @@ -17,23 +17,19 @@ * under the License. */ -export const mockOsCollector = { - collect: jest.fn(), -}; +import { collectorMock } from './collectors/mocks'; + +export const mockOsCollector = collectorMock.create(); jest.doMock('./collectors/os', () => ({ OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector), })); -export const mockProcessCollector = { - collect: jest.fn(), -}; +export const mockProcessCollector = collectorMock.create(); jest.doMock('./collectors/process', () => ({ ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector), })); -export const mockServerCollector = { - collect: jest.fn(), -}; +export const mockServerCollector = collectorMock.create(); jest.doMock('./collectors/server', () => ({ ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector), })); diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts index 04302a195fb6c..559588db60a42 100644 --- a/src/core/server/metrics/ops_metrics_collector.test.ts +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -35,25 +35,43 @@ describe('OpsMetricsCollector', () => { mockOsCollector.collect.mockResolvedValue('osMetrics'); }); - it('gathers metrics from the underlying collectors', async () => { - mockOsCollector.collect.mockResolvedValue('osMetrics'); - mockProcessCollector.collect.mockResolvedValue('processMetrics'); - mockServerCollector.collect.mockResolvedValue({ - requests: 'serverRequestsMetrics', - response_times: 'serverTimingMetrics', + describe('#collect', () => { + it('gathers metrics from the underlying collectors', async () => { + mockOsCollector.collect.mockResolvedValue('osMetrics'); + mockProcessCollector.collect.mockResolvedValue('processMetrics'); + mockServerCollector.collect.mockResolvedValue({ + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + + const metrics = await collector.collect(); + + expect(mockOsCollector.collect).toHaveBeenCalledTimes(1); + expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1); + expect(mockServerCollector.collect).toHaveBeenCalledTimes(1); + + expect(metrics).toEqual({ + process: 'processMetrics', + os: 'osMetrics', + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); }); + }); + + describe('#reset', () => { + it('call reset on the underlying collectors', () => { + collector.reset(); - const metrics = await collector.collect(); + expect(mockOsCollector.reset).toHaveBeenCalledTimes(1); + expect(mockProcessCollector.reset).toHaveBeenCalledTimes(1); + expect(mockServerCollector.reset).toHaveBeenCalledTimes(1); - expect(mockOsCollector.collect).toHaveBeenCalledTimes(1); - expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1); - expect(mockServerCollector.collect).toHaveBeenCalledTimes(1); + collector.reset(); - expect(metrics).toEqual({ - process: 'processMetrics', - os: 'osMetrics', - requests: 'serverRequestsMetrics', - response_times: 'serverTimingMetrics', + expect(mockOsCollector.reset).toHaveBeenCalledTimes(2); + expect(mockProcessCollector.reset).toHaveBeenCalledTimes(2); + expect(mockServerCollector.reset).toHaveBeenCalledTimes(2); }); }); }); diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts index 04344f21f57f7..525515dba1457 100644 --- a/src/core/server/metrics/ops_metrics_collector.ts +++ b/src/core/server/metrics/ops_metrics_collector.ts @@ -49,4 +49,10 @@ export class OpsMetricsCollector implements MetricsCollector { ...server, }; } + + public reset() { + this.processCollector.reset(); + this.osCollector.reset(); + this.serverCollector.reset(); + } } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 30695df33345a..f7afe7a6a290a 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -419,7 +419,26 @@ export type AuthenticationHandler = (request: KibanaRequest, response: Lifecycle export type AuthHeaders = Record; // @public (undocumented) -export type AuthResult = Authenticated; +export interface AuthNotHandled { + // (undocumented) + type: AuthResultType.notHandled; +} + +// @public (undocumented) +export interface AuthRedirected extends AuthRedirectedParams { + // (undocumented) + type: AuthResultType.redirected; +} + +// @public +export interface AuthRedirectedParams { + headers: { + location: string; + } & ResponseHeaders; +} + +// @public (undocumented) +export type AuthResult = Authenticated | AuthNotHandled | AuthRedirected; // @public export interface AuthResultParams { @@ -431,7 +450,11 @@ export interface AuthResultParams { // @public (undocumented) export enum AuthResultType { // (undocumented) - authenticated = "authenticated" + authenticated = "authenticated", + // (undocumented) + notHandled = "notHandled", + // (undocumented) + redirected = "redirected" } // @public @@ -444,6 +467,10 @@ export enum AuthStatus { // @public export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; + notHandled: () => AuthResult; + redirected: (headers: { + location: string; + } & ResponseHeaders) => AuthResult; } // @public @@ -970,6 +997,10 @@ export class KibanaRequest { // @public export interface RouteConfigOptions { - authRequired?: boolean; + authRequired?: boolean | 'optional'; body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; xsrfRequired?: Method extends 'get' ? never : boolean; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 35ac4e27f9c8b..8ed64f004c9be 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -25,4 +25,5 @@ export const storybookAliases = { embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', siem: 'x-pack/legacy/plugins/siem/scripts/storybook.js', + ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 24dd1c4944bfb..bb954cb887ef3 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -38,7 +38,7 @@ import { } from '../../../../../../plugins/data/public'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; -import { calculateObjectHash } from '../../../../visualizations/public'; +import { calculateObjectHash } from '../../../../../../plugins/kibana_utils/common'; import { tabifyAggResponse } from '../../../../../core_plugins/data/public'; import { PersistedState } from '../../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../../plugins/inspector/public'; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts index 1bdff06b3a59f..dae6c9abb625e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -22,8 +22,9 @@ import { i18n } from '@kbn/i18n'; import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; -import { Status, defaultFeedbackMessage } from '../../visualizations/public'; +import { Status } from '../../visualizations/public'; import { InputControlVisDependencies } from './plugin'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/legacy/core_plugins/input_control_vis/public/plugin.ts index e9ffad8b35f21..e85ccd94f9e6a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/plugin.ts +++ b/src/legacy/core_plugins/input_control_vis/public/plugin.ts @@ -59,7 +59,7 @@ export class InputControlVisPlugin implements Plugin, void> { }; expressions.registerFunction(createInputControlVisFn); - visualizations.types.createBaseVisualization( + visualizations.createBaseVisualization( createInputControlVisTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 91b5c7f13dc95..7fa5183a4f54b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -50,7 +50,6 @@ export function setServices(newServices: any) { // EXPORT legacy static dependencies, should be migrated when available in a new version; export { angular }; export { wrapInI18nContext } from 'ui/i18n'; -export { buildVislibDimensions } from '../../../visualizations/public'; export { getRequestInspectorStats, getResponseInspectorStats } from '../../../data/public'; // @ts-ignore export { intervalOptions } from 'ui/agg_types'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index fb4158a6e3e03..81c10798936f5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -45,7 +45,6 @@ import { getPainlessError } from './get_painless_error'; import { discoverResponseHandler } from './response_handler'; import { angular, - buildVislibDimensions, getRequestInspectorStats, getResponseInspectorStats, getServices, @@ -76,6 +75,7 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; import { esFilters, + fieldFormats, indexPatterns as indexPatternsUtils, } from '../../../../../../../plugins/data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; @@ -812,21 +812,45 @@ function discoverController( $fetchObservable.next(); }; + function getDimensions(aggs, timeRange) { + const [metric, agg] = aggs; + agg.params.timeRange = timeRange; + const bounds = agg.params.timeRange ? timefilter.calculateBounds(agg.params.timeRange) : null; + agg.buckets.setBounds(bounds); + + const { esUnit, esValue } = agg.buckets.getInterval(); + return { + x: { + accessor: 0, + label: agg.makeLabel(), + format: fieldFormats.serialize(agg), + params: { + date: true, + interval: moment.duration(esValue, esUnit), + intervalESValue: esValue, + intervalESUnit: esUnit, + format: agg.buckets.getScaledDateFormat(), + bounds: agg.buckets.getBounds(), + }, + }, + y: { + accessor: 1, + format: fieldFormats.serialize(metric), + label: metric.makeLabel(), + }, + }; + } + function onResults(resp) { logInspectorResponse(resp); if ($scope.opts.timefield) { const tabifiedData = tabifyAggResponse($scope.vis.aggs, resp); $scope.searchSource.rawResponse = resp; - Promise.resolve( - buildVislibDimensions($scope.vis, { - timefilter, - timeRange: $scope.timeRange, - searchSource: $scope.searchSource, - }) - ).then(resp => { - $scope.histogramData = discoverResponseHandler(tabifiedData, resp); - }); + $scope.histogramData = discoverResponseHandler( + tabifiedData, + getDimensions($scope.vis.aggs.aggs, $scope.timeRange) + ); } $scope.hits = resp.hits.total; @@ -993,7 +1017,7 @@ function discoverController( }, }; - $scope.vis = new visualizations.Vis( + $scope.vis = visualizations.createVis( $scope.searchSource.getField('index'), visSavedObject.visState ); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts index 8dbf3cd79ccb1..7ea1863693e0d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/lib/visualize_url_utils.ts @@ -29,7 +29,7 @@ import { getServices } from '../../../../kibana_services'; function getMapsAppBaseUrl() { const mapsAppVisAlias = getServices() - .visualizations.types.getAliases() + .visualizations.getAliases() .find(({ name }) => { return name === 'maps'; }); @@ -38,7 +38,7 @@ function getMapsAppBaseUrl() { export function isMapsAppRegistered() { return getServices() - .visualizations.types.getAliases() + .visualizations.getAliases() .some(({ name }) => { return name === 'maps'; }); diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 604575a6e6220..8e73a09480c41 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; import { SavedObjectLoader } from '../../../../../plugins/saved_objects/public'; import { createSavedDashboardLoader } from '../dashboard'; -import { TypesService, createSavedVisLoader } from '../../../visualizations/public'; +import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; import { createSavedSearchesLoader } from '../../../../../plugins/discover/public'; /** @@ -58,10 +58,7 @@ const services = { savedObjectManagementRegistry.register({ id: 'savedVisualizations', - service: createSavedVisLoader({ - ...services, - ...{ visualizationTypes: new TypesService().start() }, - }), + service: visualizations.savedVisualizationsLoader, title: 'visualizations', }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts index cd7c8278adcc7..5a8460fcb51ba 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/lib/get_indices.test.ts @@ -19,7 +19,8 @@ import { getIndices } from './get_indices'; import { IndexPatternCreationConfig } from './../../../../../../../management/public'; -import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { LegacyApiCaller } from '../../../../../../../../../plugins/data/public/search'; export const successfulResponse = { hits: { diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index b8ee7cd378750..66a7bd6f33373 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,17 +24,8 @@ * directly where they are needed. */ -export { State } from 'ui/state_management/state'; -// @ts-ignore -export { GlobalStateProvider } from 'ui/state_management/global_state'; -// @ts-ignore -export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; - export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore -export { EventsProvider } from 'ui/events'; -export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; -// @ts-ignore export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index b15d89275eba7..8ef63ec5778e2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -23,13 +23,11 @@ import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; import { configureAppAngularModule, - GlobalStateProvider, KbnUrlProvider, RedirectWhenMissingProvider, IPrivate, PrivateProvider, PromiseServiceCreator, - StateManagementConfigProvider, } from '../legacy_imports'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { @@ -87,35 +85,20 @@ function createLocalAngularModule(core: AppMountContext['core'], navigation: Nav createLocalI18nModule(); createLocalPrivateModule(); createLocalPromiseModule(); - createLocalConfigModule(core); createLocalKbnUrlModule(); - createLocalStateModule(); createLocalTopNavModule(navigation); const visualizeAngularModule: IModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, - 'app/visualize/Config', 'app/visualize/I18n', 'app/visualize/Private', 'app/visualize/TopNav', - 'app/visualize/State', + 'app/visualize/KbnUrl', + 'app/visualize/Promise', ]); return visualizeAngularModule; } -function createLocalStateModule() { - angular - .module('app/visualize/State', [ - 'app/visualize/Private', - 'app/visualize/Config', - 'app/visualize/KbnUrl', - 'app/visualize/Promise', - ]) - .service('globalState', function(Private: IPrivate) { - return Private(GlobalStateProvider); - }); -} - function createLocalKbnUrlModule() { angular .module('app/visualize/KbnUrl', ['app/visualize/Private', 'ngRoute']) @@ -123,19 +106,6 @@ function createLocalKbnUrlModule() { .service('redirectWhenMissing', (Private: IPrivate) => Private(RedirectWhenMissingProvider)); } -function createLocalConfigModule(core: AppMountContext['core']) { - angular - .module('app/visualize/Config', ['app/visualize/Private']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: core.uiSettings.get.bind(core.uiSettings), - }), - }; - }); -} - function createLocalPromiseModule() { angular.module('app/visualize/Promise', []).service('Promise', PromiseServiceCreator); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html index 9dbb05ea95b48..28baf21925cbe 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.html @@ -31,8 +31,8 @@ refresh-interval="refreshInterval.value" on-refresh-change="onRefreshChange" show-save-query="showSaveQuery" - on-saved="onQuerySaved" - on-saved-query-updated="onSavedQueryUpdated" + on-saved="updateSavedQuery" + on-saved-query-updated="updateSavedQuery" on-clear-saved-query="onClearSavedQuery" > diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 2d2552b5e2f30..e1a20e3381331 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -20,6 +20,7 @@ import angular from 'angular'; import _ from 'lodash'; import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import React from 'react'; @@ -29,13 +30,17 @@ import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; -import { FilterStateManager } from '../../../../../data/public'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; import { kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, showSaveModal, } from '../../../../../../../plugins/saved_objects/public'; +import { + esFilters, + connectToQueryState, + syncQueryStateWithUrl, +} from '../../../../../../../plugins/data/public'; import { initVisEditorDirective } from './visualization_editor'; import { initVisualizationDirective } from './visualization'; @@ -65,28 +70,21 @@ export function initEditorDirective(app, deps) { function VisualizeAppController( $scope, - $element, $route, $window, $injector, $timeout, kbnUrl, redirectWhenMissing, - Promise, - globalState, - config + kbnUrlStateStorage, + history ) { const { indexPatterns, localStorage, visualizeCapabilities, share, - data: { - query: { - filterManager, - timefilter: { timefilter }, - }, - }, + data: { query: queryService }, toastNotifications, chrome, getBasePath, @@ -97,6 +95,17 @@ function VisualizeAppController( setActiveUrl, } = getServices(); + const { + filterManager, + timefilter: { timefilter }, + } = queryService; + + // starts syncing `_g` portion of url with query services + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + queryService, + kbnUrlStateStorage + ); + // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; const _applyVis = () => { @@ -284,26 +293,24 @@ function VisualizeAppController( linked: !!savedVis.savedSearchId, }; - const useHash = config.get('state:storeInSessionStorage'); const { stateContainer, stopStateSync } = useVisualizeAppState({ - useHash, stateDefaults, + kbnUrlStateStorage, }); - const filterStateManager = new FilterStateManager( - globalState, - () => { - // Temporary AppState replacement - return { - set filters(_filters) { - stateContainer.transitions.set('filters', _filters); - }, - get filters() { - return stateContainer.getState().filters; - }, - }; + // sync initial app filters from state to filterManager + filterManager.setAppFilters(_.cloneDeep(stateContainer.getState().filters)); + // setup syncing of app filters between appState and filterManager + const stopSyncingAppFilters = connectToQueryState( + queryService, + { + set: ({ filters }) => stateContainer.transitions.set('filters', filters), + get: () => ({ filters: stateContainer.getState().filters }), + state$: stateContainer.state$.pipe(map(state => ({ filters: state.filters }))), }, - filterManager + { + filters: esFilters.FilterStateStore.APP_STATE, + } ); // The savedVis is pulled from elasticsearch, but the appState is pulled from the url, with the @@ -335,6 +342,24 @@ function VisualizeAppController( } ); + const updateSavedQueryFromUrl = savedQueryId => { + if (!savedQueryId) { + delete $scope.savedQuery; + + return; + } + + if ($scope.savedQuery && $scope.savedQuery.id === savedQueryId) { + return; + } + + savedQueryService.getSavedQuery(savedQueryId).then(savedQuery => { + $scope.$evalAsync(() => { + $scope.updateSavedQuery(savedQuery); + }); + }); + }; + function init() { if (vis.indexPattern) { $scope.indexPattern = vis.indexPattern; @@ -388,7 +413,6 @@ function VisualizeAppController( }; $scope.timeRange = timefilter.getTime(); - $scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode'); const unsubscribeStateUpdates = stateContainer.subscribe(state => { const newQuery = migrateLegacyQuery(state.query); @@ -396,6 +420,7 @@ function VisualizeAppController( stateContainer.transitions.set('query', newQuery); } persistOnChange(state); + updateSavedQueryFromUrl(state.savedQuery); // if the browser history was changed manually we need to reflect changes in the editor if (!_.isEqual(vis.getState(), state.vis)) { @@ -413,6 +438,9 @@ function VisualizeAppController( $scope.$broadcast('render'); }; + // update the query if savedQuery is stored + updateSavedQueryFromUrl(initialState.savedQuery); + const subscriptions = new Subscription(); subscriptions.add( @@ -438,7 +466,7 @@ function VisualizeAppController( // update the searchSource when query updates $scope.fetch = function() { - const { query, filters, linked } = stateContainer.getState(); + const { query, linked, filters } = stateContainer.getState(); $scope.query = query; $scope.linked = linked; savedVis.searchSource.setField('query', query); @@ -451,7 +479,6 @@ function VisualizeAppController( subscribeWithScope($scope, filterManager.getUpdates$(), { next: () => { $scope.filters = filterManager.getFilters(); - $scope.globalFilters = filterManager.getGlobalFilters(); }, }) ); @@ -466,13 +493,14 @@ function VisualizeAppController( $scope._handler.destroy(); } savedVis.destroy(); - filterStateManager.destroy(); subscriptions.unsubscribe(); $scope.vis.off('apply', _applyVis); unsubscribePersisted(); unsubscribeStateUpdates(); stopStateSync(); + stopSyncingQueryServiceStateWithUrl(); + stopSyncingAppFilters(); }); $timeout(() => { @@ -501,23 +529,14 @@ function VisualizeAppController( }); }; - $scope.onQuerySaved = savedQuery => { - $scope.savedQuery = savedQuery; - }; - - $scope.onSavedQueryUpdated = savedQuery => { - $scope.savedQuery = { ...savedQuery }; - }; - $scope.onClearSavedQuery = () => { delete $scope.savedQuery; stateContainer.transitions.removeSavedQuery(defaultQuery); filterManager.setFilters(filterManager.getGlobalFilters()); - $scope.fetch(); }; const updateStateFromSavedQuery = savedQuery => { - stateContainer.transitions.set('query', savedQuery.attributes.query); + stateContainer.transitions.updateFromSavedQuery(savedQuery); const savedQueryFilters = savedQuery.attributes.filters || []; const globalFilters = filterManager.getGlobalFilters(); @@ -532,25 +551,12 @@ function VisualizeAppController( timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval); } } - - $scope.fetch(); }; - // update the query if savedQuery is stored - if (stateContainer.getState().savedQuery) { - savedQueryService.getSavedQuery(stateContainer.getState().savedQuery).then(savedQuery => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - }); - }); - } - - $scope.$watch('savedQuery', newSavedQuery => { - if (!newSavedQuery) return; - stateContainer.transitions.set('savedQuery', newSavedQuery.id); - - updateStateFromSavedQuery(newSavedQuery); - }); + $scope.updateSavedQuery = savedQuery => { + $scope.savedQuery = savedQuery; + updateStateFromSavedQuery(savedQuery); + }; $scope.$watch('linked', linked => { if (linked && !savedVis.savedSearchId) { @@ -626,7 +632,10 @@ function VisualizeAppController( savedVis.vis.title = savedVis.title; savedVis.vis.description = savedVis.description; } else { - kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }); + history.replace({ + ...history.location, + pathname: `${VisualizeConstants.EDIT_PATH}/${savedVis.id}`, + }); } } }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts index d8de81193d857..d3fae3d457b63 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/visualize_app_state.ts @@ -17,21 +17,20 @@ * under the License. */ -import { createHashHistory } from 'history'; import { isFunction, omit } from 'lodash'; import { migrateAppState } from './migrate_app_state'; import { - createKbnUrlStateStorage, createStateContainer, syncState, + IKbnUrlStateStorage, } from '../../../../../../../../plugins/kibana_utils/public'; import { PureVisState, VisualizeAppState, VisualizeAppStateTransitions } from '../../types'; const STATE_STORAGE_KEY = '_a'; interface Arguments { - useHash: boolean; + kbnUrlStateStorage: IKbnUrlStateStorage; stateDefaults: VisualizeAppState; } @@ -41,12 +40,7 @@ function toObject(state: PureVisState): PureVisState { }); } -export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) { - const history = createHashHistory(); - const kbnUrlStateStorage = createKbnUrlStateStorage({ - useHash, - history, - }); +export function useVisualizeAppState({ stateDefaults, kbnUrlStateStorage }: Arguments) { const urlState = kbnUrlStateStorage.get(STATE_STORAGE_KEY); const initialState = migrateAppState({ ...stateDefaults, @@ -88,6 +82,11 @@ export function useVisualizeAppState({ useHash, stateDefaults }: Arguments) { linked: false, }), updateVisState: state => newVisState => ({ ...state, vis: toObject(newVisState) }), + updateFromSavedQuery: state => savedQuery => ({ + ...state, + savedQuery: savedQuery.id, + query: savedQuery.attributes.query, + }), } ); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/global_state_sync.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/global_state_sync.ts deleted file mode 100644 index f29fb72a9fbc5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/global_state_sync.ts +++ /dev/null @@ -1,67 +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 { State } from '../legacy_imports'; -import { DataPublicPluginStart as DataStart } from '../../../../../../plugins/data/public'; - -/** - * Helper function to sync the global state with the various state providers - * when a local angular application mounts. There are three different ways - * global state can be passed into the application: - * * parameter in the URL hash - e.g. shared link - * * in-memory state in the data plugin exports (timefilter and filterManager) - e.g. default values - * - * This function looks up the three sources (earlier in the list means it takes precedence), - * puts it into the globalState object and syncs it with the url. - * - * Currently the legacy chrome takes care of restoring the global state when navigating from - * one app to another - to migrate away from that it will become necessary to also write the current - * state to local storage - */ -export function syncOnMount( - globalState: State, - { - query: { - filterManager, - timefilter: { timefilter }, - }, - }: DataStart -) { - // pull in global state information from the URL - globalState.fetch(); - // remember whether there were info in the URL - const hasGlobalURLState = Boolean(Object.keys(globalState.toObject()).length); - - // sync kibana platform state with the angular global state - if (!globalState.time) { - globalState.time = timefilter.getTime(); - } - if (!globalState.refreshInterval) { - globalState.refreshInterval = timefilter.getRefreshInterval(); - } - if (!globalState.filters && filterManager.getGlobalFilters().length > 0) { - globalState.filters = filterManager.getGlobalFilters(); - } - // only inject cross app global state if there is none in the url itself (that takes precedence) - if (hasGlobalURLState) { - // set flag the global state is set from the URL - globalState.$inheritedGlobalState = true; - } - globalState.save(); -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index 24055b9a2d9ed..b9409445166bc 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -19,6 +19,9 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { createHashHistory } from 'history'; + +import { createKbnUrlStateStorage } from '../../../../../../plugins/kibana_utils/public'; import editorTemplate from './editor/editor.html'; import visualizeListingTemplate from './listing/visualize_listing.html'; @@ -26,11 +29,7 @@ import visualizeListingTemplate from './listing/visualize_listing.html'; import { initVisualizeAppDirective } from './visualize_app'; import { VisualizeConstants } from './visualize_constants'; import { VisualizeListingController } from './listing/visualize_listing'; -import { - ensureDefaultIndexPattern, - registerTimefilterWithGlobalStateFactory, -} from '../legacy_imports'; -import { syncOnMount } from './global_state_sync'; +import { ensureDefaultIndexPattern } from '../legacy_imports'; import { getLandingBreadcrumbs, @@ -42,17 +41,13 @@ import { export function initVisualizeApp(app, deps) { initVisualizeAppDirective(app, deps); - app.run(globalState => { - syncOnMount(globalState, deps.data); - }); - - app.run((globalState, $rootScope) => { - registerTimefilterWithGlobalStateFactory( - deps.data.query.timefilter.timefilter, - globalState, - $rootScope - ); - }); + app.factory('history', () => createHashHistory()); + app.factory('kbnUrlStateStorage', history => + createKbnUrlStateStorage({ + history, + useHash: deps.uiSettings.get('state:storeInSessionStorage'), + }) + ); app.config(function($routeProvider) { const defaults = { @@ -107,7 +102,7 @@ export function initVisualizeApp(app, deps) { resolve: { savedVis: function(redirectWhenMissing, $route, $rootScope, kbnUrl) { const { core, data, savedVisualizations, visualizations } = deps; - const visTypes = visualizations.types.all(); + const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; const hasIndex = diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js index c0cc499b598f0..5a479a491395a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js @@ -26,23 +26,21 @@ import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; import { wrapInI18nContext } from '../../legacy_imports'; +import { syncQueryStateWithUrl } from '../../../../../../../plugins/data/public'; + export function initListingDirective(app) { app.directive('visualizeListingTable', reactDirective => reactDirective(wrapInI18nContext(VisualizeListingTable)) ); } -export function VisualizeListingController($injector, $scope, createNewVis) { +export function VisualizeListingController($injector, $scope, createNewVis, kbnUrlStateStorage) { const { addBasePath, chrome, savedObjectsClient, savedVisualizations, - data: { - query: { - timefilter: { timefilter }, - }, - }, + data: { query }, toastNotifications, uiSettings, visualizations, @@ -50,6 +48,16 @@ export function VisualizeListingController($injector, $scope, createNewVis) { } = getServices(); const kbnUrl = $injector.get('kbnUrl'); + // syncs `_g` portion of url with query services + const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( + query, + kbnUrlStateStorage + ); + + const { + timefilter: { timefilter }, + } = query; + timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); @@ -124,5 +132,7 @@ export function VisualizeListingController($injector, $scope, createNewVis) { if (this.closeNewVisModal) { this.closeNewVisModal(); } + + stopSyncingQueryServiceStateWithUrl(); }); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 8ca603eb11459..55fccd75361a0 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -17,7 +17,13 @@ * under the License. */ -import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; +import { + TimeRange, + Query, + Filter, + DataPublicPluginStart, + SavedQuery, +} from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { PersistedState } from 'src/plugins/visualizations/public'; import { LegacyCoreStart } from 'kibana/public'; @@ -48,6 +54,7 @@ export interface VisualizeAppStateTransitions { state: VisualizeAppState ) => (query: Query, filters: Filter[]) => VisualizeAppState; updateVisState: (state: VisualizeAppState) => (vis: PureVisState) => VisualizeAppState; + updateFromSavedQuery: (state: VisualizeAppState) => (savedQuery: SavedQuery) => VisualizeAppState; } export interface EditorRenderProps { diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index f11aab9b9db88..6bdb5d00e67d8 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -111,9 +111,7 @@ describe('RegionMapsVisualizationTests', function() { if (!visRegComplete) { visRegComplete = true; - visualizationsSetup.types.createBaseVisualization( - createRegionMapTypeDefinition(dependencies) - ); + visualizationsSetup.createBaseVisualization(createRegionMapTypeDefinition(dependencies)); } RegionMapsVisualization = createRegionMapVisualization(dependencies); @@ -160,7 +158,7 @@ describe('RegionMapsVisualizationTests', function() { imageComparator = new ImageComparator(); - vis = new visualizationsStart.Vis(indexPattern, { + vis = visualizationsStart.createVis(indexPattern, { type: 'region_map', }); diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index aaf0a8a308aea..98fb5604c3d65 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -70,7 +70,7 @@ export class RegionMapPlugin implements Plugin, void> { expressions.registerFunction(createRegionMapFn); - visualizations.types.createBaseVisualization( + visualizations.createBaseVisualization( createRegionMapTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 27e9459c7e06c..6a08405b5b6a5 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -88,9 +88,7 @@ describe('CoordinateMapsVisualizationTest', function() { if (!visRegComplete) { visRegComplete = true; - visualizationsSetup.types.createBaseVisualization( - createTileMapTypeDefinition(dependencies) - ); + visualizationsSetup.createBaseVisualization(createTileMapTypeDefinition(dependencies)); } CoordinateMapsVisualization = createTileMapVisualization(dependencies); @@ -126,7 +124,7 @@ describe('CoordinateMapsVisualizationTest', function() { setupDOM('512px', '512px'); imageComparator = new ImageComparator(); - vis = new visualizationsStart.Vis(indexPattern, { + vis = visualizationsStart.createVis(indexPattern, { type: 'tile_map', }); vis.params = { diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index 52acaf51b39b1..a12c2753cc525 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -64,9 +64,7 @@ export class TileMapPlugin implements Plugin, void> { expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); - visualizations.types.createBaseVisualization( - createTileMapTypeDefinition(visualizationDependencies) - ); + visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies)); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index e4a48c09db832..a9d678cfea79c 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -42,7 +42,7 @@ import '../../data/public/legacy'; import './services/saved_sheet_register'; import rootTemplate from 'plugins/timelion/index.html'; -import { createSavedVisLoader, TypesService } from '../../visualizations/public'; +import { start as visualizations } from '../../visualizations/public/np_ready/public/legacy'; import { loadKbnTopNavDirectives } from '../../../../plugins/kibana_legacy/public'; loadKbnTopNavDirectives(npStart.plugins.navigation.ui); @@ -127,13 +127,7 @@ app.controller('timelion', function( timefilter.enableAutoRefreshSelector(); timefilter.enableTimeRangeSelector(); - const savedVisualizations = createSavedVisLoader({ - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, - visualizationTypes: new TypesService().start(), - }); + const savedVisualizations = visualizations.savedVisualizationsLoader; const timezone = Private(timezoneProvider)(); const defaultExpression = '.es(*)'; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts index f131664756202..71d6c1c69ef2d 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts @@ -39,7 +39,7 @@ export class MarkdownPlugin implements Plugin { } public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) { - visualizations.types.createReactVisualization(markdownVisDefinition); + visualizations.createReactVisualization(markdownVisDefinition); expressions.registerFunction(createMarkdownVisFn); } diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts index 67b5d018f4638..5dbd59f3f1709 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.test.ts @@ -40,7 +40,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { let vis: Vis; beforeAll(() => { - visualizationsSetup.types.createReactVisualization(createMetricVisTypeDefinition()); + visualizationsSetup.createReactVisualization(createMetricVisTypeDefinition()); (npStart.plugins.data.fieldFormats.getType as jest.Mock).mockImplementation(() => { return fieldFormats.UrlFormat; }); @@ -59,7 +59,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { // TODO: remove when Vis is converted to typescript. Only importing Vis as type // @ts-ignore - vis = new visualizationsStart.Vis(stubIndexPattern, { + vis = visualizationsStart.createVis(stubIndexPattern, { type: 'metric', aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }], }); @@ -80,7 +80,7 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { }; const el = document.createElement('div'); - const metricVisType = visualizationsStart.types.get('metric'); + const metricVisType = visualizationsStart.get('metric'); const Controller = metricVisType.visualization; const controller = new Controller(el, vis); const render = (esResponse: any) => { diff --git a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts index 082fab47e573c..28b435cbc7980 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts @@ -45,7 +45,7 @@ export class MetricVisPlugin implements Plugin { { expressions, visualizations, charts }: MetricVisPluginSetupDependencies ) { expressions.registerFunction(createMetricVisFn); - visualizations.types.createReactVisualization(createMetricVisTypeDefinition()); + visualizations.createReactVisualization(createMetricVisTypeDefinition()); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js index 9fe7920588cd2..91581923b05cb 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js @@ -47,10 +47,10 @@ describe('Table Vis - AggTable Directive', function() { const tabifiedData = {}; const init = () => { - const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); + const vis1 = visualizationsStart.createVis(indexPattern, 'table'); tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); - const vis2 = new visualizationsStart.Vis(indexPattern, { + const vis2 = visualizationsStart.createVis(indexPattern, { type: 'table', params: { showMetricsAtAllLevels: true, @@ -69,7 +69,7 @@ describe('Table Vis - AggTable Directive', function() { metricsAtAllLevels: true, }); - const vis3 = new visualizationsStart.Vis(indexPattern, { + const vis3 = visualizationsStart.createVis(indexPattern, { type: 'table', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, @@ -110,7 +110,7 @@ describe('Table Vis - AggTable Directive', function() { beforeEach(initLocalAngular); ngMock.inject(function() { - visualizationsSetup.types.createBaseVisualization(tableVisTypeDefinition); + visualizationsSetup.createBaseVisualization(tableVisTypeDefinition); }); beforeEach(ngMock.module('kibana/table_vis')); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js index 79d4d7c40d355..4d62551dcf396 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js @@ -35,10 +35,10 @@ describe('Table Vis - AggTableGroup Directive', function() { const tabifiedData = {}; const init = () => { - const vis1 = new visualizationsStart.Vis(indexPattern, 'table'); + const vis1 = visualizationsStart.createVis(indexPattern, 'table'); tabifiedData.metricOnly = tabifyAggResponse(vis1.aggs, metricOnly); - const vis2 = new visualizationsStart.Vis(indexPattern, { + const vis2 = visualizationsStart.createVis(indexPattern, { type: 'pie', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts index 17c50b0567b67..519a56da23ac9 100644 --- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_table/public/plugin.ts @@ -44,7 +44,7 @@ export class TableVisPlugin implements Plugin, void> { ) { expressions.registerFunction(createTableVisFn); - visualizations.types.createBaseVisualization(tableVisTypeDefinition); + visualizations.createBaseVisualization(tableVisTypeDefinition); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js index 55ecf98f994d2..3091b3340cd6d 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js @@ -76,7 +76,7 @@ describe('TagCloudVisualizationTest', function() { beforeEach(async function() { setupDOM('512px', '512px'); imageComparator = new ImageComparator(); - vis = new visualizationsStart.Vis(indexPattern, { + vis = visualizationsStart.createVis(indexPattern, { type: 'tagcloud', params: { bucket: { accessor: 0, format: {} }, diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts index 9e5940eca1598..8244cba38edc3 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts @@ -53,7 +53,7 @@ export class TagCloudPlugin implements Plugin { colors: charts.colors, }; expressions.registerFunction(createTagCloudFn); - visualizations.types.createBaseVisualization( + visualizations.createBaseVisualization( createTagCloudVisTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts b/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts index 69a2ad3c1351a..9d69c312b48f4 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/plugin.ts @@ -66,7 +66,7 @@ export class TimelionVisPlugin implements Plugin { }; expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); - visualizations.types.createReactVisualization(getTimelionVisDefinition(dependencies)); + visualizations.createReactVisualization(getTimelionVisDefinition(dependencies)); } public start(core: CoreStart, plugins: PluginsStart) { diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts index 135cc1e181432..30c62d778933b 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/metrics_type.ts @@ -25,7 +25,7 @@ import { metricsRequestHandler } from './request_handler'; import { EditorController } from './editor_controller'; // @ts-ignore import { PANEL_TYPES } from '../../../../plugins/vis_type_timeseries/common/panel_types'; -import { defaultFeedbackMessage } from '../../visualizations/public'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; export const metricsVisDefinition = { name: 'metrics', diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts index 38a9c68487854..441b1f05ea78c 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/public/plugin.ts @@ -57,7 +57,7 @@ export class MetricsPlugin implements Plugin, void> { ) { expressions.registerFunction(createMetricsFn); setUISettings(core.uiSettings); - visualizations.types.createReactVisualization(metricsVisDefinition); + visualizations.createReactVisualization(metricsVisDefinition); } public start(core: CoreStart, { data }: MetricsPluginStartDependencies) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index 378590af29d3a..5befc09b24544 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -93,7 +93,7 @@ describe('VegaVisualizations', () => { if (!visRegComplete) { visRegComplete = true; - visualizationsSetup.types.createBaseVisualization( + visualizationsSetup.createBaseVisualization( createVegaTypeDefinition(vegaVisualizationDependencies) ); } @@ -108,7 +108,7 @@ describe('VegaVisualizations', () => { setupDOM('512px', '512px'); imageComparator = new ImageComparator(); - vis = new visualizationsStart.Vis(indexPattern, { type: 'vega' }); + vis = visualizationsStart.createVis(indexPattern, { type: 'vega' }); }); afterEach(function() { diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index b354433330caf..3b01d9ceca5a6 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -84,9 +84,7 @@ export class VegaPlugin implements Plugin, void> { expressions.registerFunction(() => createVegaFn(visualizationDependencies)); - visualizations.types.createBaseVisualization( - createVegaTypeDefinition(visualizationDependencies) - ); + visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); } public start(core: CoreStart, { data }: VegaPluginStartDependencies) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index a84948f725e0a..78f9c170ab62d 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -19,10 +19,11 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { Status, defaultFeedbackMessage } from '../../visualizations/public'; +import { Status } from '../../visualizations/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; +import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/common'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 8a7196a61ecec..a71892cc47b05 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -97,14 +97,14 @@ export class VisTypeVislibPlugin implements Plugin, void> { // Register legacy vislib types that have been converted convertedFns.forEach(expressions.registerFunction); convertedTypes.forEach(vis => - visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + visualizations.createBaseVisualization(vis(visualizationDependencies)) ); } // Register non-converted types vislibFns.forEach(expressions.registerFunction); vislibTypes.forEach(vis => - visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + visualizations.createBaseVisualization(vis(visualizationDependencies)) ); } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js index 9c9c5a84f046c..43e3b987f1962 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js @@ -133,7 +133,7 @@ describe('No global chart settings', function() { responseHandler = vislibSlicesResponseHandler; let id1 = 1; - stubVis1 = new visualizationsStart.Vis(indexPattern, { + stubVis1 = visualizationsStart.createVis(indexPattern, { type: 'pie', aggs: rowAgg, }); @@ -222,7 +222,7 @@ describe('Vislib PieChart Class Test Suite', function() { responseHandler = vislibSlicesResponseHandler; let id = 1; - stubVis = new visualizationsStart.Vis(indexPattern, { + stubVis = visualizationsStart.createVis(indexPattern, { type: 'pie', aggs: dataAgg, }); diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts index 59bb64b337256..35abb04fd8732 100644 --- a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts @@ -72,7 +72,7 @@ export class VisTypeXyPlugin implements Plugin, void> { visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); visTypeDefinitions.forEach((vis: any) => - visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + visualizations.createBaseVisualization(vis(visualizationDependencies)) ); } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 7688a7769cf79..b59eb2277411c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -43,20 +43,11 @@ export { Vis, VisParams, VisState } from './vis'; import { VisualizeEmbeddableFactory, VisualizeEmbeddable } from './embeddable'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; +export { TypesService } from './vis_types/types_service'; +export { Status } from './legacy/update_status'; // should remove +export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable'; +export { SchemaConfig } from './legacy/build_pipeline'; export function plugin(initializerContext: PluginInitializerContext) { return new VisualizationsPlugin(initializerContext); } - -/** @public static code */ -export { TypesService } from './vis_types/types_service'; -export { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from './embeddable'; - -export { Status } from './legacy/update_status'; -export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/build_pipeline'; - -// @ts-ignore -export { updateOldState } from './legacy/vis_update_state'; -export { calculateObjectHash } from './legacy/calculate_object_hash'; -export { createSavedVisLoader } from './saved_visualizations/saved_visualizations'; -export { defaultFeedbackMessage } from './misc/default_feedback_message'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js index 8c75ba24051b0..deb345a77cdb6 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/__tests__/_vis.js @@ -43,12 +43,12 @@ describe('Vis Class', function() { beforeEach( ngMock.inject(function(Private) { indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - visTypes = visualizations.types; + visTypes = visualizations; }) ); beforeEach(function() { - vis = new visualizations.Vis(indexPattern, stateFixture); + vis = visualizations.createVis(indexPattern, stateFixture); }); const verifyVis = function(vis) { @@ -84,7 +84,7 @@ describe('Vis Class', function() { describe('setState()', function() { it('should set the state to defaults', function() { - const vis = new visualizations.Vis(indexPattern); + const vis = visualizations.createVis(indexPattern); expect(vis).to.have.property('type'); expect(vis.type).to.eql(visTypes.get('histogram')); expect(vis).to.have.property('aggs'); @@ -100,7 +100,7 @@ describe('Vis Class', function() { expect(vis.isHierarchical()).to.be(true); }); it('should return false for non-hierarchical vis (like histogram)', function() { - const vis = new visualizations.Vis(indexPattern); + const vis = visualizations.createVis(indexPattern); expect(vis.isHierarchical()).to.be(false); }); }); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts index d9af5122eadec..92a9ce8366f4f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy/update_status.ts @@ -18,7 +18,7 @@ */ import { PersistedState } from '../../../../../../../plugins/visualizations/public'; -import { calculateObjectHash } from './calculate_object_hash'; +import { calculateObjectHash } from '../../../../../../../plugins/kibana_utils/common'; import { Vis } from '../vis'; enum Status { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index 8d7407b6191d6..9e8eac08c33ea 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -28,23 +28,19 @@ import { usageCollectionPluginMock } from '../../../../../../plugins/usage_colle import { uiActionsPluginMock } from '../../../../../../plugins/ui_actions/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ - types: { - createBaseVisualization: jest.fn(), - createReactVisualization: jest.fn(), - registerAlias: jest.fn(), - hideTypes: jest.fn(), - }, + createBaseVisualization: jest.fn(), + createReactVisualization: jest.fn(), + registerAlias: jest.fn(), + hideTypes: jest.fn(), }); const createStartContract = (): VisualizationsStart => ({ - types: { - get: jest.fn(), - all: jest.fn(), - getAliases: jest.fn(), - }, + get: jest.fn(), + all: jest.fn(), + getAliases: jest.fn(), savedVisualizationsLoader: {} as any, showNewVisModal: jest.fn(), - Vis: jest.fn(), + createVis: jest.fn(), }); const createInstance = async () => { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 10797a1a04df4..b8db611f30815 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -48,27 +48,27 @@ import { visualization as visualizationRenderer } from './expressions/visualizat import { DataPublicPluginSetup, DataPublicPluginStart, + IIndexPattern, } from '../../../../../../plugins/data/public'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/public'; import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; -import { VisImpl, VisImplConstructor } from './vis_impl'; +import { VisImpl } from './vis_impl'; import { showNewVisModal } from './wizard'; import { UiActionsStart } from '../../../../../../plugins/ui_actions/public'; import { DataStart as LegacyDataStart } from '../../../../data/public'; +import { VisState } from './types'; /** * Interface for this plugin's returned setup/start contracts. * * @public */ -export interface VisualizationsSetup { - types: TypesSetup; -} -export interface VisualizationsStart { - types: TypesStart; +export type VisualizationsSetup = TypesSetup; + +export interface VisualizationsStart extends TypesStart { savedVisualizationsLoader: SavedVisualizationsLoader; - Vis: VisImplConstructor; + createVis: (indexPattern: IIndexPattern, visState?: VisState) => VisImpl; showNewVisModal: typeof showNewVisModal; } @@ -122,7 +122,7 @@ export class VisualizationsPlugin embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); return { - types: this.types.setup(), + ...this.types.setup(), }; } @@ -152,9 +152,15 @@ export class VisualizationsPlugin setSavedVisualizationsLoader(savedVisualizationsLoader); return { - types, + ...types, showNewVisModal, - Vis: VisImpl, + /** + * creates new instance of Vis + * @param {IIndexPattern} indexPattern - index pattern to use + * @param {VisState} visState - visualization configuration + */ + createVis: (indexPattern: IIndexPattern, visState?: VisState) => + new VisImpl(indexPattern, visState), savedVisualizationsLoader, }; } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts index 2458ed5008ddd..e381a01edef8b 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/saved_visualizations/_saved_vis.ts @@ -29,7 +29,8 @@ import { SavedObject, SavedObjectKibanaServices, } from '../../../../../../../plugins/saved_objects/public'; -import { updateOldState } from '../../../index'; +// @ts-ignore +import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; import { IIndexPattern } from '../../../../../../../plugins/data/public'; import { VisSavedObject } from '../types'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts index 0cae83afb7861..6bcaa9a3e1dac 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/vis_types/types_service.ts @@ -67,15 +67,32 @@ export class TypesService { this.types[visDefinition.name] = visDefinition; }; return { + /** + * registers a visualization type + * @param {VisType} config - visualization type definition + */ createBaseVisualization: (config: any) => { const vis = new BaseVisType(config); registerVisualization(() => vis); }, + /** + * registers a visualization which uses react for rendering + * @param {VisType} config - visualization type definition + */ createReactVisualization: (config: any) => { const vis = new ReactVisType(config); registerVisualization(() => vis); }, + /** + * registers a visualization alias + * alias is a visualization type without implementation, it just redirects somewhere in kibana + * @param {VisTypeAlias} config - visualization alias definition + */ registerAlias: visTypeAliasRegistry.add, + /** + * allows to hide specific visualization types from create visualization dialog + * @param {string[]} typeNames - list of type ids to hide + */ hideTypes: (typeNames: string[]) => { typeNames.forEach((name: string) => { if (this.types[name]) { @@ -90,12 +107,22 @@ export class TypesService { public start() { return { + /** + * returns specific visualization or undefined if not found + * @param {string} visualization - id of visualization to return + */ get: (visualization: string) => { return this.types[visualization]; }, + /** + * returns all registered visualization types + */ all: () => { return [...Object.values(this.types)]; }, + /** + * returns all registered aliases + */ getAliases: visTypeAliasRegistry.get, }; } diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx index a79c6ad98edf6..6b37845f03db1 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx @@ -29,6 +29,11 @@ export interface ShowNewVisModalParams { onClose?: () => void; } +/** + * shows modal dialog that allows you to create new visualization + * @param {string[]} editorParams + * @param {function} onClose - function that will be called when dialog is closed + */ export function showNewVisModal({ editorParams = [], onClose }: ShowNewVisModalParams = {}) { const container = document.createElement('div'); let isClosed = false; diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index ee88ad95eeff0..43461c4c689be 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -66,7 +66,7 @@ import { ScriptingHelpFlyout } from './components/scripting_help'; import { FieldFormatEditor } from './components/field_format_editor'; import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants'; -import { copyField, getDefaultFormat, executeScript, isScriptValid } from './lib'; +import { copyField, executeScript, isScriptValid } from './lib'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -76,6 +76,25 @@ import 'brace/mode/groovy'; const getFieldFormats = () => npStart.plugins.data.fieldFormats; +const getFieldTypeFormatsList = (field, defaultFieldFormat) => { + const fieldFormats = getFieldFormats(); + const formatsByType = fieldFormats.getByFieldType(field.type).map(({ id, title }) => ({ + id, + title, + })); + + return [ + { + id: '', + defaultFieldFormat, + title: i18n.translate('common.ui.fieldEditor.defaultFormatDropDown', { + defaultMessage: '- Default -', + }), + }, + ...formatsByType, + ]; +}; + export class FieldEditor extends PureComponent { static propTypes = { indexPattern: PropTypes.object.isRequired, @@ -137,11 +156,7 @@ export class FieldEditor extends PureComponent { field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; const fieldFormats = getFieldFormats(); - - const fieldTypeFormats = [ - getDefaultFormat(fieldFormats.getDefaultType(field.type, field.esTypes)), - ...fieldFormats.getByFieldType(field.type), - ]; + const DefaultFieldFormat = fieldFormats.getDefaultType(field.type, field.esTypes); this.setState({ isReady: true, @@ -150,14 +165,14 @@ export class FieldEditor extends PureComponent { errors: [], scriptingLangs, fieldTypes, - fieldTypeFormats, + fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat), fieldFormatId: get(indexPattern, ['fieldFormatMap', field.name, 'type', 'id']), fieldFormatParams: field.format.params(), }); } onFieldChange = (fieldName, value) => { - const field = this.state.field; + const { field } = this.state; field[fieldName] = value; this.forceUpdate(); }; @@ -169,18 +184,11 @@ export class FieldEditor extends PureComponent { const DefaultFieldFormat = fieldFormats.getDefaultType(type); field.type = type; - - const fieldTypeFormats = [ - getDefaultFormat(DefaultFieldFormat), - ...getFieldFormats().getByFieldType(field.type), - ]; - - const FieldFormat = fieldTypeFormats[0]; - field.format = new FieldFormat(null, getConfig); + field.format = new DefaultFieldFormat(null, getConfig); this.setState({ - fieldTypeFormats, - fieldFormatId: FieldFormat.id, + fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat), + fieldFormatId: DefaultFieldFormat.id, fieldFormatParams: field.format.params(), }); }; @@ -197,12 +205,13 @@ export class FieldEditor extends PureComponent { }; onFormatChange = (formatId, params) => { - const { getConfig } = this.props.helpers; + const fieldFormats = getFieldFormats(); const { field, fieldTypeFormats } = this.state; - const FieldFormat = - fieldTypeFormats.find(format => format.id === formatId) || fieldTypeFormats[0]; + const FieldFormat = fieldFormats.getType( + formatId || fieldTypeFormats[0]?.defaultFieldFormat.id + ); - field.format = new FieldFormat(params, getConfig); + field.format = new FieldFormat(params, this.props.helpers.getConfig); this.setState({ fieldFormatId: FieldFormat.id, @@ -416,7 +425,8 @@ export class FieldEditor extends PureComponent { renderFormat() { const { field, fieldTypeFormats, fieldFormatId, fieldFormatParams } = this.state; const { fieldFormatEditors } = this.props.helpers; - const defaultFormat = fieldTypeFormats[0] && fieldTypeFormats[0].resolvedTitle; + const defaultFormat = fieldTypeFormats[0]?.defaultFieldFormat.title; + const label = defaultFormat ? ( { - return '0,0.[000]'; -}; - -describe('getDefaultFormat', () => { - it('should create default format', () => { - const DefaultFormat = getDefaultFormat(fieldFormats.NumberFormat); - const defaultFormatObject = new DefaultFormat(null, getConfig); - const formatObject = new fieldFormats.NumberFormat(null, getConfig); - - expect(DefaultFormat.id).toEqual(''); - expect(DefaultFormat.resolvedTitle).toEqual(fieldFormats.NumberFormat.title); - expect(DefaultFormat.title).toEqual('- Default -'); - expect(JSON.stringify(defaultFormatObject.params())).toEqual( - JSON.stringify(formatObject.params()) - ); - }); -}); diff --git a/src/legacy/ui/public/field_editor/lib/get_default_format.js b/src/legacy/ui/public/field_editor/lib/get_default_format.js deleted file mode 100644 index acb7ab9c6afa5..0000000000000 --- a/src/legacy/ui/public/field_editor/lib/get_default_format.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. - */ - -import { i18n } from '@kbn/i18n'; - -export const getDefaultFormat = Format => { - class DefaultFormat extends Format { - static id = ''; - static resolvedTitle = Format.title; - static title = i18n.translate('common.ui.fieldEditor.defaultFormatDropDown', { - defaultMessage: '- Default -', - }); - } - - return DefaultFormat; -}; diff --git a/src/legacy/ui/public/field_editor/lib/index.js b/src/legacy/ui/public/field_editor/lib/index.js index 5e12d51763a18..c74bb0cc2ef8a 100644 --- a/src/legacy/ui/public/field_editor/lib/index.js +++ b/src/legacy/ui/public/field_editor/lib/index.js @@ -18,5 +18,4 @@ */ export { copyField } from './copy_field'; -export { getDefaultFormat } from './get_default_format'; export { executeScript, isScriptValid } from './validate_script'; diff --git a/src/plugins/data/common/es_query/filters/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts index 4bf7e1c9c6ba7..03167f3080419 100644 --- a/src/plugins/data/common/es_query/filters/get_display_value.ts +++ b/src/plugins/data/common/es_query/filters/get_display_value.ts @@ -18,6 +18,7 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { IIndexPattern, IFieldType } from '../..'; import { getIndexPatternFromFilter } from './get_index_pattern_from_filter'; import { Filter } from '../filters'; @@ -27,7 +28,16 @@ function getValueFormatter(indexPattern?: IIndexPattern, key?: string) { let format = get(indexPattern, ['fields', 'byName', key, 'format']); if (!format && (indexPattern.fields as any).getByName) { // TODO: Why is indexPatterns sometimes a map and sometimes an array? - format = ((indexPattern.fields as any).getByName(key) as IFieldType).format; + const field: IFieldType = (indexPattern.fields as any).getByName(key); + if (!field) { + throw new Error( + i18n.translate('data.filter.filterBar.fieldNotFound', { + defaultMessage: 'Field {key} not found in index pattern {indexPattern}', + values: { key, indexPattern: indexPattern.title }, + }) + ); + } + format = field.format; } return format; } diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index a5f4ce2ce3c58..86cc0cca85e0b 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -283,8 +283,39 @@ export { * Search: */ -export { IRequestTypesMap, IResponseTypesMap } from './search'; -export * from './search'; +export { + ES_SEARCH_STRATEGY, + SYNC_SEARCH_STRATEGY, + defaultSearchStrategy, + esSearchStrategyProvider, + getEsPreference, + addSearchStrategy, + hasSearchStategyForIndexPattern, + getSearchErrorType, + ISearchContext, + TSearchStrategyProvider, + ISearchStrategy, + ISearch, + ISearchOptions, + IRequestTypesMap, + IResponseTypesMap, + ISearchGeneric, + IEsSearchResponse, + IEsSearchRequest, + ISyncSearchRequest, + IKibanaSearchResponse, + IKibanaSearchRequest, + SearchRequest, + SearchResponse, + SearchError, + SearchStrategyProvider, + ISearchSource, + SearchSource, + SearchSourceFields, + EsQuerySortValue, + SortDirection, + FetchOptions, +} from './search'; /* * UI components diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts index cd7058b9f8f1c..77e5b0ab02dc1 100644 --- a/src/plugins/data/public/query/state_sync/sync_state_with_url.ts +++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.ts @@ -85,7 +85,10 @@ export const syncQueryStateWithUrl = ( stateContainer: { ...globalQueryStateContainer, set: state => { - globalQueryStateContainer.set(state || defaultState); + if (state) { + // syncState utils requires to handle incoming "null" value + globalQueryStateContainer.set(state); + } }, }, storageKey: GLOBAL_STATE_STORAGE_KEY, diff --git a/src/plugins/data/public/search/fetch/types.ts b/src/plugins/data/public/search/fetch/types.ts index 62eb965703c3a..e8de0576b8a72 100644 --- a/src/plugins/data/public/search/fetch/types.ts +++ b/src/plugins/data/public/search/fetch/types.ts @@ -17,8 +17,8 @@ * under the License. */ -import { ISearchStart } from 'src/plugins/data/public'; import { IUiSettingsClient } from '../../../../../core/public'; +import { ISearchStart } from '../types'; export interface FetchOptions { abortSignal?: AbortSignal; diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 51204e2a61168..24adf0093af95 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -32,6 +32,15 @@ font-style: italic; } +.globalFilterItem-isInvalid { + text-decoration: none; + + .globalFilterLabel__value { + color: $euiColorDanger; + font-weight: $euiFontWeightBold; + } +} + .globalFilterItem-isPinned { position: relative; diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index ee6d178b25c22..070631354d8b8 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -41,6 +41,10 @@ export function FilterLabel({ filter, valueLabel }: Props) { prefixText ); + const getValue = (text?: string) => { + return {text}; + }; + if (filter.meta.alias !== null) { return ( @@ -55,35 +59,35 @@ export function FilterLabel({ filter, valueLabel }: Props) { return ( {prefix} - {filter.meta.key} {existsOperator.message} + {filter.meta.key}: {getValue(`${existsOperator.message}`)} ); case FILTERS.GEO_BOUNDING_BOX: return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); case FILTERS.GEO_POLYGON: return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); case FILTERS.PHRASES: return ( {prefix} - {filter.meta.key} {isOneOfOperator.message} {valueLabel} + {filter.meta.key}: {getValue(`${isOneOfOperator.message} ${valueLabel}`)} ); case FILTERS.QUERY_STRING: return ( {prefix} - {valueLabel} + {getValue(`${valueLabel}`)} ); case FILTERS.PHRASE: @@ -91,14 +95,14 @@ export function FilterLabel({ filter, valueLabel }: Props) { return ( {prefix} - {filter.meta.key}: {valueLabel} + {filter.meta.key}: {getValue(valueLabel)} ); default: return ( {prefix} - {JSON.stringify(filter.query) || filter.meta.value} + {getValue(`${JSON.stringify(filter.query) || filter.meta.value}`)} ); } diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 0febfe807a946..6b5fd41dc06ea 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -33,6 +33,7 @@ import { toggleFilterPinned, toggleFilterDisabled, } from '../../../common'; +import { getNotifications } from '../../services'; interface Props { id: string; @@ -64,24 +65,41 @@ class FilterItemUI extends Component { public render() { const { filter, id } = this.props; const { negate, disabled } = filter.meta; + let hasError: boolean = false; + + let valueLabel; + try { + valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); + } catch (e) { + getNotifications().toasts.addError(e, { + title: this.props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorMessage', + defaultMessage: 'Failed to display filter', + }), + }); + valueLabel = this.props.intl.formatMessage({ + id: 'data.filter.filterBar.labelErrorText', + defaultMessage: 'Error', + }); + hasError = true; + } + const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; + const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; + const dataTestSubjDisabled = `filter-${ + this.props.filter.meta.disabled ? 'disabled' : 'enabled' + }`; const classes = classNames( 'globalFilterItem', { - 'globalFilterItem-isDisabled': disabled, + 'globalFilterItem-isDisabled': disabled || hasError, + 'globalFilterItem-isInvalid': hasError, 'globalFilterItem-isPinned': isFilterPinned(filter), 'globalFilterItem-isExcluded': negate, }, this.props.className ); - const valueLabel = getDisplayValueFromFilter(filter, this.props.indexPatterns); - const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; - const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; - const dataTestSubjDisabled = `filter-${ - this.props.filter.meta.disabled ? 'disabled' : 'enabled' - }`; - const badge = ( { it('should show bars in the correct time zone after switching', async function() { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); - await browser.refresh(); + await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); await PageObjects.timePicker.setDefaultAbsoluteRange(); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js index 2976a6cd98e30..643d15c982792 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js @@ -22,7 +22,7 @@ import { SelfChangingComponent } from './self_changing_components'; import { setup as visualizations } from '../../../../../../src/legacy/core_plugins/visualizations/public/np_ready/public/legacy'; -visualizations.types.createReactVisualization({ +visualizations.createReactVisualization({ name: 'self_changing_vis', title: 'Self Changing Vis', icon: 'controlsHorizontal', diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx new file mode 100644 index 0000000000000..418430e37b21e --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -0,0 +1,43 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; + +const EmptyBannerCallOut = styled(EuiCallOut)` + margin: ${lightTheme.gutterTypes.gutterSmall}; + /* Add some extra margin so it displays to the right of the controls. */ + margin-left: calc( + ${lightTheme.gutterTypes.gutterLarge} + + ${lightTheme.gutterTypes.gutterExtraLarge} + ); + position: absolute; + z-index: 1; +`; + +export function EmptyBanner() { + return ( + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.message', { + defaultMessage: + "We will map out connected services and external requests if we can detect them. Please make sure you're running the latest version of the APM agent." + })}{' '} + + {i18n.translate('xpack.apm.serviceMap.emptyBanner.docsLink', { + defaultMessage: 'Learn more in the docs' + })} + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx new file mode 100644 index 0000000000000..926f53954e7c6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -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 { render } from '@testing-library/react'; +import React, { FunctionComponent } from 'react'; +import { License } from '../../../../../../../plugins/licensing/common/license'; +import { LicenseContext } from '../../../context/LicenseContext'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; +import { ServiceMap } from './'; + +const expiredLicense = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'expired', + type: 'platinum', + uid: '1' + } +}); + +const Wrapper: FunctionComponent = ({ children }) => { + return ( + + {children} + + ); +}; + +describe('ServiceMap', () => { + describe('with an inactive license', () => { + it('renders the license banner', async () => { + expect( + ( + await render(, { + wrapper: Wrapper + }).findAllByText(/Platinum/) + ).length + ).toBeGreaterThan(0); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 9a93c67f08187..2942ce64729e7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -26,13 +26,14 @@ import { useLicense } from '../../../hooks/useLicense'; import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; +import { EmptyBanner } from './EmptyBanner'; import { getCytoscapeElements } from './get_cytoscape_elements'; import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; -import { callApmApi } from '../../../services/rest/createCallApmApi'; interface ServiceMapProps { serviceName?: string; @@ -214,6 +215,9 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={cytoscapeDivStyle} > + {serviceName && renderedElements.current.length === 1 && ( + + )} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 7645162ab2655..0e0c318ad3299 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -23,7 +23,7 @@ export function ElasticDocsLink({ section, path, children, ...rest }: Props) { children(href) ) : ( - children + {children} ); } diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 7f96268fc2e8c..7afe6d7abedc0 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -103,7 +103,7 @@ export class LensPlugin { this.datatableVisualization.setup(core, dependencies); this.metricVisualization.setup(core, dependencies); - visualizations.types.registerAlias(getLensAliasConfig()); + visualizations.registerAlias(getLensAliasConfig()); kibanaLegacy.registerLegacyApp({ id: 'lens', diff --git a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js b/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js index b0e62d37fbf1c..4d87b6a055802 100644 --- a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js +++ b/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js @@ -23,7 +23,7 @@ The Maps app offers more functionality and is easier to use.`, } ); -visualizationsSetup.types.registerAlias({ +visualizationsSetup.registerAlias({ aliasUrl: MAP_BASE_URL, name: APP_ID, title: i18n.translate('xpack.maps.visTypeAlias.title', { @@ -37,5 +37,5 @@ visualizationsSetup.types.registerAlias({ }); if (!showMapVisualizationTypes) { - visualizationsSetup.types.hideTypes(['region_map', 'tile_map']); + visualizationsSetup.hideTypes(['region_map', 'tile_map']); } diff --git a/x-pack/legacy/plugins/ml/common/types/jobs.test.js b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/combined_job.test.ts similarity index 59% rename from x-pack/legacy/plugins/ml/common/types/jobs.test.js rename to x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/combined_job.test.ts index 02a6500403cf3..dce107a492318 100644 --- a/x-pack/legacy/plugins/ml/common/types/jobs.test.js +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/combined_job.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import jobConfigFarequote from './__mocks__/job_config_farequote'; -import { isMlJob, isMlJobs } from './jobs'; +// @ts-ignore importing JSON file +import jobConfigFarequote from '../__mocks__/job_config_farequote'; +import { isCombinedJobWithStats } from './combined_job'; describe('Types: Jobs', () => { test('Minimal integrity check.', () => { - expect(isMlJob(jobConfigFarequote)).toBe(true); - expect(isMlJobs([jobConfigFarequote])).toBe(true); + expect(isCombinedJobWithStats(jobConfigFarequote)).toBe(true); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/combined_job.ts similarity index 63% rename from x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts rename to x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/combined_job.ts index 435b7696af398..6df7643c5221f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/combined_job.ts +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/combined_job.ts @@ -6,14 +6,25 @@ import { cloneDeep } from 'lodash'; import { Datafeed } from './datafeed'; +import { DatafeedStats } from './datafeed_stats'; import { Job } from './job'; +import { JobStats } from './job_stats'; + +export type JobWithStats = Job & JobStats; +export type DatafeedWithStats = Datafeed & DatafeedStats; // in older implementations of the job config, the datafeed was placed inside the job // for convenience. export interface CombinedJob extends Job { + calendars?: string[]; datafeed_config: Datafeed; } +export interface CombinedJobWithStats extends JobWithStats { + calendars?: string[]; + datafeed_config: DatafeedWithStats; +} + export function expandCombinedJobConfig(combinedJob: CombinedJob) { const combinedJobClone = cloneDeep(combinedJob); const job = combinedJobClone; @@ -22,3 +33,7 @@ export function expandCombinedJobConfig(combinedJob: CombinedJob) { return { job, datafeed }; } + +export function isCombinedJobWithStats(arg: any): arg is CombinedJobWithStats { + return typeof arg.job_id === 'string'; +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts similarity index 85% rename from x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts rename to x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts index 538b225926f65..47ff618ffa77f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/datafeed.ts +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/datafeed.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; +import { IndexPatternTitle } from '../kibana'; import { JobId } from './job'; export type DatafeedId = string; @@ -15,11 +15,8 @@ export interface Datafeed { chunking_config?: ChunkingConfig; frequency?: string; indices: IndexPatternTitle[]; - /** - * The datafeed can contain indexes and indices - */ - indexes?: IndexPatternTitle[]; - job_id?: JobId; + indexes?: IndexPatternTitle[]; // The datafeed can contain indexes and indices + job_id: JobId; query: object; query_delay?: string; script_fields?: object; diff --git a/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts new file mode 100644 index 0000000000000..f1bb56b5337ab --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/datafeed_stats.ts @@ -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 { Node } from './job_stats'; +import { DATAFEED_STATE } from '../../constants/states'; + +export interface DatafeedStats { + datafeed_id: string; + state: DATAFEED_STATE; + node: Node; + assignment_explanation: string; + timing_stats: TimingStats; +} + +interface TimingStats { + job_id: string; + search_count: number; + bucket_count: number; + total_search_time_ms: number; + average_search_time_per_bucket_ms: number; + exponential_average_search_time_per_hour_ms: number; +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/index.ts similarity index 77% rename from x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts rename to x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/index.ts index c8b71ead4b6fb..9c299c628426a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/index.ts +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/index.ts @@ -5,5 +5,8 @@ */ export * from './job'; +export * from './job_stats'; export * from './datafeed'; +export * from './datafeed_stats'; export * from './combined_job'; +export * from './summary_job'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/job.ts similarity index 85% rename from x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts rename to x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/job.ts index 3246f8ae4b31a..823d27e4617b2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/job.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UrlConfig } from '../../../../../../../common/types/custom_urls'; -import { CREATED_BY_LABEL } from '../../../../../../../common/constants/new_job'; +import { UrlConfig } from '../custom_urls'; +import { CREATED_BY_LABEL } from '../../constants/new_job'; export type JobId = string; export type BucketSpan = string; @@ -29,6 +29,14 @@ export interface Job { renormalization_window_days?: number; results_index_name?: string; results_retention_days?: number; + + // optional properties added when the job has been created + create_time?: number; + finished_time?: number; + job_type?: 'anomaly_detector'; + job_version?: string; + model_snapshot_id?: string; + deleting?: boolean; } export interface AnalysisConfig { diff --git a/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts new file mode 100644 index 0000000000000..2d64e70bb1f78 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/job_stats.ts @@ -0,0 +1,95 @@ +/* + * 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_STATE } from '../../constants/states'; + +export interface JobStats { + job_id: string; + data_counts: DataCounts; + model_size_stats: ModelSizeStats; + forecasts_stats: ForecastsStats; + state: JOB_STATE; + node: Node; + assignment_explanation: string; + open_time: string; + timing_stats: TimingStats; +} + +export interface DataCounts { + job_id: string; + processed_record_count: number; + processed_field_count: number; + input_bytes: number; + input_field_count: number; + invalid_date_count: number; + missing_field_count: number; + out_of_order_timestamp_count: number; + empty_bucket_count: number; + sparse_bucket_count: number; + bucket_count: number; + earliest_record_timestamp: number; + latest_record_timestamp: number; + last_data_time: number; + input_record_count: number; + latest_empty_bucket_timestamp: number; + latest_sparse_bucket_timestamp: number; + latest_bucket_timestamp?: number; // stat added by the UI +} + +export interface ModelSizeStats { + job_id: string; + result_type: string; + model_bytes: number; + model_bytes_exceeded: number; + model_bytes_memory_limit: number; + total_by_field_count: number; + total_over_field_count: number; + total_partition_field_count: number; + bucket_allocation_failures_count: number; + memory_status: 'ok' | 'soft_limit' | 'hard_limit'; + categorized_doc_count: number; + total_category_count: number; + frequent_category_count: number; + rare_category_count: number; + dead_category_count: number; + categorization_status: 'ok' | 'warn'; + log_time: number; + timestamp: number; +} + +export interface ForecastsStats { + total: number; + forecasted_jobs: number; + memory_bytes?: any; + records?: any; + processing_time_ms?: any; + status?: any; +} + +export interface Node { + id: string; + name: string; + ephemeral_id: string; + transport_address: string; + attributes: { + 'transform.remote_connect'?: boolean; + 'ml.machine_memory'?: number; + 'xpack.installed'?: boolean; + 'transform.node'?: boolean; + 'ml.max_open_jobs'?: number; + }; +} + +interface TimingStats { + job_id: string; + bucket_count: number; + total_bucket_processing_time_ms: number; + minimum_bucket_processing_time_ms: number; + maximum_bucket_processing_time_ms: number; + average_bucket_processing_time_ms: number; + exponential_average_bucket_processing_time_ms: number; + exponential_average_bucket_processing_time_per_hour_ms: number; +} diff --git a/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts new file mode 100644 index 0000000000000..6cf109dc553ae --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -0,0 +1,57 @@ +/* + * 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 { CombinedJob, CombinedJobWithStats } from './combined_job'; +export { Datafeed } from './datafeed'; +export { DatafeedStats } from './datafeed_stats'; + +export interface MlSummaryJob { + id: string; + description: string; + groups: string[]; + processed_record_count?: number; + memory_status?: string; + jobState: string; + datafeedIndices: string[]; + hasDatafeed: boolean; + datafeedId: string; + datafeedState: string; + latestTimestampMs?: number; + earliestTimestampMs?: number; + latestResultsTimestampMs?: number; + fullJob?: CombinedJob; + nodeName?: string; + auditMessage?: Partial; + isSingleMetricViewerJob: boolean; + deleting?: boolean; + latestTimestampSortValue?: number; +} + +export interface AuditMessage { + job_id: string; + msgTime: number; + level: number; + highestLevel: number; + highestLevelText: string; + text: string; +} + +export type MlSummaryJobs = MlSummaryJob[]; + +export interface MlJobWithTimeRange extends CombinedJobWithStats { + timeRange: { + from: number; + to: number; + fromPx: number; + toPx: number; + fromMoment: Moment; + toMoment: Moment; + widthPx: number; + label: string; + }; +} diff --git a/x-pack/legacy/plugins/ml/common/types/jobs.ts b/x-pack/legacy/plugins/ml/common/types/jobs.ts deleted file mode 100644 index a9885048550bb..0000000000000 --- a/x-pack/legacy/plugins/ml/common/types/jobs.ts +++ /dev/null @@ -1,97 +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 { Moment } from 'moment'; - -// TS TODO: This is not yet a fully fledged representation of the job data structure, -// but it fulfills some basic TypeScript related needs. -export interface MlJob { - analysis_config: { - bucket_span: string; - detectors: object[]; - influencers: string[]; - }; - analysis_limits: { - categorization_examples_limit: number; - model_memory_limit: string; - }; - create_time: number; - custom_settings: object; - data_counts: { - earliest_record_timestamp: number; - latest_record_timestamp: number; - }; - data_description: { - time_field: string; - time_format: string; - }; - datafeed_config: object; - description: string; - established_model_memory: number; - finished_time: number; - job_id: string; - job_type: string; - job_version: string; - model_plot_config: object; - model_size_stats: object; - model_snapshot_id: string; - model_snapshot_min_version: string; - model_snapshot_retention_days: number; - results_index_name: string; - state: string; -} - -export interface MlSummaryJob { - id: string; - description: string; - groups: string[]; - processed_record_count: number; - memory_status?: string; - jobState: string; - hasDatafeed: boolean; - datafeedId?: string; - datafeedIndices: any[]; - datafeedState?: string; - latestTimestampMs: number; - earliestTimestampMs?: number; - latestResultsTimestampMs: number; - isSingleMetricViewerJob: boolean; - nodeName?: string; - deleting?: boolean; - fullJob?: any; - auditMessage?: any; - latestTimestampSortValue?: number; -} - -export type MlSummaryJobs = MlSummaryJob[]; - -export interface MlJobWithTimeRange extends MlJob { - groups: string[]; - timeRange: { - from: number; - to: number; - fromPx: number; - toPx: number; - fromMoment: Moment; - toMoment: Moment; - widthPx: number; - label: string; - }; -} - -export function isMlJob(arg: any): arg is MlJob { - return typeof arg.job_id === 'string'; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MlJobs extends Array {} - -export function isMlJobs(arg: any): arg is MlJobs { - if (Array.isArray(arg) === false) { - return false; - } - return arg.every((d: MlJob) => isMlJob(d)); -} diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index 3e1a2cf9ab2e6..87e19d09da30c 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectAttributes } from 'src/core/public'; -import { Datafeed, Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; +import { Datafeed, Job } from '../types/anomaly_detection_jobs'; export interface ModuleJob { id: string; diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts index 7dcd4b20fe0bf..bfad422e0ab48 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; +import { Job } from '../types/anomaly_detection_jobs'; export interface ValidationMessage { id: string; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.ts b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.ts index 1484f0a391b67..3a215f8cfb46d 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_select_service_utils.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import d3 from 'd3'; import { Dictionary } from '../../../../common/types/common'; -import { MlJobWithTimeRange } from '../../../../common/types/jobs'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; export function getGroupsFromJobs(jobs: MlJobWithTimeRange[]) { const groups: Dictionary = {}; diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx index bd2ec2d1511a3..bd75b7be4d5ef 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { useMlKibana } from '../../contexts/kibana'; import { Dictionary } from '../../../../common/types/common'; -import { MlJobWithTimeRange } from '../../../../common/types/jobs'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { ml } from '../../services/ml_api_service'; import { useUrlState } from '../../util/url_state'; // @ts-ignore diff --git a/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 214bb90917302..d3fad9ae6bc2c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -10,7 +10,7 @@ import { useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { getToastNotifications } from '../../util/dependency_cache'; -import { MlJobWithTimeRange } from '../../../../common/types/jobs'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { useUrlState } from '../../util/url_state'; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts index c60b2d55d8686..2d3c9a389110a 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_utils.d.ts @@ -6,7 +6,7 @@ import { Moment } from 'moment'; -import { CombinedJob } from '../jobs/new_job/common/job_creator/configs'; +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { TimeBucketsInterval } from '../util/time_buckets'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx index 42ee18bafd870..923d3bf5ce59f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Job } from '../../new_job/common/job_creator/configs'; +import { Job } from '../../../../../common/types/anomaly_detection_jobs'; import { CustomUrlList, CustomUrlListProps } from './list'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx index 4c0956a46d669..1b18afaf2569f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx @@ -25,7 +25,7 @@ import { getTestUrl } from './utils'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { TIME_RANGE_TYPE } from './constants'; import { UrlConfig, KibanaUrlConfig } from '../../../../../common/types/custom_urls'; -import { Job } from '../../new_job/common/job_creator/configs'; +import { Job } from '../../../../../common/types/anomaly_detection_jobs'; function isValidTimeRange(timeRange: KibanaUrlConfig['time_range']): boolean { // Allow empty timeRange string, which gives the 'auto' behaviour. diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts index cce0c7c29912c..8ae61a01d5e42 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts @@ -6,7 +6,7 @@ import { IIndexPattern } from 'src/plugins/data/common'; import { UrlConfig } from '../../../../../common/types/custom_urls'; -import { Job } from '../../new_job/common/job_creator/configs'; +import { Job } from '../../../../../common/types/anomaly_detection_jobs'; import { TimeRangeType } from './constants'; export interface TimeRange { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index fe6f72fd10279..dd85934d11150 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -35,7 +35,7 @@ import { import { withKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { loadSavedDashboards, loadIndexPatterns } from '../edit_utils'; import { openCustomUrlWindow } from '../../../../../util/custom_url_utils'; -import { Job } from '../../../../new_job/common/job_creator/configs'; +import { Job } from '../../../../../../../common/types/anomaly_detection_jobs'; import { UrlConfig } from '../../../../../../../common/types/custom_urls'; import { IIndexPattern } from '../../../../../../../../../../../src/plugins/data/common/index_patterns'; import { MlKibanaReactContextValue } from '../../../../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 4530c00c10535..099f587caa051 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -8,7 +8,12 @@ import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, Aggregation, SplitField } from '../../../../../../common/types/fields'; -import { Job, Datafeed, Detector, CustomRule } from './configs'; +import { + Job, + Datafeed, + Detector, + CustomRule, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts index eb2f0f4e368a3..25ec8becd9a05 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts @@ -9,7 +9,7 @@ import { IndexPattern } from '../../../../../../../../../../src/plugins/data/pub import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, Aggregation, mlCategory } from '../../../../../../common/types/fields'; -import { Job, Datafeed, Detector } from './configs'; +import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index 5b33aa3556980..98b8a7904eda9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -9,7 +9,15 @@ import { UrlConfig } from '../../../../../../common/types/custom_urls'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { Job, Datafeed, Detector, JobId, DatafeedId, BucketSpan, CustomSettings } from './configs'; +import { + Job, + Datafeed, + Detector, + JobId, + DatafeedId, + BucketSpan, + CustomSettings, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { Aggregation, Field } from '../../../../../../common/types/fields'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { mlJobService } from '../../../../services/job_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 7c5fba028d9e8..120eee984a10b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -12,7 +12,7 @@ import { SplitField, AggFieldPair, } from '../../../../../../common/types/fields'; -import { Job, Datafeed, Detector } from './configs'; +import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 3009d68ca67ca..032ebc202142d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -12,7 +12,7 @@ import { SplitField, AggFieldPair, } from '../../../../../../common/types/fields'; -import { Job, Datafeed, Detector } from './configs'; +import { Job, Datafeed, Detector } from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index 9f3500185c2bf..b16d624cff463 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -8,7 +8,12 @@ import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; import { Field, Aggregation, AggFieldPair } from '../../../../../../common/types/fields'; -import { Job, Datafeed, Detector, BucketSpan } from './configs'; +import { + Job, + Datafeed, + Detector, + BucketSpan, +} from '../../../../../../common/types/anomaly_detection_jobs'; import { createBasicDetector } from './util/default_configs'; import { ML_JOB_AGGREGATION, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 1160401478ab7..306fd82dc8758 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Job, Datafeed } from '../configs'; import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; -import { Detector } from '../configs'; +import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; export function createEmptyJob(): Job { return { @@ -28,6 +27,7 @@ export function createEmptyJob(): Job { export function createEmptyDatafeed(indexPatternTitle: IndexPatternTitle): Datafeed { return { datafeed_id: '', + job_id: '', indices: [indexPatternTitle], query: {}, }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 0764e276d635e..b07d55c5b8516 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { Job, Datafeed, Detector } from '../configs'; +import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { ML_JOB_AGGREGATION, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts index 9627d2e477528..2571fe70f4a83 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_runner/job_runner.ts @@ -8,7 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import { ml } from '../../../../services/ml_api_service'; import { mlJobService } from '../../../../services/job_service'; import { JobCreator } from '../job_creator'; -import { DatafeedId, JobId } from '../job_creator/configs'; +import { DatafeedId, JobId } from '../../../../../../common/types/anomaly_detection_jobs'; import { DATAFEED_STATE } from '../../../../../../common/constants/states'; const REFRESH_INTERVAL_MS = 100; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index ab33afb23ef51..39e3ef2ae0007 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { BasicValidations } from './job_validator'; -import { Job, Datafeed } from '../job_creator/configs'; +import { Job, Datafeed } from '../../../../../../common/types/anomaly_detection_jobs'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH, diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx index 7f5d2bfbe0e90..03be38adfbbe0 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview_flyout.tsx @@ -18,7 +18,7 @@ import { EuiSpacer, EuiLoadingSpinner, } from '@elastic/eui'; -import { CombinedJob } from '../../../../common/job_creator/configs'; +import { CombinedJob } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { JobCreatorContext } from '../../job_creator_context'; import { mlJobService } from '../../../../../../services/job_service'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index 061d92b54472c..dd5c8aa3e280a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -19,7 +19,7 @@ import { EuiSpacer, } from '@elastic/eui'; import { collapseLiteralStrings } from '../../../../../../../../shared_imports'; -import { Datafeed } from '../../../../common/job_creator/configs'; +import { Datafeed } from '../../../../../../../../common/types/anomaly_detection_jobs'; import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { isValidJson } from '../../../../../../../../common/util/validation_utils'; import { JobCreatorContext } from '../../job_creator_context'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/custom_urls_selection.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/custom_urls_selection.tsx index cd580e60c0843..2e5135d4d8840 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/custom_urls_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/additional_section/components/custom_urls/custom_urls_selection.tsx @@ -9,7 +9,7 @@ import { CustomUrls } from '../../../../../../../../jobs_list/components/edit_jo import { UrlConfig } from '../../../../../../../../../../../common/types/custom_urls'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; -import { CombinedJob } from '../../../../../../../common/job_creator/configs'; +import { CombinedJob } from '../../../../../../../../../../../common/types/anomaly_detection_jobs'; export const CustomUrlsSelection: FC = () => { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx index f996a0e9728ba..38903dd4845a6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_view/detector_list.tsx @@ -25,7 +25,7 @@ import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; import { Validation } from '../../../../../common/job_validator'; import { detectorToString } from '../../../../../../../util/string_utils'; -import { Detector } from '../../../../../common/job_creator/configs'; +import { Detector } from '../../../../../../../../../common/types/anomaly_detection_jobs'; interface Props { isActive: boolean; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 51fc226751ae2..d1a6ca7c19a24 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -7,7 +7,7 @@ import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; import { mlJobService } from '../../../../services/job_service'; import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils'; -import { CombinedJob } from '../../common/job_creator/configs'; +import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index b2383b6c08a58..d851559815de8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -37,7 +37,7 @@ import { useMlContext } from '../../../../contexts/ml'; import { getTimeFilterRange } from '../../../../components/full_time_range_selector'; import { TimeBuckets } from '../../../../util/time_buckets'; import { ExistingJobsAndGroups, mlJobService } from '../../../../services/job_service'; -import { expandCombinedJobConfig } from '../../common/job_creator/configs'; +import { expandCombinedJobConfig } from '../../../../../../common/types/anomaly_detection_jobs'; import { newJobCapsService } from '../../../../services/new_job_capabilities_service'; import { EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; import { getNewJobDefaults } from '../../../../services/ml_server_info'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 8571ae43da587..ac7a2093d1f81 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -41,7 +41,7 @@ import { ModuleJobs } from './components/module_jobs'; import { checkForSavedObjects } from './resolvers'; import { JobSettingsForm, JobSettingsFormValues } from './components/job_settings_form'; import { TimeRange } from '../common/components'; -import { JobId } from '../common/job_creator/configs'; +import { JobId } from '../../../../../common/types/anomaly_detection_jobs'; export interface ModuleJobUI extends ModuleJob { datafeedResult?: DatafeedResponse; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx index f638094cfb434..3a64c623dd129 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -9,7 +9,7 @@ import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; // @ts-ignore no module file import { getLink } from '../../../jobs/jobs_list/components/job_actions/results'; -import { MlSummaryJobs } from '../../../../../common/types/jobs'; +import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs'; interface Props { jobsList: MlSummaryJobs; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index cda03b21b0d65..5f5c3f7c28670 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -21,7 +21,7 @@ import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; import { Dictionary } from '../../../../../common/types/common'; -import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/jobs'; +import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs'; export type GroupsDictionary = Dictionary; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx index cca86e2fc0e3e..cd45721d54a68 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx @@ -27,7 +27,7 @@ import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; import { ExplorerLink } from './actions'; import { getJobsFromGroup } from './utils'; import { GroupsDictionary, Group } from './anomaly_detection_panel'; -import { MlSummaryJobs } from '../../../../../common/types/jobs'; +import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs'; import { StatsBar, JobStatsBarStats } from '../../../components/stats_bar'; // @ts-ignore import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts index 01848bad2670e..eab40c0f577f8 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { Group, GroupsDictionary } from './anomaly_detection_panel'; -import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/jobs'; +import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs'; export function getGroupsFromJobs( jobs: MlSummaryJobs @@ -43,7 +43,7 @@ export function getGroupsFromJobs( // if incoming job latest timestamp is greater than the last saved one, replace it if (groups[g].latest_timestamp === undefined) { groups[g].latest_timestamp = job.latestTimestampMs; - } else if (job.latestTimestampMs > groups[g].latest_timestamp) { + } else if (job.latestTimestampMs && job.latestTimestampMs > groups[g].latest_timestamp) { groups[g].latest_timestamp = job.latestTimestampMs; } } @@ -53,7 +53,7 @@ export function getGroupsFromJobs( groups.ungrouped.docs_processed += job.processed_record_count; groups.ungrouped.jobs_in_group++; // if incoming job latest timestamp is greater than the last saved one, replace it - if (job.latestTimestampMs > groups.ungrouped.latest_timestamp) { + if (job.latestTimestampMs && job.latestTimestampMs > groups.ungrouped.latest_timestamp) { groups.ungrouped.latest_timestamp = job.latestTimestampMs; } } diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx index 2c6726338d2f1..2e355c6073abd 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -9,7 +9,7 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; -import { MlJobWithTimeRange } from '../../../../common/types/jobs'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { MlRoute, PageLoader, PageProps } from '../router'; import { useRefresh } from '../use_refresh'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index f8a6f6c454fc0..a41a6c83615d3 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -11,7 +11,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { MlJobWithTimeRange } from '../../../../common/types/jobs'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { TimeSeriesExplorer } from '../../timeseriesexplorer'; import { getDateFormatTz, TimeRangeBounds } from '../../explorer/explorer_utils'; diff --git a/x-pack/legacy/plugins/ml/public/application/services/calendar_service.ts b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.ts index f17c9f359c7b5..98001fce1cf71 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/calendar_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/calendar_service.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ml } from './ml_api_service'; import { Calendar, CalendarId } from '../../../common/types/calendars'; -import { JobId } from '../jobs/new_job/common/job_creator/configs'; +import { JobId } from '../../../common/types/anomaly_detection_jobs'; class CalendarService { /** * Assigns a job id to the calendar. diff --git a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts index 8de903a422f34..9eff86c753da9 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/forecast_service.d.ts @@ -5,7 +5,7 @@ */ import { Observable } from 'rxjs'; -import { Job } from '../jobs/new_job/common/job_creator/configs'; +import { Job } from '../../../common/types/anomaly_detection_jobs'; export interface ForecastData { success: boolean; diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts index b9ed83eeffba1..2134d157e1baa 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; -import { CombinedJob } from '../jobs/new_job/common/job_creator/configs'; +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import { Calendar } from '../../../common/types/calendars'; export interface ExistingJobsAndGroups { diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index 6cb8eccafe151..97e001389c5f1 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -11,7 +11,6 @@ import { AggFieldNamePair } from '../../../../common/types/fields'; import { Category } from '../../../../common/types/categories'; import { ExistingJobsAndGroups } from '../job_service'; import { PrivilegesResponse } from '../../../../common/types/privileges'; -import { MlJobWithTimeRange, MlSummaryJobs } from '../../../../common/types/jobs'; import { MlServerDefaults, MlServerLimits } from '../ml_server_info'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; @@ -21,7 +20,12 @@ import { DeepPartial } from '../../../../common/types/common'; import { PartitionFieldsDefinition } from '../results_service/result_service_rx'; import { annotations } from './annotations'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; -import { CombinedJob, JobId } from '../../jobs/new_job/common/job_creator/configs'; +import { + MlJobWithTimeRange, + MlSummaryJobs, + CombinedJob, + JobId, +} from '../../../../common/types/anomaly_detection_jobs'; import { CategorizationAnalyzer, CategoryFieldExample, diff --git a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts index 299dfe0167694..8f701a9ebe057 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -16,7 +16,7 @@ import { map } from 'rxjs/operators'; import _ from 'lodash'; import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; -import { JobId } from '../../jobs/new_job/common/job_creator/configs'; +import { JobId } from '../../../../common/types/anomaly_detection_jobs'; import { ml } from '../ml_api_service'; import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns'; import { CriteriaField } from './index'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts index 1f49ec1826422..de294ffc21421 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts @@ -7,10 +7,10 @@ import d3 from 'd3'; import { Annotation } from '../../../../../common/types/annotations'; -import { MlJob } from '../../../../../common/types/jobs'; +import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; interface Props { - selectedJob: MlJob; + selectedJob: CombinedJob; } interface State { diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 65bcc9d355fd6..978f5f68d9d8d 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -14,7 +14,7 @@ import { isModelPlotEnabled } from '../../../common/util/job_utils'; import { buildConfigFromDetector } from '../util/chart_config_builder'; import { mlResultsService } from '../services/results_service'; import { ModelPlotOutput } from '../services/results_service/result_service_rx'; -import { Job } from '../jobs/new_job/common/job_creator/configs'; +import { Job } from '../../../common/types/anomaly_detection_jobs'; function getMetricData( job: Job, diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index 2a4eaf1a545a1..534c8c567359e 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -13,7 +13,7 @@ import { } from '../../../../common/constants/search'; import { mlTimeSeriesSearchService } from '../timeseries_search_service'; import { mlResultsService, CriteriaField } from '../../services/results_service'; -import { Job } from '../../jobs/new_job/common/job_creator/configs'; +import { Job } from '../../../../common/types/anomaly_detection_jobs'; import { MAX_SCHEDULED_EVENTS, TIME_FIELD_NAME } from '../timeseriesexplorer_constants'; import { processDataForFocusAnomalies, diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts index bd8f98e0428a1..d31ae710b025e 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/validate_job_selection.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { getToastNotifications } from '../../util/dependency_cache'; -import { MlJobWithTimeRange } from '../../../../common/types/jobs'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; import { getTimeRangeFromSelection } from '../../components/job_selector/job_select_service_utils'; import { mlJobService } from '../../services/job_service'; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 1a57408f41dd6..f90f2c7aee395 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -3,27 +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 { i18n } from '@kbn/i18n'; +import del from 'del'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { Browser, - Page, - LaunchOptions, ConsoleMessage, + LaunchOptions, + Page, Request as PuppeteerRequest, } from 'puppeteer'; -import del from 'del'; import * as Rx from 'rxjs'; -import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; - +import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { BrowserConfig, CaptureConfig } from '../../../../types'; import { LevelLogger as Logger } from '../../../lib/level_logger'; -import { HeadlessChromiumDriver } from '../driver'; import { safeChildProcess } from '../../safe_child_process'; -import { puppeteerLaunch } from '../puppeteer'; +import { HeadlessChromiumDriver } from '../driver'; import { getChromeLogLocation } from '../paths'; +import { puppeteerLaunch } from '../puppeteer'; import { args } from './args'; type binaryPath = string; @@ -167,7 +168,7 @@ export class HeadlessChromiumDriverFactory { logger.debug(`deleting chromium user data directory at [${userDataDir}]`); // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. - del(userDataDir).catch(error => { + del(userDataDir, { force: true }).catch(error => { logger.error(`error deleting user data directory at [${userDataDir}]: [${error}]`); }); }); @@ -216,17 +217,35 @@ export class HeadlessChromiumDriverFactory { } getPageExit(browser: Browser, page: Page) { - const pageError$ = Rx.fromEvent(page, 'error').pipe(mergeMap(err => Rx.throwError(err))); + const pageError$ = Rx.fromEvent(page, 'error').pipe( + mergeMap(err => { + return Rx.throwError( + i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { + defaultMessage: 'Reporting detected an error: {err}', + values: { err: err.toString() }, + }) + ); + }) + ); const uncaughtExceptionPageError$ = Rx.fromEvent(page, 'pageerror').pipe( - mergeMap(err => Rx.throwError(err)) + mergeMap(err => { + return Rx.throwError( + i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { + defaultMessage: `Reporting detected an error on the page: {err}`, + values: { err: err.toString() }, + }) + ); + }) ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( mergeMap(() => Rx.throwError( new Error( - `Puppeteer was disconnected from the Chromium instance! Chromium has closed or crashed.` + i18n.translate('xpack.reporting.browsers.chromium.chromiumClosed', { + defaultMessage: `Reporting detected that Chromium has closed.`, + }) ) ) ) diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts index 4355a6a0a1773..a2d1fc7f91a29 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/download/clean.ts @@ -31,7 +31,7 @@ export async function clean(dir: string, expectedPaths: string[]) { const path = resolvePath(dir, filename); if (!expectedPaths.includes(path)) { log(`Deleting unexpected file ${path}`); - await del(path); + await del(path, { force: true }); } }); } diff --git a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts new file mode 100644 index 0000000000000..f2ed9d48daaf6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules.spec.ts @@ -0,0 +1,50 @@ +/* + * 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 { ELASTIC_RULES_BTN, RULES_TABLE, RULES_ROW } from '../screens/signal_detection_rules'; + +import { + changeToThreeHundredRowsPerPage, + loadPrebuiltDetectionRules, + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, + waitForPrebuiltDetectionRulesToBeLoaded, + waitForRulesToBeLoaded, +} from '../tasks/signal_detection_rules'; +import { + goToManageSignalDetectionRules, + waitForSignalsIndexToBeCreated, + waitForSignalsPanelToBeLoaded, +} from '../tasks/detections'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('Signal detection rules', () => { + before(() => { + loginAndWaitForPageWithoutDateRange(DETECTIONS); + }); + it('Loads prebuilt rules', () => { + waitForSignalsPanelToBeLoaded(); + waitForSignalsIndexToBeCreated(); + goToManageSignalDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + loadPrebuiltDetectionRules(); + waitForPrebuiltDetectionRulesToBeLoaded(); + + const expectedElasticRulesBtnText = 'Elastic rules (92)'; + cy.get(ELASTIC_RULES_BTN) + .invoke('text') + .should('eql', expectedElasticRulesBtnText); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 92; + cy.get(RULES_TABLE).then($table => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts new file mode 100644 index 0000000000000..8089b028a10d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -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 const LOADING_SIGNALS_PANEL = '[data-test-subj="loading-signals-panel"]'; + +export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal-detection-rules"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts b/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts new file mode 100644 index 0000000000000..bfaa86e83f301 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/signal_detection_rules.ts @@ -0,0 +1,22 @@ +/* + * 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 ELASTIC_RULES_BTN = '[data-test-subj="show-elastic-rules-filter-button"]'; + +export const LOAD_PREBUILT_RULES_BTN = '[data-test-subj="load-prebuilt-rules"]'; + +export const LOADING_INITIAL_PREBUILT_RULES_TABLE = + '[data-test-subj="initialLoadingPanelAllRulesTable"]'; + +export const LOADING_SPINNER = '[data-test-subj="loading-spinner"]'; + +export const PAGINATION_POPOVER_BTN = '[data-test-subj="tablePaginationPopoverButton"]'; + +export const RULES_TABLE = '[data-test-subj="rules-table"]'; + +export const RULES_ROW = '.euiTableRow'; + +export const THREE_HUNDRED_ROWS = '[data-test-subj="tablePagination-300-rows"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts new file mode 100644 index 0000000000000..4a0a565a74e27 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -0,0 +1,28 @@ +/* + * 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 { LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN } from '../screens/detections'; + +export const goToManageSignalDetectionRules = () => { + cy.get(MANAGE_SIGNAL_DETECTION_RULES_BTN) + .should('exist') + .click({ force: true }); +}; + +export const waitForSignalsIndexToBeCreated = () => { + cy.request({ url: '/api/detection_engine/index', retryOnStatusCodeFailure: true }).then( + response => { + if (response.status !== 200) { + cy.wait(7500); + } + } + ); +}; + +export const waitForSignalsPanelToBeLoaded = () => { + cy.get(LOADING_SIGNALS_PANEL).should('exist'); + cy.get(LOADING_SIGNALS_PANEL).should('not.exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts b/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts new file mode 100644 index 0000000000000..cc0e4bce1035a --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/signal_detection_rules.ts @@ -0,0 +1,40 @@ +/* + * 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 { + LOAD_PREBUILT_RULES_BTN, + LOADING_INITIAL_PREBUILT_RULES_TABLE, + LOADING_SPINNER, + PAGINATION_POPOVER_BTN, + RULES_TABLE, + THREE_HUNDRED_ROWS, +} from '../screens/signal_detection_rules'; + +export const changeToThreeHundredRowsPerPage = () => { + cy.get(PAGINATION_POPOVER_BTN).click({ force: true }); + cy.get(THREE_HUNDRED_ROWS).click(); +}; + +export const loadPrebuiltDetectionRules = () => { + cy.get(LOAD_PREBUILT_RULES_BTN) + .should('exist') + .click({ force: true }); +}; + +export const waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded = () => { + cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('exist'); + cy.get(LOADING_INITIAL_PREBUILT_RULES_TABLE).should('not.exist'); +}; + +export const waitForPrebuiltDetectionRulesToBeLoaded = () => { + cy.get(LOAD_PREBUILT_RULES_BTN).should('not.exist'); + cy.get(RULES_TABLE).should('exist'); +}; + +export const waitForRulesToBeLoaded = () => { + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 8fdc939e7ee51..5e65e5aa34c18 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const DETECTIONS = 'app/siem#/detections'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/siem#/hosts/allHosts', diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap index 0885f15b1efba..ad2d57b948ba0 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap @@ -16,6 +16,7 @@ exports[`rendering renders correctly 1`] = ` grow={false} > diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index be2ce3dde951c..e78f148418588 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -62,7 +62,7 @@ export const Loader = React.memo(({ children, overlay, overlayBackg