diff --git a/.eslintignore b/.eslintignore index 972c818d791bf..93c69b4f9b207 100644 --- a/.eslintignore +++ b/.eslintignore @@ -39,7 +39,7 @@ target /x-pack/legacy/plugins/maps/public/vendor/** # package overrides -/packages/eslint-config-kibana +/packages/elastic-eslint-config-kibana /packages/kbn-interpreter/src/common/lib/grammar.js /packages/kbn-plugin-generator/template /packages/kbn-pm/dist diff --git a/.eslintrc.js b/.eslintrc.js index ff4ac180c3774..9b75c36c95abd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -94,12 +94,6 @@ module.exports = { 'jsx-a11y/no-onchange': 'off', }, }, - { - files: ['src/plugins/es_ui_shared/**/*.{js,mjs,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/plugins/kibana_react/**/*.{js,mjs,ts,tsx}'], rules: { @@ -125,25 +119,12 @@ module.exports = { 'jsx-a11y/click-events-have-key-events': 'off', }, }, - { - files: ['x-pack/legacy/plugins/index_management/**/*.{js,mjs,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', - }, - }, { files: ['x-pack/plugins/ml/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,mjs,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, /** * Files that require Apache 2.0 headers, settings diff --git a/docs/developer/contributing/development-github.asciidoc b/docs/developer/contributing/development-github.asciidoc index a6d4e29940487..84f51843098a7 100644 --- a/docs/developer/contributing/development-github.asciidoc +++ b/docs/developer/contributing/development-github.asciidoc @@ -1,5 +1,5 @@ [[development-github]] -== How we use git and github +== How we use Git and GitHub [discrete] === Forking @@ -12,17 +12,21 @@ repo, which we'll refer to in later code snippets. [discrete] === Branching -* All work on the next major release goes into master. -* Past major release branches are named `{majorVersion}.x`. They contain -work that will go into the next minor release. For example, if the next -minor release is `5.2.0`, work for it should go into the `5.x` branch. -* Past minor release branches are named `{majorVersion}.{minorVersion}`. -They contain work that will go into the next patch release. For example, -if the next patch release is `5.3.1`, work for it should go into the -`5.3` branch. -* All work is done on feature branches and merged into one of these -branches. -* Where appropriate, we'll backport changes into older release branches. +At Elastic, all products in the stack, including Kibana, are released at the same time with the same version number. Most of these projects have the following branching strategy: + +* `master` is the next major version. +* `.x` is the next minor version. +* `.` is the next release of a minor version, including patch releases. + +As an example, let's assume that the `7.x` branch is currently a not-yet-released `7.6.0`. Once `7.6.0` has reached feature freeze, it will be branched to `7.6` and `7.x` will be updated to reflect `7.7.0`. The release of `7.6.0` and subsequent patch releases will be cut from the `7.6` branch. At any time, you can verify the current version of a branch by inspecting the `version` attribute in the `package.json` file within the Kibana source. + +Pull requests are made into the `master` branch and then backported when it is safe and appropriate. + +* Breaking changes do not get backported and only go into `master`. +* All non-breaking changes can be backported to the `.x` branch. +* Features should not be backported to a `.` branch. +* Bugs can be backported to a `.` branch if the changes are safe and appropriate. Safety is a judgment call you make based on factors like the bug's severity, test coverage, confidence in the changes, etc. Your reasoning should be included in the pull request description. +* Documentation changes can be backported to any branch at any time. [discrete] === Commits and Merging @@ -109,4 +113,4 @@ Assuming you've successfully rebased and you're happy with the code, you should [discrete] === Creating a pull request -See <> for the next steps on getting your code changes merged into {kib}. \ No newline at end of file +See <> for the next steps on getting your code changes merged into {kib}. diff --git a/docs/developer/getting-started/running-kibana-advanced.asciidoc b/docs/developer/getting-started/running-kibana-advanced.asciidoc index 44897184f88f2..277e52a3dc8e9 100644 --- a/docs/developer/getting-started/running-kibana-advanced.asciidoc +++ b/docs/developer/getting-started/running-kibana-advanced.asciidoc @@ -48,7 +48,7 @@ If you’re installing dependencies and seeing an error that looks something like .... -Unsupported URL Type: link:packages/eslint-config-kibana +Unsupported URL Type: link:packages/elastic-eslint-config-kibana .... you’re likely running `npm`. To install dependencies in {kib} you diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md new file mode 100644 index 0000000000000..7475f0e3a4c1c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) + +## StatusServiceSetup.dependencies$ property + +Current status for all plugins this plugin depends on. Each key of the `Record` is a plugin id. + +Signature: + +```typescript +dependencies$: Observable>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md new file mode 100644 index 0000000000000..6c65e44270a06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) + +## StatusServiceSetup.derivedStatus$ property + +The status of this plugin as derived from its dependencies. + +Signature: + +```typescript +derivedStatus$: Observable; +``` + +## Remarks + +By default, plugins inherit this derived status from their dependencies. Calling overrides this default status. + +This may emit multliple times for a single status change event as propagates through the dependency tree + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index 3d3b73ccda25f..ba0645be4d26c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -12,10 +12,73 @@ API for accessing status of Core and this plugin's dependencies as well as for c export interface StatusServiceSetup ``` +## Remarks + +By default, a plugin inherits it's current status from the most severe status level of any Core services and any plugins that it depends on. This default status is available on the API. + +Plugins may customize their status calculation by calling the API with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its status dependent on other external services. + +## Example 1 + +Customize a plugin's status to only depend on the status of SavedObjects: + +```ts +core.status.set( + core.status.core$.pipe( +. map((coreStatus) => { + return coreStatus.savedObjects; + }) ; + ); +); + +``` + +## Example 2 + +Customize a plugin's status to include an external service: + +```ts +const externalStatus$ = interval(1000).pipe( + switchMap(async () => { + const resp = await fetch(`https://myexternaldep.com/_healthz`); + const body = await resp.json(); + if (body.ok) { + return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); + } else { + return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); + } + }), + catchError((error) => { + of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) + }) +); + +core.status.set( + combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( + map(([derivedStatus, externalStatus]) => { + if (externalStatus.level > derivedStatus) { + return externalStatus; + } else { + return derivedStatus; + } + }) + ) +); + +``` + ## Properties | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | +| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | +| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | | [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | +## Methods + +| Method | Description | +| --- | --- | +| [set(status$)](./kibana-plugin-core-server.statusservicesetup.set.md) | Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md new file mode 100644 index 0000000000000..143cd397c40ae --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [set](./kibana-plugin-core-server.statusservicesetup.set.md) + +## StatusServiceSetup.set() method + +Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. + +Signature: + +```typescript +set(status$: Observable): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| status$ | Observable<ServiceStatus> | | + +Returns: + +`void` + +## Remarks + +See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index e139b326b7500..9f3ed8c1263ba 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/packages/eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js similarity index 100% rename from packages/eslint-config-kibana/.eslintrc.js rename to packages/elastic-eslint-config-kibana/.eslintrc.js diff --git a/packages/eslint-config-kibana/.gitignore b/packages/elastic-eslint-config-kibana/.gitignore similarity index 100% rename from packages/eslint-config-kibana/.gitignore rename to packages/elastic-eslint-config-kibana/.gitignore diff --git a/packages/eslint-config-kibana/.npmignore b/packages/elastic-eslint-config-kibana/.npmignore similarity index 100% rename from packages/eslint-config-kibana/.npmignore rename to packages/elastic-eslint-config-kibana/.npmignore diff --git a/packages/eslint-config-kibana/README.md b/packages/elastic-eslint-config-kibana/README.md similarity index 94% rename from packages/eslint-config-kibana/README.md rename to packages/elastic-eslint-config-kibana/README.md index 68c1639b834a5..2049440cd8ff7 100644 --- a/packages/eslint-config-kibana/README.md +++ b/packages/elastic-eslint-config-kibana/README.md @@ -1,4 +1,4 @@ -# eslint-config-kibana +# elastic-eslint-config-kibana The eslint config used by the kibana team diff --git a/packages/eslint-config-kibana/javascript.js b/packages/elastic-eslint-config-kibana/javascript.js similarity index 100% rename from packages/eslint-config-kibana/javascript.js rename to packages/elastic-eslint-config-kibana/javascript.js diff --git a/packages/eslint-config-kibana/jest.js b/packages/elastic-eslint-config-kibana/jest.js similarity index 100% rename from packages/eslint-config-kibana/jest.js rename to packages/elastic-eslint-config-kibana/jest.js diff --git a/packages/eslint-config-kibana/package.json b/packages/elastic-eslint-config-kibana/package.json similarity index 89% rename from packages/eslint-config-kibana/package.json rename to packages/elastic-eslint-config-kibana/package.json index 4ec3bcdfd7c05..a4bb8d5449ee8 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/elastic-eslint-config-kibana/package.json @@ -5,15 +5,15 @@ "main": ".eslintrc.js", "repository": { "type": "git", - "url": "git+https://github.com/elastic/eslint-config-kibana.git" + "url": "git+https://github.com/elastic/kibana.git" }, "keywords": [], "author": "Spencer Alger ", "license": "Apache-2.0", "bugs": { - "url": "https://github.com/elastic/kibana/tree/master/packages/eslint-config-kibana" + "url": "https://github.com/elastic/kibana/tree/master/packages/elastic-eslint-config-kibana" }, - "homepage": "https://github.com/elastic/kibana/tree/master/packages/eslint-config-kibana", + "homepage": "https://github.com/elastic/kibana/tree/master/packages/elastic-eslint-config-kibana", "peerDependencies": { "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", diff --git a/packages/eslint-config-kibana/react.js b/packages/elastic-eslint-config-kibana/react.js similarity index 100% rename from packages/eslint-config-kibana/react.js rename to packages/elastic-eslint-config-kibana/react.js diff --git a/packages/eslint-config-kibana/restricted_globals.js b/packages/elastic-eslint-config-kibana/restricted_globals.js similarity index 100% rename from packages/eslint-config-kibana/restricted_globals.js rename to packages/elastic-eslint-config-kibana/restricted_globals.js diff --git a/packages/eslint-config-kibana/typescript.js b/packages/elastic-eslint-config-kibana/typescript.js similarity index 100% rename from packages/eslint-config-kibana/typescript.js rename to packages/elastic-eslint-config-kibana/typescript.js diff --git a/packages/kbn-pm/README.md b/packages/kbn-pm/README.md index 05405e7b98374..c169b5c75e178 100644 --- a/packages/kbn-pm/README.md +++ b/packages/kbn-pm/README.md @@ -19,7 +19,7 @@ From a plugin perspective there are two different types of Kibana dependencies: runtime and static dependencies. Runtime dependencies are things that are instantiated at runtime and that are injected into the plugin, for example config and elasticsearch clients. Static dependencies are those dependencies -that we want to `import`. `eslint-config-kibana` is one example of this, and +that we want to `import`. `elastic-eslint-config-kibana` is one example of this, and it's actually needed because eslint requires it to be a separate package. But we also have dependencies like `datemath`, `flot`, `eui` and others that we control, but where we want to `import` them in plugins instead of injecting them diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md index ded594930a367..76195c4f1ab89 100644 --- a/rfcs/text/0010_service_status.md +++ b/rfcs/text/0010_service_status.md @@ -137,7 +137,7 @@ interface StatusSetup { * Current status for all dependencies of the current plugin. * Each key of the `Record` is a plugin id. */ - plugins$: Observable>; + dependencies$: Observable>; /** * The status of this plugin as derived from its dependencies. diff --git a/src/core/public/chrome/recently_accessed/persisted_log.test.ts b/src/core/public/chrome/recently_accessed/persisted_log.test.ts index 4229efdf7ca9d..345ad8f3a1f5a 100644 --- a/src/core/public/chrome/recently_accessed/persisted_log.test.ts +++ b/src/core/public/chrome/recently_accessed/persisted_log.test.ts @@ -28,12 +28,6 @@ const createMockStorage = () => ({ length: 0, }); -jest.mock('ui/chrome', () => { - return { - getBasePath: () => `/some/base/path`, - }; -}); - const historyName = 'testHistory'; const historyLimit = 10; const payload = [ diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index adfdecdd7c976..7d5557be92b30 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -323,6 +323,9 @@ export class LegacyService implements CoreService { status: { core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, + set: setupDeps.core.status.plugins.set.bind(null, 'legacy'), + dependencies$: setupDeps.core.status.plugins.getDependenciesStatus$('legacy'), + derivedStatus$: setupDeps.core.status.plugins.getDerivedStatus$('legacy'), }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index fa2659ca130a0..eb31b2380d177 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -185,6 +185,9 @@ export function createPluginSetupContext( status: { core$: deps.status.core$, overall$: deps.status.overall$, + set: deps.status.plugins.set.bind(null, plugin.name), + dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), + derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 7af77491df1ab..71ac31db13f92 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -100,15 +100,27 @@ test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('no-dep')); expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` - Map { - Symbol(plugin-a) => Array [ - Symbol(no-dep), - ], - Symbol(plugin-b) => Array [ - Symbol(plugin-a), - Symbol(no-dep), - ], - Symbol(no-dep) => Array [], + Object { + "asNames": Map { + "plugin-a" => Array [ + "no-dep", + ], + "plugin-b" => Array [ + "plugin-a", + "no-dep", + ], + "no-dep" => Array [], + }, + "asOpaqueIds": Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], + }, } `); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index f5c1b35d678a3..b2acd9a6fd04b 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -20,10 +20,11 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { withTimeout } from '../../utils'; +import { PluginDependencies } from '.'; const Sec = 1000; /** @internal */ @@ -45,9 +46,19 @@ export class PluginsSystem { * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal */ - public getPluginDependencies(): ReadonlyMap { - // Return dependency map of opaque ids - return new Map( + public getPluginDependencies(): PluginDependencies { + const asNames = new Map( + [...this.plugins].map(([name, plugin]) => [ + plugin.name, + [ + ...new Set([ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)), + ]), + ].map((depId) => this.plugins.get(depId)!.name), + ]) + ); + const asOpaqueIds = new Map( [...this.plugins].map(([name, plugin]) => [ plugin.opaqueId, [ @@ -58,6 +69,8 @@ export class PluginsSystem { ].map((depId) => this.plugins.get(depId)!.opaqueId), ]) ); + + return { asNames, asOpaqueIds }; } public async setupPlugins(deps: PluginsServiceSetupDeps) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index eb2a9ca3daf5f..517261b5bc9bb 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -93,6 +93,12 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; +/** @internal */ +export interface PluginDependencies { + asNames: ReadonlyMap; + asOpaqueIds: ReadonlyMap; +} + /** * Describes the set of required and optional properties plugin can define in its * mandatory JSON manifest file. diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 49c97d837579d..fb4e4494801ed 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2853,10 +2853,17 @@ export type SharedGlobalConfig = RecursiveReadonly<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" +// // @public export interface StatusServiceSetup { core$: Observable; + dependencies$: Observable>; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup" + derivedStatus$: Observable; overall$: Observable; + set(status$: Observable): void; } // @public @@ -2949,8 +2956,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 417f66a2988c2..1bd364c2f87b7 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -41,6 +41,7 @@ import { Server } from './server'; import { getEnvOptions } from './config/__mocks__/env'; import { loggingSystemMock } from './logging/logging_system.mock'; import { rawConfigServiceMock } from './config/raw_config_service.mock'; +import { PluginName } from './plugins'; const env = new Env('.', getEnvOptions()); const logger = loggingSystemMock.create(); @@ -49,7 +50,7 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ - pluginTree: new Map(), + pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -98,7 +99,7 @@ test('injects legacy dependency to context#setup()', async () => { [pluginB, [pluginA]], ]); mockPluginsService.discover.mockResolvedValue({ - pluginTree: pluginDependencies, + pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -113,6 +114,31 @@ test('injects legacy dependency to context#setup()', async () => { }); }); +test('injects legacy dependency to status#setup()', async () => { + const server = new Server(rawConfigService, env, logger); + + const pluginDependencies = new Map([ + ['a', []], + ['b', ['a']], + ]); + mockPluginsService.discover.mockResolvedValue({ + pluginTree: { asOpaqueIds: new Map(), asNames: pluginDependencies }, + uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, + }); + + await server.setup(); + + expect(mockStatusService.setup).toHaveBeenCalledWith({ + elasticsearch: expect.any(Object), + savedObjects: expect.any(Object), + pluginDependencies: new Map([ + ['a', []], + ['b', ['a']], + ['legacy', ['a', 'b']], + ]), + }); +}); + test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index cc6d8171e7a03..e2f77f0551f34 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -121,10 +121,13 @@ export class Server { const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any NP plugin + // 1) Can access context from any KP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), + pluginDependencies: new Map([ + ...pluginTree.asOpaqueIds, + [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], + ]), }); const auditTrailSetup = this.auditTrail.setup(); @@ -154,6 +157,12 @@ export class Server { const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, + // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy can access plugin status from + // any KP plugin + pluginDependencies: new Map([ + ...pluginTree.asNames, + ['legacy', [...pluginTree.asNames.keys()]], + ]), savedObjects: savedObjectsSetup, }); diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index 7516e82ee784d..d97083162b502 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -94,6 +94,38 @@ describe('getSummaryStatus', () => { describe('summary', () => { describe('when a single service is at highest level', () => { it('returns all information about that single service', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[s2]: Lorem ipsum', + detail: 'See the status page for more information', + meta: { + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, + }, + }, + }, + }, + }); + }); + + it('allows the single service to override the detail and documentationUrl fields', () => { expect( getSummaryStatus( Object.entries({ @@ -115,7 +147,17 @@ describe('getSummaryStatus', () => { detail: 'Vivamus pulvinar sem ac luctus ultrices.', documentationUrl: 'http://helpmenow.com/problem1', meta: { - custom: { data: 'here' }, + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + }, }, }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 748a54f0bf8bb..1dc92839e8261 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -23,7 +23,10 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types' * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. * @param statuses */ -export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { +export const getSummaryStatus = ( + statuses: Array<[string, ServiceStatus]>, + { allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {} +): ServiceStatus => { const grouped = groupByLevel(statuses); const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); const highestSeverityGroup = grouped.get(highestSeverityLevel)!; @@ -31,13 +34,18 @@ export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): Serv if (highestSeverityLevel === ServiceStatusLevels.available) { return { level: ServiceStatusLevels.available, - summary: `All services are available`, + summary: allAvailableSummary, }; } else if (highestSeverityGroup.size === 1) { const [serviceName, status] = [...highestSeverityGroup.entries()][0]; return { ...status, summary: `[${serviceName}]: ${status.summary!}`, + // TODO: include URL to status page + detail: status.detail ?? `See the status page for more information`, + meta: { + affectedServices: { [serviceName]: status }, + }, }; } else { return { diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts new file mode 100644 index 0000000000000..b2d2ac8a5ef90 --- /dev/null +++ b/src/core/server/status/plugins_status.test.ts @@ -0,0 +1,338 @@ +/* + * 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 { PluginName } from '../plugins'; +import { PluginsStatusService } from './plugins_status'; +import { of, Observable, BehaviorSubject } from 'rxjs'; +import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types'; +import { first } from 'rxjs/operators'; +import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +describe('PluginStatusService', () => { + const coreAllAvailable$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, + savedObjects: { level: ServiceStatusLevels.available, summary: 'savedObjects avail' }, + }); + const coreOneDegraded$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, + savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, + }); + const coreOneCriticalOneDegraded$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.critical, summary: 'elasticsearch critical' }, + savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, + }); + const pluginDependencies: Map = new Map([ + ['a', []], + ['b', ['a']], + ['c', ['a', 'b']], + ]); + + describe('getDerivedStatus$', () => { + it(`defaults to core's most severe status`, async () => { + const serviceAvailable = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.available, + summary: 'All dependencies are available', + }); + + const serviceDegraded = new PluginsStatusService({ + core$: coreOneDegraded$, + pluginDependencies, + }); + expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + + const serviceCritical = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`provides a summary status when core and dependencies are at same severity level`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows dependencies status to take precedence over lower severity core statuses`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[a]: a is not working', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows core status to take precedence over lower severity dependencies statuses`, async () => { + const service = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows a severe dependency status to take precedence over a less severe dependency status`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); + service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); + expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[b]: b is not working', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + }); + + describe('getAll$', () => { + it('defaults to empty record if no plugins', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map(), + }); + expect(await service.getAll$().pipe(first()).toPromise()).toEqual({}); + }); + + it('defaults to core status when no plugin statuses are set', async () => { + const serviceAvailable = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + + const serviceDegraded = new PluginsStatusService({ + core$: coreOneDegraded$, + pluginDependencies, + }); + expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ + a: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + b: { + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.degraded, + summary: '[3] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + + const serviceCritical = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ + a: { + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + b: { + level: ServiceStatusLevels.critical, + summary: '[2] services are critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.critical, + summary: '[3] services are critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('uses the manually set status level if plugin specifies one', async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); + + expect(await service.getAll$().pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + b: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('updates when a new plugin status observable is set', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map([['a', []]]), + }); + const statusUpdates: Array> = []; + const subscription = service + .getAll$() + .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); + + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' })); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' })); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a available' })); + subscription.unsubscribe(); + + expect(statusUpdates).toEqual([ + { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, + { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, + { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, + { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, + ]); + }); + }); + + describe('getDependenciesStatus$', () => { + it('only includes dependencies of specified plugin', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({}); + expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + }); + + it('uses the manually set status level if plugin specifies one', async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); + + expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + b: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('throws error if unknown plugin passed', () => { + const service = new PluginsStatusService({ core$: coreAllAvailable$, pluginDependencies }); + expect(() => { + service.getDependenciesStatus$('dont-exist'); + }).toThrowError(); + }); + + it('debounces events in quick succession', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'a available', + }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'a degraded', + }; + const pluginA$ = new BehaviorSubject(available); + service.set('a', pluginA$); + + const statusUpdates: Array> = []; + const subscription = service + .getDependenciesStatus$('b') + .subscribe((status) => statusUpdates.push(status)); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + // Waiting for the debounce timeout should cut a new update + await delay(100); + pluginA$.next(available); + await delay(100); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "a": Object { + "level": degraded, + "summary": "a degraded", + }, + }, + Object { + "a": Object { + "level": available, + "summary": "a available", + }, + }, + ] + `); + }); + }); +}); diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts new file mode 100644 index 0000000000000..df6f13eeec4e5 --- /dev/null +++ b/src/core/server/status/plugins_status.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; +import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators'; +import { isDeepStrictEqual } from 'util'; + +import { PluginName } from '../plugins'; +import { ServiceStatus, CoreStatus } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +interface Deps { + core$: Observable; + pluginDependencies: ReadonlyMap; +} + +export class PluginsStatusService { + private readonly pluginStatuses = new Map>(); + private readonly update$ = new BehaviorSubject(true); + constructor(private readonly deps: Deps) {} + + public set(plugin: PluginName, status$: Observable) { + this.pluginStatuses.set(plugin, status$); + this.update$.next(true); // trigger all existing Observables to update from the new source Observable + } + + public getAll$(): Observable> { + return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); + } + + public getDependenciesStatus$(plugin: PluginName): Observable> { + const dependencies = this.deps.pluginDependencies.get(plugin); + if (!dependencies) { + throw new Error(`Unknown plugin: ${plugin}`); + } + + return this.getPluginStatuses$(dependencies).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(100) + ); + } + + public getDerivedStatus$(plugin: PluginName): Observable { + return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( + map(([coreStatus, pluginStatuses]) => { + return getSummaryStatus( + [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], + { + allAvailableSummary: `All dependencies are available`, + } + ); + }) + ); + } + + private getPluginStatuses$(plugins: PluginName[]): Observable> { + if (plugins.length === 0) { + return of({}); + } + + return this.update$.pipe( + switchMap(() => { + const pluginStatuses = plugins + .map( + (depName) => + [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [ + PluginName, + Observable + ] + ) + .map(([pName, status$]) => + status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus])) + ); + + return combineLatest(pluginStatuses).pipe( + map((statuses) => Object.fromEntries(statuses)), + distinctUntilChanged(isDeepStrictEqual) + ); + }) + ); + } +} diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 47ef8659b4079..42b3eecdca310 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -40,6 +40,9 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), + set: jest.fn(), + dependencies$: new BehaviorSubject({}), + derivedStatus$: new BehaviorSubject(available), }; return setupContract; @@ -50,6 +53,11 @@ const createInternalSetupContractMock = () => { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), isStatusPageAnonymous: jest.fn().mockReturnValue(false), + plugins: { + set: jest.fn(), + getDependenciesStatus$: jest.fn(), + getDerivedStatus$: jest.fn(), + }, }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 863fe34e8ecea..341c40a86bf77 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -34,6 +34,7 @@ describe('StatusService', () => { service = new StatusService(mockCoreContext.create()); }); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available', @@ -53,6 +54,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, @@ -68,6 +70,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); @@ -96,6 +99,7 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, + pluginDependencies: new Map(), }); const statusUpdates: CoreStatus[] = []; @@ -158,6 +162,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, @@ -173,6 +178,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); @@ -201,26 +207,95 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, + pluginDependencies: new Map(), }); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); + // Wait for timers to ensure that duplicate events are still filtered out regardless of debouncing. elasticsearch$.next(available); + await delay(100); elasticsearch$.next(available); + await delay(100); elasticsearch$.next({ level: ServiceStatusLevels.available, summary: `Wow another summary`, }); + await delay(100); savedObjects$.next(degraded); + await delay(100); savedObjects$.next(available); + await delay(100); savedObjects$.next(available); + await delay(100); subscription.unsubscribe(); expect(statusUpdates).toMatchInlineSnapshot(` Array [ Object { + "detail": "See the status page for more information", "level": degraded, + "meta": Object { + "affectedServices": Object { + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + }, + "summary": "[savedObjects]: This is degraded!", + }, + Object { + "level": available, + "summary": "All services are available", + }, + ] + `); + }); + + it('debounces events in quick succession', async () => { + const savedObjects$ = new BehaviorSubject(available); + const setup = await service.setup({ + elasticsearch: { + status$: new BehaviorSubject(available), + }, + savedObjects: { + status$: savedObjects$, + }, + pluginDependencies: new Map(), + }); + + const statusUpdates: ServiceStatus[] = []; + const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); + + // All of these should debounced into a single `available` status + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + // Waiting for the debounce timeout should cut a new update + await delay(100); + savedObjects$.next(available); + await delay(100); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "detail": "See the status page for more information", + "level": degraded, + "meta": Object { + "affectedServices": Object { + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + }, "summary": "[savedObjects]: This is degraded!", }, Object { diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index aea335e64babf..59e81343597c9 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -18,7 +18,7 @@ */ import { Observable, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { CoreService } from '../../types'; @@ -26,13 +26,16 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; +import { PluginName } from '../plugins'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; +import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; + pluginDependencies: ReadonlyMap; savedObjects: Pick; } @@ -40,17 +43,29 @@ export class StatusService implements CoreService { private readonly logger: Logger; private readonly config$: Observable; + private pluginsStatus?: PluginsStatusService; + constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup(core: SetupDeps) { + public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); - const core$ = this.setupCoreStatus(core); - const overall$: Observable = core$.pipe( - map((coreStatus) => { - const summary = getSummaryStatus(Object.entries(coreStatus)); + const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); + this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); + + const overall$: Observable = combineLatest( + core$, + this.pluginsStatus.getAll$() + ).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(100), + map(([coreStatus, pluginsStatus]) => { + const summary = getSummaryStatus([ + ...Object.entries(coreStatus), + ...Object.entries(pluginsStatus), + ]); this.logger.debug(`Recalculated overall status`, { status: summary }); return summary; }), @@ -60,6 +75,11 @@ export class StatusService implements CoreService { return { core$, overall$, + plugins: { + set: this.pluginsStatus.set.bind(this.pluginsStatus), + getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus), + getDerivedStatus$: this.pluginsStatus.getDerivedStatus$.bind(this.pluginsStatus), + }, isStatusPageAnonymous: () => statusConfig.allowAnonymous, }; } @@ -68,7 +88,10 @@ export class StatusService implements CoreService { public stop() {} - private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { + private setupCoreStatus({ + elasticsearch, + savedObjects, + }: Pick): Observable { return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( map(([elasticsearchStatus, savedObjectsStatus]) => ({ elasticsearch: elasticsearchStatus, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index 2ecf11deb2960..f884b80316fa8 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs'; import { deepFreeze } from '../../utils'; +import { PluginName } from '../plugins'; /** * The current status of a service at a point in time. @@ -116,6 +117,60 @@ export interface CoreStatus { /** * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + * + * @remarks + * By default, a plugin inherits it's current status from the most severe status level of any Core services and any + * plugins that it depends on. This default status is available on the + * {@link ServiceStatusSetup.derivedStatus$ | core.status.derviedStatus$} API. + * + * Plugins may customize their status calculation by calling the {@link ServiceStatusSetup.set | core.status.set} API + * with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its + * dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its + * status dependent on other external services. + * + * @example + * Customize a plugin's status to only depend on the status of SavedObjects: + * ```ts + * core.status.set( + * core.status.core$.pipe( + * . map((coreStatus) => { + * return coreStatus.savedObjects; + * }) ; + * ); + * ); + * ``` + * + * @example + * Customize a plugin's status to include an external service: + * ```ts + * const externalStatus$ = interval(1000).pipe( + * switchMap(async () => { + * const resp = await fetch(`https://myexternaldep.com/_healthz`); + * const body = await resp.json(); + * if (body.ok) { + * return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); + * } else { + * return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); + * } + * }), + * catchError((error) => { + * of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) + * }) + * ); + * + * core.status.set( + * combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( + * map(([derivedStatus, externalStatus]) => { + * if (externalStatus.level > derivedStatus) { + * return externalStatus; + * } else { + * return derivedStatus; + * } + * }) + * ) + * ); + * ``` + * * @public */ export interface StatusServiceSetup { @@ -134,9 +189,43 @@ export interface StatusServiceSetup { * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; + + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default inherited status. + * + * @remarks + * See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status + * calculation that is provided by Core. + */ + set(status$: Observable): void; + + /** + * Current status for all plugins this plugin depends on. + * Each key of the `Record` is a plugin id. + */ + dependencies$: Observable>; + + /** + * The status of this plugin as derived from its dependencies. + * + * @remarks + * By default, plugins inherit this derived status from their dependencies. + * Calling {@link StatusSetup.set} overrides this default status. + * + * This may emit multliple times for a single status change event as propagates + * through the dependency tree + */ + derivedStatus$: Observable; } /** @internal */ -export interface InternalStatusServiceSetup extends StatusServiceSetup { +export interface InternalStatusServiceSetup extends Pick { isStatusPageAnonymous: () => boolean; + // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. + plugins: { + set(plugin: PluginName, status$: Observable): void; + getDependenciesStatus$(plugin: PluginName): Observable>; + getDerivedStatus$(plugin: PluginName): Observable; + }; } diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 74e1ec5e2b4ed..486c8563c5456 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -59,7 +59,6 @@ export default { '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', '^plugins/([^/.]*)(.*)': '/src/legacy/core_plugins/$1/public$2', - '^ui/(.*)': '/src/legacy/ui/public/$1', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/dev/jest/setup/mocks.js b/src/dev/jest/setup/mocks.js index 6e7160e858cd7..cea28d8abdbd1 100644 --- a/src/dev/jest/setup/mocks.js +++ b/src/dev/jest/setup/mocks.js @@ -34,10 +34,6 @@ * The mocks that are enabled that way live inside the `__mocks__` folders beside their implementation files. */ -jest.mock('ui/metadata'); -jest.mock('ui/documentation_links/documentation_links'); -jest.mock('ui/chrome'); - jest.mock('moment-timezone', () => { // We always want to mock the timezone moment-timezone guesses, since otherwise // test results might be depending on which time zone you are running them. diff --git a/src/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index 5bb926799fcf6..5735b01eb3db4 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -21,7 +21,12 @@ import StubIndexPattern from 'test_utils/stub_index_pattern'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../plugins/data/common'; -import { npSetup } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; +import { uiSettingsServiceMock } from '../core/public/ui_settings/ui_settings_service.mock'; + +const uiSettingSetupMock = uiSettingsServiceMock.createSetupContract(); +uiSettingSetupMock.get.mockImplementation((item, defaultValue) => { + return defaultValue; +}); export default function stubbedLogstashIndexPatternService() { const mockLogstashFields = stubbedLogstashFields(); @@ -41,13 +46,9 @@ export default function stubbedLogstashIndexPatternService() { }; }); - const indexPattern = new StubIndexPattern( - 'logstash-*', - (cfg) => cfg, - 'time', - fields, - npSetup.core - ); + const indexPattern = new StubIndexPattern('logstash-*', (cfg) => cfg, 'time', fields, { + uiSettings: uiSettingSetupMock, + }); indexPattern.id = 'logstash-*'; indexPattern.isTimeNanosBased = () => false; diff --git a/src/legacy/server/capabilities/capabilities_mixin.test.ts b/src/legacy/server/capabilities/capabilities_mixin.test.ts deleted file mode 100644 index 3422d6a8cbb34..0000000000000 --- a/src/legacy/server/capabilities/capabilities_mixin.test.ts +++ /dev/null @@ -1,65 +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 { Server } from 'hapi'; -import KbnServer from '../kbn_server'; - -import { capabilitiesMixin } from './capabilities_mixin'; - -describe('capabilitiesMixin', () => { - let registerMock: jest.Mock; - - const getKbnServer = (pluginSpecs: any[] = []) => { - return ({ - afterPluginsInit: (callback: () => void) => callback(), - pluginSpecs, - newPlatform: { - setup: { - core: { - capabilities: { - registerProvider: registerMock, - }, - }, - }, - }, - } as unknown) as KbnServer; - }; - - let server: Server; - beforeEach(() => { - server = new Server(); - server.getUiNavLinks = () => []; - registerMock = jest.fn(); - }); - - it('calls capabilities#registerCapabilitiesProvider for each legacy plugin specs', async () => { - const getPluginSpec = (provider: () => any) => ({ - getUiCapabilitiesProvider: () => provider, - }); - - const capaA = { catalogue: { A: true } }; - const capaB = { catalogue: { B: true } }; - const kbnServer = getKbnServer([getPluginSpec(() => capaA), getPluginSpec(() => capaB)]); - await capabilitiesMixin(kbnServer, server); - - expect(registerMock).toHaveBeenCalledTimes(2); - expect(registerMock.mock.calls[0][0]()).toEqual(capaA); - expect(registerMock.mock.calls[1][0]()).toEqual(capaB); - }); -}); diff --git a/src/legacy/server/capabilities/capabilities_mixin.ts b/src/legacy/server/capabilities/capabilities_mixin.ts deleted file mode 100644 index 1f8c869f17f66..0000000000000 --- a/src/legacy/server/capabilities/capabilities_mixin.ts +++ /dev/null @@ -1,42 +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 { Server } from 'hapi'; -import KbnServer from '../kbn_server'; - -export async function capabilitiesMixin(kbnServer: KbnServer, server: Server) { - const registerLegacyCapabilities = async () => { - const capabilitiesList = await Promise.all( - kbnServer.pluginSpecs - .map((spec) => spec.getUiCapabilitiesProvider()) - .filter((provider) => !!provider) - .map((provider) => provider(server)) - ); - - capabilitiesList.forEach((capabilities) => { - kbnServer.newPlatform.setup.core.capabilities.registerProvider(() => capabilities); - }); - }; - - // Some plugin capabilities are derived from data provided by other plugins, - // so we need to wait until after all plugins have been init'd to fetch uiCapabilities. - kbnServer.afterPluginsInit(async () => { - await registerLegacyCapabilities(); - }); -} diff --git a/src/legacy/server/capabilities/index.ts b/src/legacy/server/capabilities/index.ts deleted file mode 100644 index 8c5dea1226f2b..0000000000000 --- a/src/legacy/server/capabilities/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { capabilitiesMixin } from './capabilities_mixin'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 1084521235ea0..320086b6d6531 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -34,7 +34,6 @@ import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; -import { capabilitiesMixin } from './capabilities'; import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -115,9 +114,6 @@ export default class KbnServer { // setup saved object routes savedObjectsMixin, - // setup capabilities routes - capabilitiesMixin, - // setup routes that serve the @kbn/optimizer output optimizeMixin, diff --git a/src/legacy/ui/public/.eslintrc b/src/legacy/ui/public/.eslintrc deleted file mode 100644 index cc44af915ba25..0000000000000 --- a/src/legacy/ui/public/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -rules: - no-console: 2 - 'import/no-default-export': error diff --git a/src/legacy/ui/public/__tests__/events.js b/src/legacy/ui/public/__tests__/events.js deleted file mode 100644 index c225c2a8ac1c0..0000000000000 --- a/src/legacy/ui/public/__tests__/events.js +++ /dev/null @@ -1,244 +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 _ from 'lodash'; -import sinon from 'sinon'; -import ngMock from 'ng_mock'; -import { EventsProvider } from '../events'; -import expect from '@kbn/expect'; -import '../private'; -import { createDefer } from 'ui/promises'; -import { createLegacyClass } from '../utils/legacy_class'; - -describe('Events', function () { - require('test_utils/no_digest_promises').activateForSuite(); - - let Events; - let Promise; - let eventsInstance; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector, Private) { - Promise = $injector.get('Promise'); - Events = Private(EventsProvider); - eventsInstance = new Events(); - }) - ); - - it('should handle on events', function () { - const obj = new Events(); - const prom = obj.on('test', function (message) { - expect(message).to.equal('Hello World'); - }); - - obj.emit('test', 'Hello World'); - - return prom; - }); - - it('should work with inherited objects', function () { - createLegacyClass(MyEventedObject).inherits(Events); - function MyEventedObject() { - MyEventedObject.Super.call(this); - } - const obj = new MyEventedObject(); - - const prom = obj.on('test', function (message) { - expect(message).to.equal('Hello World'); - }); - - obj.emit('test', 'Hello World'); - - return prom; - }); - - it('should clear events when off is called', function () { - const obj = new Events(); - obj.on('test', _.noop); - expect(obj._listeners).to.have.property('test'); - expect(obj._listeners.test).to.have.length(1); - obj.off(); - expect(obj._listeners).to.not.have.property('test'); - }); - - it('should clear a specific handler when off is called for an event', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - const handler2 = sinon.stub(); - obj.on('test', handler1); - obj.on('test', handler2); - expect(obj._listeners).to.have.property('test'); - obj.off('test', handler1); - - return obj.emit('test', 'Hello World').then(function () { - sinon.assert.calledOnce(handler2); - sinon.assert.notCalled(handler1); - }); - }); - - it('should clear a all handlers when off is called for an event', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - obj.on('test', handler1); - expect(obj._listeners).to.have.property('test'); - obj.off('test'); - expect(obj._listeners).to.not.have.property('test'); - - return obj.emit('test', 'Hello World').then(function () { - sinon.assert.notCalled(handler1); - }); - }); - - it('should handle multiple identical emits in the same tick', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - - obj.on('test', handler1); - const emits = [obj.emit('test', 'one'), obj.emit('test', 'two'), obj.emit('test', 'three')]; - - return Promise.all(emits).then(function () { - expect(handler1.callCount).to.be(emits.length); - expect(handler1.getCall(0).calledWith('one')).to.be(true); - expect(handler1.getCall(1).calledWith('two')).to.be(true); - expect(handler1.getCall(2).calledWith('three')).to.be(true); - }); - }); - - it('should handle emits from the handler', function () { - const obj = new Events(); - const secondEmit = createDefer(Promise); - - const handler1 = sinon.spy(function () { - if (handler1.calledTwice) { - return; - } - obj.emit('test').then(_.bindKey(secondEmit, 'resolve')); - }); - - obj.on('test', handler1); - - return Promise.all([obj.emit('test'), secondEmit.promise]).then(function () { - expect(handler1.callCount).to.be(2); - }); - }); - - it('should only emit to handlers registered before emit is called', function () { - const obj = new Events(); - const handler1 = sinon.stub(); - const handler2 = sinon.stub(); - - obj.on('test', handler1); - const emits = [obj.emit('test', 'one'), obj.emit('test', 'two'), obj.emit('test', 'three')]; - - return Promise.all(emits).then(function () { - expect(handler1.callCount).to.be(emits.length); - - obj.on('test', handler2); - - const emits2 = [obj.emit('test', 'four'), obj.emit('test', 'five'), obj.emit('test', 'six')]; - - return Promise.all(emits2).then(function () { - expect(handler1.callCount).to.be(emits.length + emits2.length); - expect(handler2.callCount).to.be(emits2.length); - }); - }); - }); - - it('should pass multiple arguments from the emitter', function () { - const obj = new Events(); - const handler = sinon.stub(); - const payload = ['one', { hello: 'tests' }, null]; - - obj.on('test', handler); - - return obj.emit('test', payload[0], payload[1], payload[2]).then(function () { - expect(handler.callCount).to.be(1); - expect(handler.calledWithExactly(payload[0], payload[1], payload[2])).to.be(true); - }); - }); - - it('should preserve the scope of the handler', function () { - const obj = new Events(); - const expected = 'some value'; - let testValue; - - function handler() { - testValue = this.getVal(); - } - handler.getVal = _.constant(expected); - - obj.on('test', handler); - return obj.emit('test').then(function () { - expect(testValue).to.equal(expected); - }); - }); - - it('should always emit in the same order', function () { - const handler = sinon.stub(); - - const obj = new Events(); - obj.on('block', _.partial(handler, 'block')); - obj.on('last', _.partial(handler, 'last')); - - return Promise.all([ - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('block'), - obj.emit('last'), - ]).then(function () { - expect(handler.callCount).to.be(10); - handler.args.forEach(function (args, i) { - expect(args[0]).to.be(i < 9 ? 'block' : 'last'); - }); - }); - }); - - it('calls emitted handlers asynchronously', (done) => { - const listenerStub = sinon.stub(); - eventsInstance.on('test', listenerStub); - eventsInstance.emit('test'); - sinon.assert.notCalled(listenerStub); - - setTimeout(() => { - sinon.assert.calledOnce(listenerStub); - done(); - }, 100); - }); - - it('calling off after an emit that has not yet triggered the handler, will not call the handler', (done) => { - const listenerStub = sinon.stub(); - eventsInstance.on('test', listenerStub); - eventsInstance.emit('test'); - // It's called asynchronously so it shouldn't be called yet. - sinon.assert.notCalled(listenerStub); - eventsInstance.off('test', listenerStub); - - setTimeout(() => { - sinon.assert.notCalled(listenerStub); - done(); - }, 100); - }); -}); diff --git a/src/legacy/ui/public/__tests__/metadata.js b/src/legacy/ui/public/__tests__/metadata.js deleted file mode 100644 index c5051d70849cd..0000000000000 --- a/src/legacy/ui/public/__tests__/metadata.js +++ /dev/null @@ -1,29 +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 expect from '@kbn/expect'; -import { metadata } from '../metadata'; -describe('ui/metadata', () => { - it('is immutable', () => { - expect(() => (metadata.foo = 'something')).to.throw; - expect(() => (metadata.version = 'something')).to.throw; - expect(() => (metadata.vars = {})).to.throw; - expect(() => (metadata.vars.kbnIndex = 'something')).to.throw; - }); -}); diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss deleted file mode 100644 index a441b773d4a4e..0000000000000 --- a/src/legacy/ui/public/_index.scss +++ /dev/null @@ -1,9 +0,0 @@ -// Prefix all styles with "kbn" to avoid conflicts. -// Examples -// kbnChart -// kbnChart__legend -// kbnChart__legend--small -// kbnChart__legend-isLoading - -@import './accessibility/index'; -@import './directives/index'; diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js b/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js deleted file mode 100644 index f3b7ab29d8a14..0000000000000 --- a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js +++ /dev/null @@ -1,127 +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 angular from 'angular'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../kbn_accessible_click'; -import { keys } from '@elastic/eui'; - -describe('kbnAccessibleClick directive', () => { - let $compile; - let $rootScope; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_; - $rootScope = _$rootScope_; - }) - ); - - describe('throws an error', () => { - it('when the element is a button', () => { - const html = ``; - expect(() => { - $compile(html)($rootScope); - }).to.throwError(/kbnAccessibleClick doesn't need to be used on a button./); - }); - - it('when the element is a link with an href', () => { - const html = ``; - expect(() => { - $compile(html)($rootScope); - }).to.throwError( - /kbnAccessibleClick doesn't need to be used on a link if it has a href attribute./ - ); - }); - - it(`when the element doesn't have an ng-click`, () => { - const html = `
`; - expect(() => { - $compile(html)($rootScope); - }).to.throwError(/kbnAccessibleClick requires ng-click to be defined on its element./); - }); - }); - - describe(`doesn't throw an error`, () => { - it('when the element is a link without an href', () => { - const html = ``; - expect(() => { - $compile(html)($rootScope); - }).not.to.throwError(); - }); - }); - - describe('adds accessibility attributes', () => { - it('tabindex', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('tabindex')).to.be('0'); - }); - - it('role', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('role')).to.be('button'); - }); - }); - - describe(`doesn't override pre-existing accessibility attributes`, () => { - it('tabindex', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('tabindex')).to.be('1'); - }); - - it('role', () => { - const html = `
`; - const element = $compile(html)($rootScope); - expect(element.attr('role')).to.be('submit'); - }); - }); - - describe(`calls ng-click`, () => { - let scope; - let element; - - beforeEach(function () { - scope = $rootScope.$new(); - scope.handleClick = sinon.stub(); - const html = `
`; - element = $compile(html)(scope); - }); - - it(`on ENTER keyup`, () => { - const e = angular.element.Event('keyup'); // eslint-disable-line new-cap - e.key = keys.ENTER; - element.trigger(e); - sinon.assert.calledOnce(scope.handleClick); - }); - - it(`on SPACE keyup`, () => { - const e = angular.element.Event('keyup'); // eslint-disable-line new-cap - e.key = keys.SPACE; - element.trigger(e); - sinon.assert.calledOnce(scope.handleClick); - }); - }); -}); diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js b/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js deleted file mode 100644 index ce1bf95bf0fb7..0000000000000 --- a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js +++ /dev/null @@ -1,129 +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 angular from 'angular'; -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../kbn_ui_ace_keyboard_mode'; -import { keys } from '@elastic/eui'; - -describe('kbnUiAceKeyboardMode directive', () => { - let element; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(($compile, $rootScope) => { - element = $compile(`
`)($rootScope.$new()); - }) - ); - - it('should add the hint element', () => { - expect(element.find('.kbnUiAceKeyboardHint').length).to.be(1); - }); - - describe('hint element', () => { - it('should be tabable', () => { - expect(element.find('.kbnUiAceKeyboardHint').attr('tabindex')).to.be('0'); - }); - - it('should move focus to textbox and be inactive if pressed enter on it', () => { - const textarea = element.find('textarea'); - sinon.spy(textarea[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ENTER; - element.find('.kbnUiAceKeyboardHint').trigger(ev); - expect(textarea[0].focus.called).to.be(true); - expect( - element.find('.kbnUiAceKeyboardHint').hasClass('kbnUiAceKeyboardHint-isInactive') - ).to.be(true); - }); - - it('should be shown again, when pressing Escape in ace editor', () => { - const textarea = element.find('textarea'); - const hint = element.find('.kbnUiAceKeyboardHint'); - sinon.spy(hint[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ESCAPE; - textarea.trigger(ev); - expect(hint[0].focus.called).to.be(true); - expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false); - }); - }); - - describe('ui-ace textarea', () => { - it('should not be tabable anymore', () => { - expect(element.find('textarea').attr('tabindex')).to.be('-1'); - }); - }); -}); - -describe('kbnUiAceKeyboardModeService', () => { - let element; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(($compile, $rootScope, kbnUiAceKeyboardModeService) => { - const scope = $rootScope.$new(); - element = $compile(`
`)(scope); - kbnUiAceKeyboardModeService.initialize(scope, element); - }) - ); - - it('should add the hint element', () => { - expect(element.find('.kbnUiAceKeyboardHint').length).to.be(1); - }); - - describe('hint element', () => { - it('should be tabable', () => { - expect(element.find('.kbnUiAceKeyboardHint').attr('tabindex')).to.be('0'); - }); - - it('should move focus to textbox and be inactive if pressed enter on it', () => { - const textarea = element.find('textarea'); - sinon.spy(textarea[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ENTER; - element.find('.kbnUiAceKeyboardHint').trigger(ev); - expect(textarea[0].focus.called).to.be(true); - expect( - element.find('.kbnUiAceKeyboardHint').hasClass('kbnUiAceKeyboardHint-isInactive') - ).to.be(true); - }); - - it('should be shown again, when pressing Escape in ace editor', () => { - const textarea = element.find('textarea'); - const hint = element.find('.kbnUiAceKeyboardHint'); - sinon.spy(hint[0], 'focus'); - const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap - ev.key = keys.ESCAPE; - textarea.trigger(ev); - expect(hint[0].focus.called).to.be(true); - expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false); - }); - }); - - describe('ui-ace textarea', () => { - it('should not be tabable anymore', () => { - expect(element.find('textarea').attr('tabindex')).to.be('-1'); - }); - }); -}); diff --git a/src/legacy/ui/public/accessibility/__tests__/scrollto_activedescendant.js b/src/legacy/ui/public/accessibility/__tests__/scrollto_activedescendant.js deleted file mode 100644 index d5ccbf887e79b..0000000000000 --- a/src/legacy/ui/public/accessibility/__tests__/scrollto_activedescendant.js +++ /dev/null @@ -1,56 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../scrollto_activedescendant'; - -describe('scrolltoActivedescendant directive', () => { - let $compile; - let $rootScope; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject((_$compile_, _$rootScope_) => { - $compile = _$compile_; - $rootScope = _$rootScope_; - }) - ); - - it('should call scrollIntoView on aria-activedescendant changes', () => { - const scope = $rootScope.$new(); - scope.ad = ''; - const element = $compile(`
- - -
`)(scope); - const child1 = element.find('#child1'); - const child2 = element.find('#child2'); - sinon.spy(child1[0], 'scrollIntoView'); - sinon.spy(child2[0], 'scrollIntoView'); - scope.ad = 'child1'; - scope.$digest(); - expect(child1[0].scrollIntoView.calledOnce).to.be.eql(true); - scope.ad = 'child2'; - scope.$digest(); - expect(child2[0].scrollIntoView.calledOnce).to.be.eql(true); - }); -}); diff --git a/src/legacy/ui/public/accessibility/_index.scss b/src/legacy/ui/public/accessibility/_index.scss deleted file mode 100644 index 95062449e4b13..0000000000000 --- a/src/legacy/ui/public/accessibility/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './kbn_ui_ace_keyboard_mode'; diff --git a/src/legacy/ui/public/accessibility/_kbn_ui_ace_keyboard_mode.scss b/src/legacy/ui/public/accessibility/_kbn_ui_ace_keyboard_mode.scss deleted file mode 100644 index 9ace600db4197..0000000000000 --- a/src/legacy/ui/public/accessibility/_kbn_ui_ace_keyboard_mode.scss +++ /dev/null @@ -1,24 +0,0 @@ -.kbnUiAceKeyboardHint { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - background: transparentize($euiColorEmptyShade, 0.3); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - opacity: 0; - - &:focus { - opacity: 1; - border: 2px solid $euiColorPrimary; - z-index: 1000; - } - - &.kbnUiAceKeyboardHint-isInactive { - display: none; - } -} diff --git a/src/legacy/ui/public/accessibility/angular_aria.js b/src/legacy/ui/public/accessibility/angular_aria.js deleted file mode 100644 index 4335eddf0d8cd..0000000000000 --- a/src/legacy/ui/public/accessibility/angular_aria.js +++ /dev/null @@ -1,39 +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 'angular-aria'; -import { uiModules } from '../modules'; - -/** - * This module will take care of attaching appropriate aria tags related to some angular stuff, - * e.g. it will attach aria-invalid if the model state is set to invalid. - * - * You can find more infos in the official documentation: https://docs.angularjs.org/api/ngAria. - * - * Three settings are disabled: it won't automatically attach `tabindex`, `role=button` or - * handling keyboard events for `ngClick` directives. Kibana uses `kbnAccessibleClick` to handle - * those cases where you need an `ngClick` non button element to have keyboard access. - */ -uiModules.get('kibana', ['ngAria']).config(($ariaProvider) => { - $ariaProvider.config({ - bindKeydown: false, - bindRoleForClick: false, - tabindex: false, - }); -}); diff --git a/src/legacy/ui/public/accessibility/index.js b/src/legacy/ui/public/accessibility/index.js deleted file mode 100644 index 5ff2521421866..0000000000000 --- a/src/legacy/ui/public/accessibility/index.js +++ /dev/null @@ -1,22 +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 './angular_aria'; -import './kbn_accessible_click'; -import './scrollto_activedescendant'; diff --git a/src/legacy/ui/public/accessibility/kbn_accessible_click.js b/src/legacy/ui/public/accessibility/kbn_accessible_click.js deleted file mode 100644 index a57fbc6be82fd..0000000000000 --- a/src/legacy/ui/public/accessibility/kbn_accessible_click.js +++ /dev/null @@ -1,43 +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. - */ - -/** - * Interactive elements must be able to receive focus. - * - * Ideally, this means using elements that are natively keyboard accessible (, - * , or - - -`; diff --git a/src/legacy/ui/public/exit_full_screen/_index.scss b/src/legacy/ui/public/exit_full_screen/_index.scss deleted file mode 100644 index 33dff05e2a687..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../plugins/kibana_react/public/exit_full_screen_button/index'; diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js deleted file mode 100644 index d4273c0fdb207..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js +++ /dev/null @@ -1,88 +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. - */ - -jest.mock( - 'ui/chrome', - () => ({ - getKibanaVersion: () => '6.0.0', - setVisible: () => {}, - }), - { virtual: true } -); - -import React from 'react'; -import { mountWithIntl, renderWithIntl } from 'test_utils/enzyme_helpers'; -import sinon from 'sinon'; -import chrome from 'ui/chrome'; - -import { ExitFullScreenButton } from './exit_full_screen_button'; - -import { keys } from '@elastic/eui'; - -test('is rendered', () => { - const component = renderWithIntl( {}} />); - - expect(component).toMatchSnapshot(); -}); - -describe('onExitFullScreenMode', () => { - test('is called when the button is pressed', () => { - const onExitHandler = sinon.stub(); - - const component = mountWithIntl(); - - component.find('button').simulate('click'); - - sinon.assert.calledOnce(onExitHandler); - }); - - test('is called when the ESC key is pressed', () => { - const onExitHandler = sinon.stub(); - - mountWithIntl(); - - const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE }); - document.dispatchEvent(escapeKeyEvent); - - sinon.assert.calledOnce(onExitHandler); - }); -}); - -describe('chrome.setVisible', () => { - test('is called with false when the component is rendered', () => { - chrome.setVisible = sinon.stub(); - - const component = mountWithIntl( {}} />); - - component.find('button').simulate('click'); - - sinon.assert.calledOnce(chrome.setVisible); - sinon.assert.calledWith(chrome.setVisible, false); - }); - - test('is called with true the component is unmounted', () => { - const component = mountWithIntl( {}} />); - - chrome.setVisible = sinon.stub(); - component.unmount(); - - sinon.assert.calledOnce(chrome.setVisible); - sinon.assert.calledWith(chrome.setVisible, true); - }); -}); diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx deleted file mode 100644 index db4101010f6d6..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.tsx +++ /dev/null @@ -1,45 +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 React, { PureComponent } from 'react'; -import chrome from 'ui/chrome'; -import { ExitFullScreenButton as ExitFullScreenButtonUi } from '../../../../plugins/kibana_react/public'; - -/** - * DO NOT USE THIS COMPONENT, IT IS DEPRECATED. - * Use the one in `src/plugins/kibana_react`. - */ - -interface Props { - onExitFullScreenMode: () => void; -} - -export class ExitFullScreenButton extends PureComponent { - public UNSAFE_componentWillMount() { - chrome.setVisible(false); - } - - public componentWillUnmount() { - chrome.setVisible(true); - } - - public render() { - return ; - } -} diff --git a/src/legacy/ui/public/exit_full_screen/index.ts b/src/legacy/ui/public/exit_full_screen/index.ts deleted file mode 100644 index a965fd776e0c2..0000000000000 --- a/src/legacy/ui/public/exit_full_screen/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { ExitFullScreenButton } from './exit_full_screen_button'; diff --git a/src/legacy/ui/public/flot-charts/API.md b/src/legacy/ui/public/flot-charts/API.md deleted file mode 100644 index 699e2500f4942..0000000000000 --- a/src/legacy/ui/public/flot-charts/API.md +++ /dev/null @@ -1,1498 +0,0 @@ -# Flot Reference # - -**Table of Contents** - -[Introduction](#introduction) -| [Data Format](#data-format) -| [Plot Options](#plot-options) -| [Customizing the legend](#customizing-the-legend) -| [Customizing the axes](#customizing-the-axes) -| [Multiple axes](#multiple-axes) -| [Time series data](#time-series-data) -| [Customizing the data series](#customizing-the-data-series) -| [Customizing the grid](#customizing-the-grid) -| [Specifying gradients](#specifying-gradients) -| [Plot Methods](#plot-methods) -| [Hooks](#hooks) -| [Plugins](#plugins) -| [Version number](#version-number) - ---- - -## Introduction ## - -Consider a call to the plot function: - -```js -var plot = $.plot(placeholder, data, options) -``` - -The placeholder is a jQuery object or DOM element or jQuery expression -that the plot will be put into. This placeholder needs to have its -width and height set as explained in the [README](README.md) (go read that now if -you haven't, it's short). The plot will modify some properties of the -placeholder so it's recommended you simply pass in a div that you -don't use for anything else. Make sure you check any fancy styling -you apply to the div, e.g. background images have been reported to be a -problem on IE 7. - -The plot function can also be used as a jQuery chainable property. This form -naturally can't return the plot object directly, but you can still access it -via the 'plot' data key, like this: - -```js -var plot = $("#placeholder").plot(data, options).data("plot"); -``` - -The format of the data is documented below, as is the available -options. The plot object returned from the call has some methods you -can call. These are documented separately below. - -Note that in general Flot gives no guarantees if you change any of the -objects you pass in to the plot function or get out of it since -they're not necessarily deep-copied. - - -## Data Format ## - -The data is an array of data series: - -```js -[ series1, series2, ... ] -``` - -A series can either be raw data or an object with properties. The raw -data format is an array of points: - -```js -[ [x1, y1], [x2, y2], ... ] -``` - -E.g. - -```js -[ [1, 3], [2, 14.01], [3.5, 3.14] ] -``` - -Note that to simplify the internal logic in Flot both the x and y -values must be numbers (even if specifying time series, see below for -how to do this). This is a common problem because you might retrieve -data from the database and serialize them directly to JSON without -noticing the wrong type. If you're getting mysterious errors, double -check that you're inputting numbers and not strings. - -If a null is specified as a point or if one of the coordinates is null -or couldn't be converted to a number, the point is ignored when -drawing. As a special case, a null value for lines is interpreted as a -line segment end, i.e. the points before and after the null value are -not connected. - -Lines and points take two coordinates. For filled lines and bars, you -can specify a third coordinate which is the bottom of the filled -area/bar (defaults to 0). - -The format of a single series object is as follows: - -```js -{ - color: color or number - data: rawdata - label: string - lines: specific lines options - bars: specific bars options - points: specific points options - xaxis: number - yaxis: number - clickable: boolean - hoverable: boolean - shadowSize: number - highlightColor: color or number -} -``` - -You don't have to specify any of them except the data, the rest are -options that will get default values. Typically you'd only specify -label and data, like this: - -```js -{ - label: "y = 3", - data: [[0, 3], [10, 3]] -} -``` - -The label is used for the legend, if you don't specify one, the series -will not show up in the legend. - -If you don't specify color, the series will get a color from the -auto-generated colors. The color is either a CSS color specification -(like "rgb(255, 100, 123)") or an integer that specifies which of -auto-generated colors to select, e.g. 0 will get color no. 0, etc. - -The latter is mostly useful if you let the user add and remove series, -in which case you can hard-code the color index to prevent the colors -from jumping around between the series. - -The "xaxis" and "yaxis" options specify which axis to use. The axes -are numbered from 1 (default), so { yaxis: 2} means that the series -should be plotted against the second y axis. - -"clickable" and "hoverable" can be set to false to disable -interactivity for specific series if interactivity is turned on in -the plot, see below. - -The rest of the options are all documented below as they are the same -as the default options passed in via the options parameter in the plot -command. When you specify them for a specific data series, they will -override the default options for the plot for that data series. - -Here's a complete example of a simple data specification: - -```js -[ { label: "Foo", data: [ [10, 1], [17, -14], [30, 5] ] }, - { label: "Bar", data: [ [11, 13], [19, 11], [30, -7] ] } -] -``` - - -## Plot Options ## - -All options are completely optional. They are documented individually -below, to change them you just specify them in an object, e.g. - -```js -var options = { - series: { - lines: { show: true }, - points: { show: true } - } -}; - -$.plot(placeholder, data, options); -``` - - -## Customizing the legend ## - -```js -legend: { - show: boolean - labelFormatter: null or (fn: string, series object -> string) - labelBoxBorderColor: color - noColumns: number - position: "ne" or "nw" or "se" or "sw" - margin: number of pixels or [x margin, y margin] - backgroundColor: null or color - backgroundOpacity: number between 0 and 1 - container: null or jQuery object/DOM element/jQuery expression - sorted: null/false, true, "ascending", "descending", "reverse", or a comparator -} -``` - -The legend is generated as a table with the data series labels and -small label boxes with the color of the series. If you want to format -the labels in some way, e.g. make them to links, you can pass in a -function for "labelFormatter". Here's an example that makes them -clickable: - -```js -labelFormatter: function(label, series) { - // series is the series object for the label - return '' + label + ''; -} -``` - -To prevent a series from showing up in the legend, simply have the function -return null. - -"noColumns" is the number of columns to divide the legend table into. -"position" specifies the overall placement of the legend within the -plot (top-right, top-left, etc.) and margin the distance to the plot -edge (this can be either a number or an array of two numbers like [x, -y]). "backgroundColor" and "backgroundOpacity" specifies the -background. The default is a partly transparent auto-detected -background. - -If you want the legend to appear somewhere else in the DOM, you can -specify "container" as a jQuery object/expression to put the legend -table into. The "position" and "margin" etc. options will then be -ignored. Note that Flot will overwrite the contents of the container. - -Legend entries appear in the same order as their series by default. If "sorted" -is "reverse" then they appear in the opposite order from their series. To sort -them alphabetically, you can specify true, "ascending" or "descending", where -true and "ascending" are equivalent. - -You can also provide your own comparator function that accepts two -objects with "label" and "color" properties, and returns zero if they -are equal, a positive value if the first is greater than the second, -and a negative value if the first is less than the second. - -```js -sorted: function(a, b) { - // sort alphabetically in ascending order - return a.label == b.label ? 0 : ( - a.label > b.label ? 1 : -1 - ) -} -``` - - -## Customizing the axes ## - -```js -xaxis, yaxis: { - show: null or true/false - position: "bottom" or "top" or "left" or "right" - mode: null or "time" ("time" requires jquery.flot.time.js plugin) - timezone: null, "browser" or timezone (only makes sense for mode: "time") - - color: null or color spec - tickColor: null or color spec - font: null or font spec object - - min: null or number - max: null or number - autoscaleMargin: null or number - - transform: null or fn: number -> number - inverseTransform: null or fn: number -> number - - ticks: null or number or ticks array or (fn: axis -> ticks array) - tickSize: number or array - minTickSize: number or array - tickFormatter: (fn: number, object -> string) or string - tickDecimals: null or number - - labelWidth: null or number - labelHeight: null or number - reserveSpace: null or true - - tickLength: null or number - - alignTicksWithAxis: null or number -} -``` - -All axes have the same kind of options. The following describes how to -configure one axis, see below for what to do if you've got more than -one x axis or y axis. - -If you don't set the "show" option (i.e. it is null), visibility is -auto-detected, i.e. the axis will show up if there's data associated -with it. You can override this by setting the "show" option to true or -false. - -The "position" option specifies where the axis is placed, bottom or -top for x axes, left or right for y axes. The "mode" option determines -how the data is interpreted, the default of null means as decimal -numbers. Use "time" for time series data; see the time series data -section. The time plugin (jquery.flot.time.js) is required for time -series support. - -The "color" option determines the color of the line and ticks for the axis, and -defaults to the grid color with transparency. For more fine-grained control you -can also set the color of the ticks separately with "tickColor". - -You can customize the font and color used to draw the axis tick labels with CSS -or directly via the "font" option. When "font" is null - the default - each -tick label is given the 'flot-tick-label' class. For compatibility with Flot -0.7 and earlier the labels are also given the 'tickLabel' class, but this is -deprecated and scheduled to be removed with the release of version 1.0.0. - -To enable more granular control over styles, labels are divided between a set -of text containers, with each holding the labels for one axis. These containers -are given the classes 'flot-[x|y]-axis', and 'flot-[x|y]#-axis', where '#' is -the number of the axis when there are multiple axes. For example, the x-axis -labels for a simple plot with only a single x-axis might look like this: - -```html -
-
January 2013
- ... -
-``` - -For direct control over label styles you can also provide "font" as an object -with this format: - -```js -{ - size: 11, - lineHeight: 13, - style: "italic", - weight: "bold", - family: "sans-serif", - variant: "small-caps", - color: "#545454" -} -``` - -The size and lineHeight must be expressed in pixels; CSS units such as 'em' -or 'smaller' are not allowed. - -The options "min"/"max" are the precise minimum/maximum value on the -scale. If you don't specify either of them, a value will automatically -be chosen based on the minimum/maximum data values. Note that Flot -always examines all the data values you feed to it, even if a -restriction on another axis may make some of them invisible (this -makes interactive use more stable). - -The "autoscaleMargin" is a bit esoteric: it's the fraction of margin -that the scaling algorithm will add to avoid that the outermost points -ends up on the grid border. Note that this margin is only applied when -a min or max value is not explicitly set. If a margin is specified, -the plot will furthermore extend the axis end-point to the nearest -whole tick. The default value is "null" for the x axes and 0.02 for y -axes which seems appropriate for most cases. - -"transform" and "inverseTransform" are callbacks you can put in to -change the way the data is drawn. You can design a function to -compress or expand certain parts of the axis non-linearly, e.g. -suppress weekends or compress far away points with a logarithm or some -other means. When Flot draws the plot, each value is first put through -the transform function. Here's an example, the x axis can be turned -into a natural logarithm axis with the following code: - -```js -xaxis: { - transform: function (v) { return Math.log(v); }, - inverseTransform: function (v) { return Math.exp(v); } -} -``` - -Similarly, for reversing the y axis so the values appear in inverse -order: - -```js -yaxis: { - transform: function (v) { return -v; }, - inverseTransform: function (v) { return -v; } -} -``` - -Note that for finding extrema, Flot assumes that the transform -function does not reorder values (it should be monotone). - -The inverseTransform is simply the inverse of the transform function -(so v == inverseTransform(transform(v)) for all relevant v). It is -required for converting from canvas coordinates to data coordinates, -e.g. for a mouse interaction where a certain pixel is clicked. If you -don't use any interactive features of Flot, you may not need it. - - -The rest of the options deal with the ticks. - -If you don't specify any ticks, a tick generator algorithm will make -some for you. The algorithm has two passes. It first estimates how -many ticks would be reasonable and uses this number to compute a nice -round tick interval size. Then it generates the ticks. - -You can specify how many ticks the algorithm aims for by setting -"ticks" to a number. The algorithm always tries to generate reasonably -round tick values so even if you ask for three ticks, you might get -five if that fits better with the rounding. If you don't want any -ticks at all, set "ticks" to 0 or an empty array. - -Another option is to skip the rounding part and directly set the tick -interval size with "tickSize". If you set it to 2, you'll get ticks at -2, 4, 6, etc. Alternatively, you can specify that you just don't want -ticks at a size less than a specific tick size with "minTickSize". -Note that for time series, the format is an array like [2, "month"], -see the next section. - -If you want to completely override the tick algorithm, you can specify -an array for "ticks", either like this: - -```js -ticks: [0, 1.2, 2.4] -``` - -Or like this where the labels are also customized: - -```js -ticks: [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]] -``` - -You can mix the two if you like. - -For extra flexibility you can specify a function as the "ticks" -parameter. The function will be called with an object with the axis -min and max and should return a ticks array. Here's a simplistic tick -generator that spits out intervals of pi, suitable for use on the x -axis for trigonometric functions: - -```js -function piTickGenerator(axis) { - var res = [], i = Math.floor(axis.min / Math.PI); - do { - var v = i * Math.PI; - res.push([v, i + "\u03c0"]); - ++i; - } while (v < axis.max); - return res; -} -``` - -You can control how the ticks look like with "tickDecimals", the -number of decimals to display (default is auto-detected). - -Alternatively, for ultimate control over how ticks are formatted you can -provide a function to "tickFormatter". The function is passed two -parameters, the tick value and an axis object with information, and -should return a string. The default formatter looks like this: - -```js -function formatter(val, axis) { - return val.toFixed(axis.tickDecimals); -} -``` - -The axis object has "min" and "max" with the range of the axis, -"tickDecimals" with the number of decimals to round the value to and -"tickSize" with the size of the interval between ticks as calculated -by the automatic axis scaling algorithm (or specified by you). Here's -an example of a custom formatter: - -```js -function suffixFormatter(val, axis) { - if (val > 1000000) - return (val / 1000000).toFixed(axis.tickDecimals) + " MB"; - else if (val > 1000) - return (val / 1000).toFixed(axis.tickDecimals) + " kB"; - else - return val.toFixed(axis.tickDecimals) + " B"; -} -``` - -"labelWidth" and "labelHeight" specifies a fixed size of the tick -labels in pixels. They're useful in case you need to align several -plots. "reserveSpace" means that even if an axis isn't shown, Flot -should reserve space for it - it is useful in combination with -labelWidth and labelHeight for aligning multi-axis charts. - -"tickLength" is the length of the tick lines in pixels. By default, the -innermost axes will have ticks that extend all across the plot, while -any extra axes use small ticks. A value of null means use the default, -while a number means small ticks of that length - set it to 0 to hide -the lines completely. - -If you set "alignTicksWithAxis" to the number of another axis, e.g. -alignTicksWithAxis: 1, Flot will ensure that the autogenerated ticks -of this axis are aligned with the ticks of the other axis. This may -improve the looks, e.g. if you have one y axis to the left and one to -the right, because the grid lines will then match the ticks in both -ends. The trade-off is that the forced ticks won't necessarily be at -natural places. - - -## Multiple axes ## - -If you need more than one x axis or y axis, you need to specify for -each data series which axis they are to use, as described under the -format of the data series, e.g. { data: [...], yaxis: 2 } specifies -that a series should be plotted against the second y axis. - -To actually configure that axis, you can't use the xaxis/yaxis options -directly - instead there are two arrays in the options: - -```js -xaxes: [] -yaxes: [] -``` - -Here's an example of configuring a single x axis and two y axes (we -can leave options of the first y axis empty as the defaults are fine): - -```js -{ - xaxes: [ { position: "top" } ], - yaxes: [ { }, { position: "right", min: 20 } ] -} -``` - -The arrays get their default values from the xaxis/yaxis settings, so -say you want to have all y axes start at zero, you can simply specify -yaxis: { min: 0 } instead of adding a min parameter to all the axes. - -Generally, the various interfaces in Flot dealing with data points -either accept an xaxis/yaxis parameter to specify which axis number to -use (starting from 1), or lets you specify the coordinate directly as -x2/x3/... or x2axis/x3axis/... instead of "x" or "xaxis". - - -## Time series data ## - -Please note that it is now required to include the time plugin, -jquery.flot.time.js, for time series support. - -Time series are a bit more difficult than scalar data because -calendars don't follow a simple base 10 system. For many cases, Flot -abstracts most of this away, but it can still be a bit difficult to -get the data into Flot. So we'll first discuss the data format. - -The time series support in Flot is based on JavaScript timestamps, -i.e. everywhere a time value is expected or handed over, a JavaScript -timestamp number is used. This is a number, not a Date object. A -JavaScript timestamp is the number of milliseconds since January 1, -1970 00:00:00 UTC. This is almost the same as Unix timestamps, except it's -in milliseconds, so remember to multiply by 1000! - -You can see a timestamp like this - -```js -alert((new Date()).getTime()) -``` - -There are different schools of thought when it comes to display of -timestamps. Many will want the timestamps to be displayed according to -a certain time zone, usually the time zone in which the data has been -produced. Some want the localized experience, where the timestamps are -displayed according to the local time of the visitor. Flot supports -both. Optionally you can include a third-party library to get -additional timezone support. - -Default behavior is that Flot always displays timestamps according to -UTC. The reason being that the core JavaScript Date object does not -support other fixed time zones. Often your data is at another time -zone, so it may take a little bit of tweaking to work around this -limitation. - -The easiest way to think about it is to pretend that the data -production time zone is UTC, even if it isn't. So if you have a -datapoint at 2002-02-20 08:00, you can generate a timestamp for eight -o'clock UTC even if it really happened eight o'clock UTC+0200. - -In PHP you can get an appropriate timestamp with: - -```php -strtotime("2002-02-20 UTC") * 1000 -``` - -In Python you can get it with something like: - -```python -calendar.timegm(datetime_object.timetuple()) * 1000 -``` -In Ruby you can get it using the `#to_i` method on the -[`Time`](http://apidock.com/ruby/Time/to_i) object. If you're using the -`active_support` gem (default for Ruby on Rails applications) `#to_i` is also -available on the `DateTime` and `ActiveSupport::TimeWithZone` objects. You -simply need to multiply the result by 1000: - -```ruby -Time.now.to_i * 1000 # => 1383582043000 -# ActiveSupport examples: -DateTime.now.to_i * 1000 # => 1383582043000 -ActiveSupport::TimeZone.new('Asia/Shanghai').now.to_i * 1000 -# => 1383582043000 -``` - -In .NET you can get it with something like: - -```aspx -public static int GetJavaScriptTimestamp(System.DateTime input) -{ - System.TimeSpan span = new System.TimeSpan(System.DateTime.Parse("1/1/1970").Ticks); - System.DateTime time = input.Subtract(span); - return (long)(time.Ticks / 10000); -} -``` - -JavaScript also has some support for parsing date strings, so it is -possible to generate the timestamps manually client-side. - -If you've already got the real UTC timestamp, it's too late to use the -pretend trick described above. But you can fix up the timestamps by -adding the time zone offset, e.g. for UTC+0200 you would add 2 hours -to the UTC timestamp you got. Then it'll look right on the plot. Most -programming environments have some means of getting the timezone -offset for a specific date (note that you need to get the offset for -each individual timestamp to account for daylight savings). - -The alternative with core JavaScript is to interpret the timestamps -according to the time zone that the visitor is in, which means that -the ticks will shift with the time zone and daylight savings of each -visitor. This behavior is enabled by setting the axis option -"timezone" to the value "browser". - -If you need more time zone functionality than this, there is still -another option. If you include the "timezone-js" library - in the page and set axis.timezone -to a value recognized by said library, Flot will use timezone-js to -interpret the timestamps according to that time zone. - -Once you've gotten the timestamps into the data and specified "time" -as the axis mode, Flot will automatically generate relevant ticks and -format them. As always, you can tweak the ticks via the "ticks" option -- just remember that the values should be timestamps (numbers), not -Date objects. - -Tick generation and formatting can also be controlled separately -through the following axis options: - -```js -minTickSize: array -timeformat: null or format string -monthNames: null or array of size 12 of strings -dayNames: null or array of size 7 of strings -twelveHourClock: boolean -``` - -Here "timeformat" is a format string to use. You might use it like -this: - -```js -xaxis: { - mode: "time", - timeformat: "%Y/%m/%d" -} -``` - -This will result in tick labels like "2000/12/24". A subset of the -standard strftime specifiers are supported (plus the nonstandard %q): - -```js -%a: weekday name (customizable) -%b: month name (customizable) -%d: day of month, zero-padded (01-31) -%e: day of month, space-padded ( 1-31) -%H: hours, 24-hour time, zero-padded (00-23) -%I: hours, 12-hour time, zero-padded (01-12) -%m: month, zero-padded (01-12) -%M: minutes, zero-padded (00-59) -%q: quarter (1-4) -%S: seconds, zero-padded (00-59) -%y: year (two digits) -%Y: year (four digits) -%p: am/pm -%P: AM/PM (uppercase version of %p) -%w: weekday as number (0-6, 0 being Sunday) -``` - -Flot 0.8 switched from %h to the standard %H hours specifier. The %h specifier -is still available, for backwards-compatibility, but is deprecated and -scheduled to be removed permanently with the release of version 1.0. - -You can customize the month names with the "monthNames" option. For -instance, for Danish you might specify: - -```js -monthNames: ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"] -``` - -Similarly you can customize the weekday names with the "dayNames" -option. An example in French: - -```js -dayNames: ["dim", "lun", "mar", "mer", "jeu", "ven", "sam"] -``` - -If you set "twelveHourClock" to true, the autogenerated timestamps -will use 12 hour AM/PM timestamps instead of 24 hour. This only -applies if you have not set "timeformat". Use the "%I" and "%p" or -"%P" options if you want to build your own format string with 12-hour -times. - -If the Date object has a strftime property (and it is a function), it -will be used instead of the built-in formatter. Thus you can include -a strftime library such as http://hacks.bluesmoon.info/strftime/ for -more powerful date/time formatting. - -If everything else fails, you can control the formatting by specifying -a custom tick formatter function as usual. Here's a simple example -which will format December 24 as 24/12: - -```js -tickFormatter: function (val, axis) { - var d = new Date(val); - return d.getUTCDate() + "/" + (d.getUTCMonth() + 1); -} -``` - -Note that for the time mode "tickSize" and "minTickSize" are a bit -special in that they are arrays on the form "[value, unit]" where unit -is one of "second", "minute", "hour", "day", "month" and "year". So -you can specify - -```js -minTickSize: [1, "month"] -``` - -to get a tick interval size of at least 1 month and correspondingly, -if axis.tickSize is [2, "day"] in the tick formatter, the ticks have -been produced with two days in-between. - - -## Customizing the data series ## - -```js -series: { - lines, points, bars: { - show: boolean - lineWidth: number - fill: boolean or number - fillColor: null or color/gradient - } - - lines, bars: { - zero: boolean - } - - points: { - radius: number - symbol: "circle" or function - } - - bars: { - barWidth: number - align: "left", "right" or "center" - horizontal: boolean - } - - lines: { - steps: boolean - } - - shadowSize: number - highlightColor: color or number -} - -colors: [ color1, color2, ... ] -``` - -The options inside "series: {}" are copied to each of the series. So -you can specify that all series should have bars by putting it in the -global options, or override it for individual series by specifying -bars in a particular the series object in the array of data. - -The most important options are "lines", "points" and "bars" that -specify whether and how lines, points and bars should be shown for -each data series. In case you don't specify anything at all, Flot will -default to showing lines (you can turn this off with -lines: { show: false }). You can specify the various types -independently of each other, and Flot will happily draw each of them -in turn (this is probably only useful for lines and points), e.g. - -```js -var options = { - series: { - lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" }, - points: { show: true, fill: false } - } -}; -``` - -"lineWidth" is the thickness of the line or outline in pixels. You can -set it to 0 to prevent a line or outline from being drawn; this will -also hide the shadow. - -"fill" is whether the shape should be filled. For lines, this produces -area graphs. You can use "fillColor" to specify the color of the fill. -If "fillColor" evaluates to false (default for everything except -points which are filled with white), the fill color is auto-set to the -color of the data series. You can adjust the opacity of the fill by -setting fill to a number between 0 (fully transparent) and 1 (fully -opaque). - -For bars, fillColor can be a gradient, see the gradient documentation -below. "barWidth" is the width of the bars in units of the x axis (or -the y axis if "horizontal" is true), contrary to most other measures -that are specified in pixels. For instance, for time series the unit -is milliseconds so 24 * 60 * 60 * 1000 produces bars with the width of -a day. "align" specifies whether a bar should be left-aligned -(default), right-aligned or centered on top of the value it represents. -When "horizontal" is on, the bars are drawn horizontally, i.e. from the -y axis instead of the x axis; note that the bar end points are still -defined in the same way so you'll probably want to swap the -coordinates if you've been plotting vertical bars first. - -Area and bar charts normally start from zero, regardless of the data's range. -This is because they convey information through size, and starting from a -different value would distort their meaning. In cases where the fill is purely -for decorative purposes, however, "zero" allows you to override this behavior. -It defaults to true for filled lines and bars; setting it to false tells the -series to use the same automatic scaling as an un-filled line. - -For lines, "steps" specifies whether two adjacent data points are -connected with a straight (possibly diagonal) line or with first a -horizontal and then a vertical line. Note that this transforms the -data by adding extra points. - -For points, you can specify the radius and the symbol. The only -built-in symbol type is circles, for other types you can use a plugin -or define them yourself by specifying a callback: - -```js -function cross(ctx, x, y, radius, shadow) { - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); -} -``` - -The parameters are the drawing context, x and y coordinates of the -center of the point, a radius which corresponds to what the circle -would have used and whether the call is to draw a shadow (due to -limited canvas support, shadows are currently faked through extra -draws). It's good practice to ensure that the area covered by the -symbol is the same as for the circle with the given radius, this -ensures that all symbols have approximately the same visual weight. - -"shadowSize" is the default size of shadows in pixels. Set it to 0 to -remove shadows. - -"highlightColor" is the default color of the translucent overlay used -to highlight the series when the mouse hovers over it. - -The "colors" array specifies a default color theme to get colors for -the data series from. You can specify as many colors as you like, like -this: - -```js -colors: ["#d18b2c", "#dba255", "#919733"] -``` - -If there are more data series than colors, Flot will try to generate -extra colors by lightening and darkening colors in the theme. - - -## Customizing the grid ## - -```js -grid: { - show: boolean - aboveData: boolean - color: color - backgroundColor: color/gradient or null - margin: number or margin object - labelMargin: number - axisMargin: number - markings: array of markings or (fn: axes -> array of markings) - borderWidth: number or object with "top", "right", "bottom" and "left" properties with different widths - borderColor: color or null or object with "top", "right", "bottom" and "left" properties with different colors - minBorderMargin: number or null - clickable: boolean - hoverable: boolean - autoHighlight: boolean - mouseActiveRadius: number -} - -interaction: { - redrawOverlayInterval: number or -1 -} -``` - -The grid is the thing with the axes and a number of ticks. Many of the -things in the grid are configured under the individual axes, but not -all. "color" is the color of the grid itself whereas "backgroundColor" -specifies the background color inside the grid area, here null means -that the background is transparent. You can also set a gradient, see -the gradient documentation below. - -You can turn off the whole grid including tick labels by setting -"show" to false. "aboveData" determines whether the grid is drawn -above the data or below (below is default). - -"margin" is the space in pixels between the canvas edge and the grid, -which can be either a number or an object with individual margins for -each side, in the form: - -```js -margin: { - top: top margin in pixels - left: left margin in pixels - bottom: bottom margin in pixels - right: right margin in pixels -} -``` - -"labelMargin" is the space in pixels between tick labels and axis -line, and "axisMargin" is the space in pixels between axes when there -are two next to each other. - -"borderWidth" is the width of the border around the plot. Set it to 0 -to disable the border. Set it to an object with "top", "right", -"bottom" and "left" properties to use different widths. You can -also set "borderColor" if you want the border to have a different color -than the grid lines. Set it to an object with "top", "right", "bottom" -and "left" properties to use different colors. "minBorderMargin" controls -the default minimum margin around the border - it's used to make sure -that points aren't accidentally clipped by the canvas edge so by default -the value is computed from the point radius. - -"markings" is used to draw simple lines and rectangular areas in the -background of the plot. You can either specify an array of ranges on -the form { xaxis: { from, to }, yaxis: { from, to } } (with multiple -axes, you can specify coordinates for other axes instead, e.g. as -x2axis/x3axis/...) or with a function that returns such an array given -the axes for the plot in an object as the first parameter. - -You can set the color of markings by specifying "color" in the ranges -object. Here's an example array: - -```js -markings: [ { xaxis: { from: 0, to: 2 }, yaxis: { from: 10, to: 10 }, color: "#bb0000" }, ... ] -``` - -If you leave out one of the values, that value is assumed to go to the -border of the plot. So for example if you only specify { xaxis: { -from: 0, to: 2 } } it means an area that extends from the top to the -bottom of the plot in the x range 0-2. - -A line is drawn if from and to are the same, e.g. - -```js -markings: [ { yaxis: { from: 1, to: 1 } }, ... ] -``` - -would draw a line parallel to the x axis at y = 1. You can control the -line width with "lineWidth" in the range object. - -An example function that makes vertical stripes might look like this: - -```js -markings: function (axes) { - var markings = []; - for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2) - markings.push({ xaxis: { from: x, to: x + 1 } }); - return markings; -} -``` - -If you set "clickable" to true, the plot will listen for click events -on the plot area and fire a "plotclick" event on the placeholder with -a position and a nearby data item object as parameters. The coordinates -are available both in the unit of the axes (not in pixels) and in -global screen coordinates. - -Likewise, if you set "hoverable" to true, the plot will listen for -mouse move events on the plot area and fire a "plothover" event with -the same parameters as the "plotclick" event. If "autoHighlight" is -true (the default), nearby data items are highlighted automatically. -If needed, you can disable highlighting and control it yourself with -the highlight/unhighlight plot methods described elsewhere. - -You can use "plotclick" and "plothover" events like this: - -```js -$.plot($("#placeholder"), [ d ], { grid: { clickable: true } }); - -$("#placeholder").bind("plotclick", function (event, pos, item) { - alert("You clicked at " + pos.x + ", " + pos.y); - // axis coordinates for other axes, if present, are in pos.x2, pos.x3, ... - // if you need global screen coordinates, they are pos.pageX, pos.pageY - - if (item) { - highlight(item.series, item.datapoint); - alert("You clicked a point!"); - } -}); -``` - -The item object in this example is either null or a nearby object on the form: - -```js -item: { - datapoint: the point, e.g. [0, 2] - dataIndex: the index of the point in the data array - series: the series object - seriesIndex: the index of the series - pageX, pageY: the global screen coordinates of the point -} -``` - -For instance, if you have specified the data like this - -```js -$.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...); -``` - -and the mouse is near the point (7, 3), "datapoint" is [7, 3], -"dataIndex" will be 1, "series" is a normalized series object with -among other things the "Foo" label in series.label and the color in -series.color, and "seriesIndex" is 0. Note that plugins and options -that transform the data can shift the indexes from what you specified -in the original data array. - -If you use the above events to update some other information and want -to clear out that info in case the mouse goes away, you'll probably -also need to listen to "mouseout" events on the placeholder div. - -"mouseActiveRadius" specifies how far the mouse can be from an item -and still activate it. If there are two or more points within this -radius, Flot chooses the closest item. For bars, the top-most bar -(from the latest specified data series) is chosen. - -If you want to disable interactivity for a specific data series, you -can set "hoverable" and "clickable" to false in the options for that -series, like this: - -```js -{ data: [...], label: "Foo", clickable: false } -``` - -"redrawOverlayInterval" specifies the maximum time to delay a redraw -of interactive things (this works as a rate limiting device). The -default is capped to 60 frames per second. You can set it to -1 to -disable the rate limiting. - - -## Specifying gradients ## - -A gradient is specified like this: - -```js -{ colors: [ color1, color2, ... ] } -``` - -For instance, you might specify a background on the grid going from -black to gray like this: - -```js -grid: { - backgroundColor: { colors: ["#000", "#999"] } -} -``` - -For the series you can specify the gradient as an object that -specifies the scaling of the brightness and the opacity of the series -color, e.g. - -```js -{ colors: [{ opacity: 0.8 }, { brightness: 0.6, opacity: 0.8 } ] } -``` - -where the first color simply has its alpha scaled, whereas the second -is also darkened. For instance, for bars the following makes the bars -gradually disappear, without outline: - -```js -bars: { - show: true, - lineWidth: 0, - fill: true, - fillColor: { colors: [ { opacity: 0.8 }, { opacity: 0.1 } ] } -} -``` - -Flot currently only supports vertical gradients drawn from top to -bottom because that's what works with IE. - - -## Plot Methods ## - -The Plot object returned from the plot function has some methods you -can call: - - - highlight(series, datapoint) - - Highlight a specific datapoint in the data series. You can either - specify the actual objects, e.g. if you got them from a - "plotclick" event, or you can specify the indices, e.g. - highlight(1, 3) to highlight the fourth point in the second series - (remember, zero-based indexing). - - - unhighlight(series, datapoint) or unhighlight() - - Remove the highlighting of the point, same parameters as - highlight. - - If you call unhighlight with no parameters, e.g. as - plot.unhighlight(), all current highlights are removed. - - - setData(data) - - You can use this to reset the data used. Note that axis scaling, - ticks, legend etc. will not be recomputed (use setupGrid() to do - that). You'll probably want to call draw() afterwards. - - You can use this function to speed up redrawing a small plot if - you know that the axes won't change. Put in the new data with - setData(newdata), call draw(), and you're good to go. Note that - for large datasets, almost all the time is consumed in draw() - plotting the data so in this case don't bother. - - - setupGrid() - - Recalculate and set axis scaling, ticks, legend etc. - - Note that because of the drawing model of the canvas, this - function will immediately redraw (actually reinsert in the DOM) - the labels and the legend, but not the actual tick lines because - they're drawn on the canvas. You need to call draw() to get the - canvas redrawn. - - - draw() - - Redraws the plot canvas. - - - triggerRedrawOverlay() - - Schedules an update of an overlay canvas used for drawing - interactive things like a selection and point highlights. This - is mostly useful for writing plugins. The redraw doesn't happen - immediately, instead a timer is set to catch multiple successive - redraws (e.g. from a mousemove). You can get to the overlay by - setting up a drawOverlay hook. - - - width()/height() - - Gets the width and height of the plotting area inside the grid. - This is smaller than the canvas or placeholder dimensions as some - extra space is needed (e.g. for labels). - - - offset() - - Returns the offset of the plotting area inside the grid relative - to the document, useful for instance for calculating mouse - positions (event.pageX/Y minus this offset is the pixel position - inside the plot). - - - pointOffset({ x: xpos, y: ypos }) - - Returns the calculated offset of the data point at (x, y) in data - space within the placeholder div. If you are working with multiple - axes, you can specify the x and y axis references, e.g. - - ```js - o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 }) - // o.left and o.top now contains the offset within the div - ```` - - - resize() - - Tells Flot to resize the drawing canvas to the size of the - placeholder. You need to run setupGrid() and draw() afterwards as - canvas resizing is a destructive operation. This is used - internally by the resize plugin. - - - shutdown() - - Cleans up any event handlers Flot has currently registered. This - is used internally. - -There are also some members that let you peek inside the internal -workings of Flot which is useful in some cases. Note that if you change -something in the objects returned, you're changing the objects used by -Flot to keep track of its state, so be careful. - - - getData() - - Returns an array of the data series currently used in normalized - form with missing settings filled in according to the global - options. So for instance to find out what color Flot has assigned - to the data series, you could do this: - - ```js - var series = plot.getData(); - for (var i = 0; i < series.length; ++i) - alert(series[i].color); - ``` - - A notable other interesting field besides color is datapoints - which has a field "points" with the normalized data points in a - flat array (the field "pointsize" is the increment in the flat - array to get to the next point so for a dataset consisting only of - (x,y) pairs it would be 2). - - - getAxes() - - Gets an object with the axes. The axes are returned as the - attributes of the object, so for instance getAxes().xaxis is the - x axis. - - Various things are stuffed inside an axis object, e.g. you could - use getAxes().xaxis.ticks to find out what the ticks are for the - xaxis. Two other useful attributes are p2c and c2p, functions for - transforming from data point space to the canvas plot space and - back. Both returns values that are offset with the plot offset. - Check the Flot source code for the complete set of attributes (or - output an axis with console.log() and inspect it). - - With multiple axes, the extra axes are returned as x2axis, x3axis, - etc., e.g. getAxes().y2axis is the second y axis. You can check - y2axis.used to see whether the axis is associated with any data - points and y2axis.show to see if it is currently shown. - - - getPlaceholder() - - Returns placeholder that the plot was put into. This can be useful - for plugins for adding DOM elements or firing events. - - - getCanvas() - - Returns the canvas used for drawing in case you need to hack on it - yourself. You'll probably need to get the plot offset too. - - - getPlotOffset() - - Gets the offset that the grid has within the canvas as an object - with distances from the canvas edges as "left", "right", "top", - "bottom". I.e., if you draw a circle on the canvas with the center - placed at (left, top), its center will be at the top-most, left - corner of the grid. - - - getOptions() - - Gets the options for the plot, normalized, with default values - filled in. You get a reference to actual values used by Flot, so - if you modify the values in here, Flot will use the new values. - If you change something, you probably have to call draw() or - setupGrid() or triggerRedrawOverlay() to see the change. - - -## Hooks ## - -In addition to the public methods, the Plot object also has some hooks -that can be used to modify the plotting process. You can install a -callback function at various points in the process, the function then -gets access to the internal data structures in Flot. - -Here's an overview of the phases Flot goes through: - - 1. Plugin initialization, parsing options - - 2. Constructing the canvases used for drawing - - 3. Set data: parsing data specification, calculating colors, - copying raw data points into internal format, - normalizing them, finding max/min for axis auto-scaling - - 4. Grid setup: calculating axis spacing, ticks, inserting tick - labels, the legend - - 5. Draw: drawing the grid, drawing each of the series in turn - - 6. Setting up event handling for interactive features - - 7. Responding to events, if any - - 8. Shutdown: this mostly happens in case a plot is overwritten - -Each hook is simply a function which is put in the appropriate array. -You can add them through the "hooks" option, and they are also available -after the plot is constructed as the "hooks" attribute on the returned -plot object, e.g. - -```js - // define a simple draw hook - function hellohook(plot, canvascontext) { alert("hello!"); }; - - // pass it in, in an array since we might want to specify several - var plot = $.plot(placeholder, data, { hooks: { draw: [hellohook] } }); - - // we can now find it again in plot.hooks.draw[0] unless a plugin - // has added other hooks -``` - -The available hooks are described below. All hook callbacks get the -plot object as first parameter. You can find some examples of defined -hooks in the plugins bundled with Flot. - - - processOptions [phase 1] - - ```function(plot, options)``` - - Called after Flot has parsed and merged options. Useful in the - instance where customizations beyond simple merging of default - values is needed. A plugin might use it to detect that it has been - enabled and then turn on or off other options. - - - - processRawData [phase 3] - - ```function(plot, series, data, datapoints)``` - - Called before Flot copies and normalizes the raw data for the given - series. If the function fills in datapoints.points with normalized - points and sets datapoints.pointsize to the size of the points, - Flot will skip the copying/normalization step for this series. - - In any case, you might be interested in setting datapoints.format, - an array of objects for specifying how a point is normalized and - how it interferes with axis scaling. It accepts the following options: - - ```js - { - x, y: boolean, - number: boolean, - required: boolean, - defaultValue: value, - autoscale: boolean - } - ``` - - "x" and "y" specify whether the value is plotted against the x or y axis, - and is currently used only to calculate axis min-max ranges. The default - format array, for example, looks like this: - - ```js - [ - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ] - ``` - - This indicates that a point, i.e. [0, 25], consists of two values, with the - first being plotted on the x axis and the second on the y axis. - - If "number" is true, then the value must be numeric, and is set to null if - it cannot be converted to a number. - - "defaultValue" provides a fallback in case the original value is null. This - is for instance handy for bars, where one can omit the third coordinate - (the bottom of the bar), which then defaults to zero. - - If "required" is true, then the value must exist (be non-null) for the - point as a whole to be valid. If no value is provided, then the entire - point is cleared out with nulls, turning it into a gap in the series. - - "autoscale" determines whether the value is considered when calculating an - automatic min-max range for the axes that the value is plotted against. - - - processDatapoints [phase 3] - - ```function(plot, series, datapoints)``` - - Called after normalization of the given series but before finding - min/max of the data points. This hook is useful for implementing data - transformations. "datapoints" contains the normalized data points in - a flat array as datapoints.points with the size of a single point - given in datapoints.pointsize. Here's a simple transform that - multiplies all y coordinates by 2: - - ```js - function multiply(plot, series, datapoints) { - var points = datapoints.points, ps = datapoints.pointsize; - for (var i = 0; i < points.length; i += ps) - points[i + 1] *= 2; - } - ``` - - Note that you must leave datapoints in a good condition as Flot - doesn't check it or do any normalization on it afterwards. - - - processOffset [phase 4] - - ```function(plot, offset)``` - - Called after Flot has initialized the plot's offset, but before it - draws any axes or plot elements. This hook is useful for customizing - the margins between the grid and the edge of the canvas. "offset" is - an object with attributes "top", "bottom", "left" and "right", - corresponding to the margins on the four sides of the plot. - - - drawBackground [phase 5] - - ```function(plot, canvascontext)``` - - Called before all other drawing operations. Used to draw backgrounds - or other custom elements before the plot or axes have been drawn. - - - drawSeries [phase 5] - - ```function(plot, canvascontext, series)``` - - Hook for custom drawing of a single series. Called just before the - standard drawing routine has been called in the loop that draws - each series. - - - draw [phase 5] - - ```function(plot, canvascontext)``` - - Hook for drawing on the canvas. Called after the grid is drawn - (unless it's disabled or grid.aboveData is set) and the series have - been plotted (in case any points, lines or bars have been turned - on). For examples of how to draw things, look at the source code. - - - bindEvents [phase 6] - - ```function(plot, eventHolder)``` - - Called after Flot has setup its event handlers. Should set any - necessary event handlers on eventHolder, a jQuery object with the - canvas, e.g. - - ```js - function (plot, eventHolder) { - eventHolder.mousedown(function (e) { - alert("You pressed the mouse at " + e.pageX + " " + e.pageY); - }); - } - ``` - - Interesting events include click, mousemove, mouseup/down. You can - use all jQuery events. Usually, the event handlers will update the - state by drawing something (add a drawOverlay hook and call - triggerRedrawOverlay) or firing an externally visible event for - user code. See the crosshair plugin for an example. - - Currently, eventHolder actually contains both the static canvas - used for the plot itself and the overlay canvas used for - interactive features because some versions of IE get the stacking - order wrong. The hook only gets one event, though (either for the - overlay or for the static canvas). - - Note that custom plot events generated by Flot are not generated on - eventHolder, but on the div placeholder supplied as the first - argument to the plot call. You can get that with - plot.getPlaceholder() - that's probably also the one you should use - if you need to fire a custom event. - - - drawOverlay [phase 7] - - ```function (plot, canvascontext)``` - - The drawOverlay hook is used for interactive things that need a - canvas to draw on. The model currently used by Flot works the way - that an extra overlay canvas is positioned on top of the static - canvas. This overlay is cleared and then completely redrawn - whenever something interesting happens. This hook is called when - the overlay canvas is to be redrawn. - - "canvascontext" is the 2D context of the overlay canvas. You can - use this to draw things. You'll most likely need some of the - metrics computed by Flot, e.g. plot.width()/plot.height(). See the - crosshair plugin for an example. - - - shutdown [phase 8] - - ```function (plot, eventHolder)``` - - Run when plot.shutdown() is called, which usually only happens in - case a plot is overwritten by a new plot. If you're writing a - plugin that adds extra DOM elements or event handlers, you should - add a callback to clean up after you. Take a look at the section in - the [PLUGINS](PLUGINS.md) document for more info. - - -## Plugins ## - -Plugins extend the functionality of Flot. To use a plugin, simply -include its JavaScript file after Flot in the HTML page. - -If you're worried about download size/latency, you can concatenate all -the plugins you use, and Flot itself for that matter, into one big file -(make sure you get the order right), then optionally run it through a -JavaScript minifier such as YUI Compressor. - -Here's a brief explanation of how the plugin plumbings work: - -Each plugin registers itself in the global array $.plot.plugins. When -you make a new plot object with $.plot, Flot goes through this array -calling the "init" function of each plugin and merging default options -from the "option" attribute of the plugin. The init function gets a -reference to the plot object created and uses this to register hooks -and add new public methods if needed. - -See the [PLUGINS](PLUGINS.md) document for details on how to write a plugin. As the -above description hints, it's actually pretty easy. - - -## Version number ## - -The version number of Flot is available in ```$.plot.version```. diff --git a/src/legacy/ui/public/flot-charts/index.js b/src/legacy/ui/public/flot-charts/index.js deleted file mode 100644 index f98aca30d2dec..0000000000000 --- a/src/legacy/ui/public/flot-charts/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/* @notice - * - * This product includes code that is based on flot-charts, which was available - * under a "MIT" license. - * - * The MIT License (MIT) - * - * Copyright (c) 2007-2014 IOLA and Ole Laursen - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -import $ from 'jquery'; -if (window) window.jQuery = $; -require('ui/flot-charts/jquery.flot'); -require('ui/flot-charts/jquery.flot.time'); -require('ui/flot-charts/jquery.flot.canvas'); -require('ui/flot-charts/jquery.flot.symbol'); -require('ui/flot-charts/jquery.flot.crosshair'); -require('ui/flot-charts/jquery.flot.selection'); -require('ui/flot-charts/jquery.flot.pie'); -require('ui/flot-charts/jquery.flot.stack'); -require('ui/flot-charts/jquery.flot.threshold'); -require('ui/flot-charts/jquery.flot.fillbetween'); -require('ui/flot-charts/jquery.flot.log'); -module.exports = $; diff --git a/src/legacy/ui/public/flot-charts/jquery.colorhelpers.js b/src/legacy/ui/public/flot-charts/jquery.colorhelpers.js deleted file mode 100644 index b2f6dc4e433a3..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.colorhelpers.js +++ /dev/null @@ -1,180 +0,0 @@ -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ - -(function($) { - $.color = {}; - - // construct color object with some convenient chainable helpers - $.color.make = function (r, g, b, a) { - var o = {}; - o.r = r || 0; - o.g = g || 0; - o.b = b || 0; - o.a = a != null ? a : 1; - - o.add = function (c, d) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] += d; - return o.normalize(); - }; - - o.scale = function (c, f) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] *= f; - return o.normalize(); - }; - - o.toString = function () { - if (o.a >= 1.0) { - return "rgb("+[o.r, o.g, o.b].join(",")+")"; - } else { - return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; - } - }; - - o.normalize = function () { - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - o.r = clamp(0, parseInt(o.r), 255); - o.g = clamp(0, parseInt(o.g), 255); - o.b = clamp(0, parseInt(o.b), 255); - o.a = clamp(0, o.a, 1); - return o; - }; - - o.clone = function () { - return $.color.make(o.r, o.b, o.g, o.a); - }; - - return o.normalize(); - } - - // extract CSS color property from element, going up in the DOM - // if it's "transparent" - $.color.extract = function (elem, css) { - var c; - - do { - c = elem.css(css).toLowerCase(); - // keep going until we find an element that has color, or - // we hit the body or root (have no parent) - if (c != '' && c != 'transparent') - break; - elem = elem.parent(); - } while (elem.length && !$.nodeName(elem.get(0), "body")); - - // catch Safari's way of signalling transparent - if (c == "rgba(0, 0, 0, 0)") - c = "transparent"; - - return $.color.parse(c); - } - - // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), - // returns color object, if parsing failed, you get black (0, 0, - // 0) out - $.color.parse = function (str) { - var res, m = $.color.make; - - // Look for rgb(num,num,num) - if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); - - // Look for rgba(num,num,num,num) - if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); - - // Look for rgb(num%,num%,num%) - if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); - - // Look for rgba(num%,num%,num%,num) - if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); - - // Look for #a0b1c2 - if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) - return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); - - // Look for #fff - if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) - return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); - - // Otherwise, we're most likely dealing with a named color - var name = $.trim(str).toLowerCase(); - if (name == "transparent") - return m(255, 255, 255, 0); - else { - // default to black - res = lookupColors[name] || [0, 0, 0]; - return m(res[0], res[1], res[2]); - } - } - - var lookupColors = { - aqua:[0,255,255], - azure:[240,255,255], - beige:[245,245,220], - black:[0,0,0], - blue:[0,0,255], - brown:[165,42,42], - cyan:[0,255,255], - darkblue:[0,0,139], - darkcyan:[0,139,139], - darkgrey:[169,169,169], - darkgreen:[0,100,0], - darkkhaki:[189,183,107], - darkmagenta:[139,0,139], - darkolivegreen:[85,107,47], - darkorange:[255,140,0], - darkorchid:[153,50,204], - darkred:[139,0,0], - darksalmon:[233,150,122], - darkviolet:[148,0,211], - fuchsia:[255,0,255], - gold:[255,215,0], - green:[0,128,0], - indigo:[75,0,130], - khaki:[240,230,140], - lightblue:[173,216,230], - lightcyan:[224,255,255], - lightgreen:[144,238,144], - lightgrey:[211,211,211], - lightpink:[255,182,193], - lightyellow:[255,255,224], - lime:[0,255,0], - magenta:[255,0,255], - maroon:[128,0,0], - navy:[0,0,128], - olive:[128,128,0], - orange:[255,165,0], - pink:[255,192,203], - purple:[128,0,128], - violet:[128,0,128], - red:[255,0,0], - silver:[192,192,192], - white:[255,255,255], - yellow:[255,255,0] - }; -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.canvas.js b/src/legacy/ui/public/flot-charts/jquery.flot.canvas.js deleted file mode 100644 index 29328d5812127..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.canvas.js +++ /dev/null @@ -1,345 +0,0 @@ -/* Flot plugin for drawing all elements of a plot on the canvas. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Flot normally produces certain elements, like axis labels and the legend, using -HTML elements. This permits greater interactivity and customization, and often -looks better, due to cross-browser canvas text inconsistencies and limitations. - -It can also be desirable to render the plot entirely in canvas, particularly -if the goal is to save it as an image, or if Flot is being used in a context -where the HTML DOM does not exist, as is the case within Node.js. This plugin -switches out Flot's standard drawing operations for canvas-only replacements. - -Currently the plugin supports only axis labels, but it will eventually allow -every element of the plot to be rendered directly to canvas. - -The plugin supports these options: - -{ - canvas: boolean -} - -The "canvas" option controls whether full canvas drawing is enabled, making it -possible to toggle on and off. This is useful when a plot uses HTML text in the -browser, but needs to redraw with canvas text when exporting as an image. - -*/ - -(function($) { - - var options = { - canvas: true - }; - - var render, getTextInfo, addText; - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - function init(plot, classes) { - - var Canvas = classes.Canvas; - - // We only want to replace the functions once; the second time around - // we would just get our new function back. This whole replacing of - // prototype functions is a disaster, and needs to be changed ASAP. - - if (render == null) { - getTextInfo = Canvas.prototype.getTextInfo, - addText = Canvas.prototype.addText, - render = Canvas.prototype.render; - } - - // Finishes rendering the canvas, including overlaid text - - Canvas.prototype.render = function() { - - if (!plot.getOptions().canvas) { - return render.call(this); - } - - var context = this.context, - cache = this._textCache; - - // For each text layer, render elements marked as active - - context.save(); - context.textBaseline = "middle"; - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - var layerCache = cache[layerKey]; - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey], - updateStyles = true; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var info = styleCache[key], - positions = info.positions, - lines = info.lines; - - // Since every element at this level of the cache have the - // same font and fill styles, we can just change them once - // using the values from the first element. - - if (updateStyles) { - context.fillStyle = info.font.color; - context.font = info.font.definition; - updateStyles = false; - } - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - for (var j = 0, line; line = position.lines[j]; j++) { - context.fillText(lines[j].text, line[0], line[1]); - } - } else { - positions.splice(i--, 1); - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - } - } - - context.restore(); - }; - - // Creates (if necessary) and returns a text info object. - // - // When the canvas option is set, the object looks like this: - // - // { - // width: Width of the text's bounding box. - // height: Height of the text's bounding box. - // positions: Array of positions at which this text is drawn. - // lines: [{ - // height: Height of this line. - // widths: Width of this line. - // text: Text on this line. - // }], - // font: { - // definition: Canvas font property string. - // color: Color of the text. - // }, - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // lines: Array of [x, y] coordinates at which to draw the line. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - if (!plot.getOptions().canvas) { - return getTextInfo.call(this, layer, text, font, angle, width); - } - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number - - text = "" + text; - - // If the font is a font-spec object, generate a CSS definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - if (info == null) { - - var context = this.context; - - // If the font was provided as CSS, create a div with those - // classes and examine it to generate a canvas font spec. - - if (typeof font !== "object") { - - var element = $("
 
") - .css("position", "absolute") - .addClass(typeof font === "string" ? font : null) - .appendTo(this.getTextLayer(layer)); - - font = { - lineHeight: element.height(), - style: element.css("font-style"), - variant: element.css("font-variant"), - weight: element.css("font-weight"), - family: element.css("font-family"), - color: element.css("color") - }; - - // Setting line-height to 1, without units, sets it equal - // to the font-size, even if the font-size is abstract, - // like 'smaller'. This enables us to read the real size - // via the element's height, working around browsers that - // return the literal 'smaller' value. - - font.size = element.css("line-height", 1).height(); - - element.remove(); - } - - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - - // Create a new info object, initializing the dimensions to - // zero so we can count them up line-by-line. - - info = styleCache[text] = { - width: 0, - height: 0, - positions: [], - lines: [], - font: { - definition: textStyle, - color: font.color - } - }; - - context.save(); - context.font = textStyle; - - // Canvas can't handle multi-line strings; break on various - // newlines, including HTML brs, to build a list of lines. - // Note that we could split directly on regexps, but IE < 9 is - // broken; revisit when we drop IE 7/8 support. - - var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); - - for (var i = 0; i < lines.length; ++i) { - - var lineText = lines[i], - measured = context.measureText(lineText); - - info.width = Math.max(measured.width, info.width); - info.height += font.lineHeight; - - info.lines.push({ - text: lineText, - width: measured.width, - height: font.lineHeight - }); - } - - context.restore(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - if (!plot.getOptions().canvas) { - return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); - } - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions, - lines = info.lines; - - // Text is drawn with baseline 'middle', which we need to account - // for by adding half a line's height to the y position. - - y += info.height / lines.length / 2; - - // Tweak the initial y-position to match vertical alignment - - if (valign == "middle") { - y = Math.round(y - info.height / 2); - } else if (valign == "bottom") { - y = Math.round(y - info.height); - } else { - y = Math.round(y); - } - - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 - - // Offset the y coordinate, since Opera is off pretty - // consistently compared to the other browsers. - - if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { - y -= 2; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - position = { - active: true, - lines: [], - x: x, - y: y - }; - - positions.push(position); - - // Fill in the x & y positions of each line, adjusting them - // individually for horizontal alignment. - - for (var i = 0, line; line = lines[i]; i++) { - if (halign == "center") { - position.lines.push([Math.round(x - line.width / 2), y]); - } else if (halign == "right") { - position.lines.push([Math.round(x - line.width), y]); - } else { - position.lines.push([Math.round(x), y]); - } - y += line.height; - } - }; - } - - $.plot.plugins.push({ - init: init, - options: options, - name: "canvas", - version: "1.0" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.categories.js b/src/legacy/ui/public/flot-charts/jquery.flot.categories.js deleted file mode 100644 index 2f9b257971499..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.categories.js +++ /dev/null @@ -1,190 +0,0 @@ -/* Flot plugin for plotting textual data or categories. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin -allows you to plot such a dataset directly. - -To enable it, you must specify mode: "categories" on the axis with the textual -labels, e.g. - - $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); - -By default, the labels are ordered as they are met in the data series. If you -need a different ordering, you can specify "categories" on the axis options -and list the categories there: - - xaxis: { - mode: "categories", - categories: ["February", "March", "April"] - } - -If you need to customize the distances between the categories, you can specify -"categories" as an object mapping labels to values - - xaxis: { - mode: "categories", - categories: { "February": 1, "March": 3, "April": 4 } - } - -If you don't specify all categories, the remaining categories will be numbered -from the max value plus 1 (with a spacing of 1 between each). - -Internally, the plugin works by transforming the input data through an auto- -generated mapping where the first category becomes 0, the second 1, etc. -Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this -is visible in hover and click events that return numbers rather than the -category labels). The plugin also overrides the tick generator to spit out the -categories as ticks instead of the values. - -If you need to map a value back to its label, the mapping is always accessible -as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. - -*/ - -(function ($) { - var options = { - xaxis: { - categories: null - }, - yaxis: { - categories: null - } - }; - - function processRawData(plot, series, data, datapoints) { - // if categories are enabled, we need to disable - // auto-transformation to numbers so the strings are intact - // for later processing - - var xCategories = series.xaxis.options.mode == "categories", - yCategories = series.yaxis.options.mode == "categories"; - - if (!(xCategories || yCategories)) - return; - - var format = datapoints.format; - - if (!format) { - // FIXME: auto-detection should really not be defined here - var s = series; - format = []; - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - datapoints.format = format; - } - - for (var m = 0; m < format.length; ++m) { - if (format[m].x && xCategories) - format[m].number = false; - - if (format[m].y && yCategories) - format[m].number = false; - } - } - - function getNextIndex(categories) { - var index = -1; - - for (var v in categories) - if (categories[v] > index) - index = categories[v]; - - return index + 1; - } - - function categoriesTickGenerator(axis) { - var res = []; - for (var label in axis.categories) { - var v = axis.categories[label]; - if (v >= axis.min && v <= axis.max) - res.push([v, label]); - } - - res.sort(function (a, b) { return a[0] - b[0]; }); - - return res; - } - - function setupCategoriesForAxis(series, axis, datapoints) { - if (series[axis].options.mode != "categories") - return; - - if (!series[axis].categories) { - // parse options - var c = {}, o = series[axis].options.categories || {}; - if ($.isArray(o)) { - for (var i = 0; i < o.length; ++i) - c[o[i]] = i; - } - else { - for (var v in o) - c[v] = o[v]; - } - - series[axis].categories = c; - } - - // fix ticks - if (!series[axis].options.ticks) - series[axis].options.ticks = categoriesTickGenerator; - - transformPointsOnAxis(datapoints, axis, series[axis].categories); - } - - function transformPointsOnAxis(datapoints, axis, categories) { - // go through the points, transforming them - var points = datapoints.points, - ps = datapoints.pointsize, - format = datapoints.format, - formatColumn = axis.charAt(0), - index = getNextIndex(categories); - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - - for (var m = 0; m < ps; ++m) { - var val = points[i + m]; - - if (val == null || !format[m][formatColumn]) - continue; - - if (!(val in categories)) { - categories[val] = index; - ++index; - } - - points[i + m] = categories[val]; - } - } - } - - function processDatapoints(plot, series, datapoints) { - setupCategoriesForAxis(series, "xaxis", datapoints); - setupCategoriesForAxis(series, "yaxis", datapoints); - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.processDatapoints.push(processDatapoints); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'categories', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.crosshair.js b/src/legacy/ui/public/flot-charts/jquery.flot.crosshair.js deleted file mode 100644 index 5111695e3d12c..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.crosshair.js +++ /dev/null @@ -1,176 +0,0 @@ -/* Flot plugin for showing crosshairs when the mouse hovers over the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical -crosshair that lets you trace the values on the x axis, "y" enables a -horizontal crosshair and "xy" enables them both. "color" is the color of the -crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of -the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair( pos ) - - Set the position of the crosshair. Note that this is cleared if the user - moves the mouse. "pos" is in coordinates of the plot and should be on the - form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple - axes), which is coincidentally the same format as what you get from a - "plothover" event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer updating if - the user moves the mouse. Optionally supply a position (passed on to - setCrosshair()) to move it to. - - Example usage: - - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind( "plothover", function ( evt, position, item ) { - if ( item ) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ - x: item.datapoint[ 0 ], - y: item.datapoint[ 1 ] - }); - } else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - }; - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - }; - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; - - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - var drawX = Math.floor(crosshair.x) + adj; - ctx.moveTo(drawX, 0); - ctx.lineTo(drawX, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - var drawY = Math.floor(crosshair.y) + adj; - ctx.moveTo(0, drawY); - ctx.lineTo(plot.width(), drawY); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.errorbars.js b/src/legacy/ui/public/flot-charts/jquery.flot.errorbars.js deleted file mode 100644 index 655036e0db846..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.errorbars.js +++ /dev/null @@ -1,353 +0,0 @@ -/* Flot plugin for plotting error bars. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Error bars are used to show standard deviation and other statistical -properties in a plot. - -* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com - -This plugin allows you to plot error-bars over points. Set "errorbars" inside -the points series to the axis name over which there will be error values in -your data array (*even* if you do not intend to plot them later, by setting -"show: null" on xerr/yerr). - -The plugin supports these options: - - series: { - points: { - errorbars: "x" or "y" or "xy", - xerr: { - show: null/false or true, - asymmetric: null/false or true, - upperCap: null or "-" or function, - lowerCap: null or "-" or function, - color: null or color, - radius: null or number - }, - yerr: { same options as xerr } - } - } - -Each data point array is expected to be of the type: - - "x" [ x, y, xerr ] - "y" [ x, y, yerr ] - "xy" [ x, y, xerr, yerr ] - -Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and -equivalently for yerr. E.g., a datapoint for the "xy" case with symmetric -error-bars on X and asymmetric on Y would be: - - [ x, y, xerr, yerr_lower, yerr_upper ] - -By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will -draw a small cap perpendicular to the error bar. They can also be set to a -user-defined drawing function, with (ctx, x, y, radius) as parameters, as e.g.: - - function drawSemiCircle( ctx, x, y, radius ) { - ctx.beginPath(); - ctx.arc( x, y, radius, 0, Math.PI, false ); - ctx.moveTo( x - radius, y ); - ctx.lineTo( x + radius, y ); - ctx.stroke(); - } - -Color and radius both default to the same ones of the points series if not -set. The independent radius parameter on xerr/yerr is useful for the case when -we may want to add error-bars to a line, without showing the interconnecting -points (with radius: 0), and still showing end caps on the error-bars. -shadowSize and lineWidth are derived as well from the points series. - -*/ - -(function ($) { - var options = { - series: { - points: { - errorbars: null, //should be 'x', 'y' or 'xy' - xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, - yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} - } - } - }; - - function processRawData(plot, series, data, datapoints){ - if (!series.points.errorbars) - return; - - // x,y values - var format = [ - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - - var errors = series.points.errorbars; - // error bars - first X then Y - if (errors == 'x' || errors == 'xy') { - // lower / upper error - if (series.points.xerr.asymmetric) { - format.push({ x: true, number: true, required: true }); - format.push({ x: true, number: true, required: true }); - } else - format.push({ x: true, number: true, required: true }); - } - if (errors == 'y' || errors == 'xy') { - // lower / upper error - if (series.points.yerr.asymmetric) { - format.push({ y: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - } else - format.push({ y: true, number: true, required: true }); - } - datapoints.format = format; - } - - function parseErrors(series, i){ - - var points = series.datapoints.points; - - // read errors from points array - var exl = null, - exu = null, - eyl = null, - eyu = null; - var xerr = series.points.xerr, - yerr = series.points.yerr; - - var eb = series.points.errorbars; - // error bars - first X - if (eb == 'x' || eb == 'xy') { - if (xerr.asymmetric) { - exl = points[i + 2]; - exu = points[i + 3]; - if (eb == 'xy') - if (yerr.asymmetric){ - eyl = points[i + 4]; - eyu = points[i + 5]; - } else eyl = points[i + 4]; - } else { - exl = points[i + 2]; - if (eb == 'xy') - if (yerr.asymmetric) { - eyl = points[i + 3]; - eyu = points[i + 4]; - } else eyl = points[i + 3]; - } - // only Y - } else if (eb == 'y') - if (yerr.asymmetric) { - eyl = points[i + 2]; - eyu = points[i + 3]; - } else eyl = points[i + 2]; - - // symmetric errors? - if (exu == null) exu = exl; - if (eyu == null) eyu = eyl; - - var errRanges = [exl, exu, eyl, eyu]; - // nullify if not showing - if (!xerr.show){ - errRanges[0] = null; - errRanges[1] = null; - } - if (!yerr.show){ - errRanges[2] = null; - errRanges[3] = null; - } - return errRanges; - } - - function drawSeriesErrors(plot, ctx, s){ - - var points = s.datapoints.points, - ps = s.datapoints.pointsize, - ax = [s.xaxis, s.yaxis], - radius = s.points.radius, - err = [s.points.xerr, s.points.yerr]; - - //sanity check, in case some inverted axis hack is applied to flot - var invertX = false; - if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { - invertX = true; - var tmp = err[0].lowerCap; - err[0].lowerCap = err[0].upperCap; - err[0].upperCap = tmp; - } - - var invertY = false; - if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { - invertY = true; - var tmp = err[1].lowerCap; - err[1].lowerCap = err[1].upperCap; - err[1].upperCap = tmp; - } - - for (var i = 0; i < s.datapoints.points.length; i += ps) { - - //parse - var errRanges = parseErrors(s, i); - - //cycle xerr & yerr - for (var e = 0; e < err.length; e++){ - - var minmax = [ax[e].min, ax[e].max]; - - //draw this error? - if (errRanges[e * err.length]){ - - //data coordinates - var x = points[i], - y = points[i + 1]; - - //errorbar ranges - var upper = [x, y][e] + errRanges[e * err.length + 1], - lower = [x, y][e] - errRanges[e * err.length]; - - //points outside of the canvas - if (err[e].err == 'x') - if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) - continue; - if (err[e].err == 'y') - if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) - continue; - - // prevent errorbars getting out of the canvas - var drawUpper = true, - drawLower = true; - - if (upper > minmax[1]) { - drawUpper = false; - upper = minmax[1]; - } - if (lower < minmax[0]) { - drawLower = false; - lower = minmax[0]; - } - - //sanity check, in case some inverted axis hack is applied to flot - if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { - //swap coordinates - var tmp = lower; - lower = upper; - upper = tmp; - tmp = drawLower; - drawLower = drawUpper; - drawUpper = tmp; - tmp = minmax[0]; - minmax[0] = minmax[1]; - minmax[1] = tmp; - } - - // convert to pixels - x = ax[0].p2c(x), - y = ax[1].p2c(y), - upper = ax[e].p2c(upper); - lower = ax[e].p2c(lower); - minmax[0] = ax[e].p2c(minmax[0]); - minmax[1] = ax[e].p2c(minmax[1]); - - //same style as points by default - var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, - sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; - - //shadow as for points - if (lw > 0 && sw > 0) { - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); - } - - ctx.strokeStyle = err[e].color? err[e].color: s.color; - ctx.lineWidth = lw; - //draw it - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); - } - } - } - } - - function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ - - //shadow offset - y += offset; - upper += offset; - lower += offset; - - // error bar - avoid plotting over circles - if (err.err == 'x'){ - if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); - else drawUpper = false; - if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); - else drawLower = false; - } - else { - if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); - else drawUpper = false; - if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); - else drawLower = false; - } - - //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps - //this is a way to get errorbars on lines without visible connecting dots - radius = err.radius != null? err.radius: radius; - - // upper cap - if (drawUpper) { - if (err.upperCap == '-'){ - if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); - else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); - } else if ($.isFunction(err.upperCap)){ - if (err.err=='x') err.upperCap(ctx, upper, y, radius); - else err.upperCap(ctx, x, upper, radius); - } - } - // lower cap - if (drawLower) { - if (err.lowerCap == '-'){ - if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); - else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); - } else if ($.isFunction(err.lowerCap)){ - if (err.err=='x') err.lowerCap(ctx, lower, y, radius); - else err.lowerCap(ctx, x, lower, radius); - } - } - } - - function drawPath(ctx, pts){ - ctx.beginPath(); - ctx.moveTo(pts[0][0], pts[0][1]); - for (var p=1; p < pts.length; p++) - ctx.lineTo(pts[p][0], pts[p][1]); - ctx.stroke(); - } - - function draw(plot, ctx){ - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - $.each(plot.getData(), function (i, s) { - if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) - drawSeriesErrors(plot, ctx, s); - }); - ctx.restore(); - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.draw.push(draw); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'errorbars', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.fillbetween.js b/src/legacy/ui/public/flot-charts/jquery.flot.fillbetween.js deleted file mode 100644 index 18b15d26db8c9..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.fillbetween.js +++ /dev/null @@ -1,226 +0,0 @@ -/* Flot plugin for computing bottoms for filled line and bar charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The case: you've got two series that you want to fill the area between. In Flot -terms, you need to use one as the fill bottom of the other. You can specify the -bottom of each data point as the third coordinate manually, or you can use this -plugin to compute it for you. - -In order to name the other series, you need to give it an id, like this: - - var dataset = [ - { data: [ ... ], id: "foo" } , // use default bottom - { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom - ]; - - $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); - -As a convenience, if the id given is a number that doesn't appear as an id in -the series, it is interpreted as the index in the array instead (so fillBetween: -0 can also mean the first series). - -Internally, the plugin modifies the datapoints in each series. For line series, -extra data points might be inserted through interpolation. Note that at points -where the bottom line is not defined (due to a null point or start/end of line), -the current line will show a gap too. The algorithm comes from the -jquery.flot.stack.js plugin, possibly some code could be shared. - -*/ - -(function ( $ ) { - - var options = { - series: { - fillBetween: null // or number - } - }; - - function init( plot ) { - - function findBottomSeries( s, allseries ) { - - var i; - - for ( i = 0; i < allseries.length; ++i ) { - if ( allseries[ i ].id === s.fillBetween ) { - return allseries[ i ]; - } - } - - if ( typeof s.fillBetween === "number" ) { - if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { - return null; - } - return allseries[ s.fillBetween ]; - } - - return null; - } - - function computeFillBottoms( plot, s, datapoints ) { - - if ( s.fillBetween == null ) { - return; - } - - var other = findBottomSeries( s, plot.getData() ); - - if ( !other ) { - return; - } - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - withbottom = ps > 2 && datapoints.format[2].y, - withsteps = withlines && s.lines.steps, - fromgap = true, - i = 0, - j = 0, - l, m; - - while ( true ) { - - if ( i >= points.length ) { - break; - } - - l = newpoints.length; - - if ( points[ i ] == null ) { - - // copy gaps - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - i += ps; - - } else if ( j >= otherpoints.length ) { - - // for lines, we can't use the rest of the points - - if ( !withlines ) { - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - } - - i += ps; - - } else if ( otherpoints[ j ] == null ) { - - // oops, got a gap - - for ( m = 0; m < ps; ++m ) { - newpoints.push( null ); - } - - fromgap = true; - j += otherps; - - } else { - - // cases where we actually got two points - - px = points[ i ]; - py = points[ i + 1 ]; - qx = otherpoints[ j ]; - qy = otherpoints[ j + 1 ]; - bottom = 0; - - if ( px === qx ) { - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - //newpoints[ l + 1 ] += qy; - bottom = qy; - - i += ps; - j += otherps; - - } else if ( px > qx ) { - - // we got past point below, might need to - // insert interpolated extra point - - if ( withlines && i > 0 && points[ i - ps ] != null ) { - intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); - newpoints.push( qx ); - newpoints.push( intery ); - for ( m = 2; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - bottom = qy; - } - - j += otherps; - - } else { // px < qx - - // if we come from a gap, we just skip this point - - if ( fromgap && withlines ) { - i += ps; - continue; - } - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - // we might be able to interpolate a point below, - // this can give us a better y - - if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { - bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); - } - - //newpoints[l + 1] += bottom; - - i += ps; - } - - fromgap = false; - - if ( l !== newpoints.length && withbottom ) { - newpoints[ l + 2 ] = bottom; - } - } - - // maintain the line steps invariant - - if ( withsteps && l !== newpoints.length && l > 0 && - newpoints[ l ] !== null && - newpoints[ l ] !== newpoints[ l - ps ] && - newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { - for (m = 0; m < ps; ++m) { - newpoints[ l + ps + m ] = newpoints[ l + m ]; - } - newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push( computeFillBottoms ); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: "fillbetween", - version: "1.0" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.image.js b/src/legacy/ui/public/flot-charts/jquery.flot.image.js deleted file mode 100644 index 178f0e69069ef..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.image.js +++ /dev/null @@ -1,241 +0,0 @@ -/* Flot plugin for plotting images. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and -(x2, y2) are where you intend the two opposite corners of the image to end up -in the plot. Image must be a fully loaded JavaScript image (you can make one -with new Image()). If the image is not complete, it's skipped when plotting. - -There are two helpers included for retrieving images. The easiest work the way -that you put in URLs instead of images in the data, like this: - - [ "myimage.png", 0, 0, 10, 10 ] - -Then call $.plot.image.loadData( data, options, callback ) where data and -options are the same as you pass in to $.plot. This loads the images, replaces -the URLs in the data with the corresponding images and calls "callback" when -all images are loaded (or failed loading). In the callback, you can then call -$.plot with the data set. See the included example. - -A more low-level helper, $.plot.image.load(urls, callback) is also included. -Given a list of URLs, it calls callback with an object mapping from URL to -Image object when all images are loaded or have failed loading. - -The plugin supports these options: - - series: { - images: { - show: boolean - anchor: "corner" or "center" - alpha: [ 0, 1 ] - } - } - -They can be specified for a specific series: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - images: { ... } - ]) - -Note that because the data format is different from usual data points, you -can't use images with anything else in a specific data series. - -Setting "anchor" to "center" causes the pixels in the image to be anchored at -the corner pixel centers inside of at the pixel corners, effectively letting -half a pixel stick out to each side in the plot. - -A possible future direction could be support for tiling for large images (like -Google Maps). - -*/ - -(function ($) { - var options = { - series: { - images: { - show: false, - alpha: 1, - anchor: "corner" // or "center" - } - } - }; - - $.plot.image = {}; - - $.plot.image.loadDataImages = function (series, options, callback) { - var urls = [], points = []; - - var defaultShow = options.series.images.show; - - $.each(series, function (i, s) { - if (!(defaultShow || s.images.show)) - return; - - if (s.data) - s = s.data; - - $.each(s, function (i, p) { - if (typeof p[0] == "string") { - urls.push(p[0]); - points.push(p); - } - }); - }); - - $.plot.image.load(urls, function (loadedImages) { - $.each(points, function (i, p) { - var url = p[0]; - if (loadedImages[url]) - p[0] = loadedImages[url]; - }); - - callback(); - }); - } - - $.plot.image.load = function (urls, callback) { - var missing = urls.length, loaded = {}; - if (missing == 0) - callback({}); - - $.each(urls, function (i, url) { - var handler = function () { - --missing; - - loaded[url] = this; - - if (missing == 0) - callback(loaded); - }; - - $('').load(handler).error(handler).attr('src', url); - }); - }; - - function drawSeries(plot, ctx, series) { - var plotOffset = plot.getPlotOffset(); - - if (!series.images || !series.images.show) - return; - - var points = series.datapoints.points, - ps = series.datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var img = points[i], - x1 = points[i + 1], y1 = points[i + 2], - x2 = points[i + 3], y2 = points[i + 4], - xaxis = series.xaxis, yaxis = series.yaxis, - tmp; - - // actually we should check img.complete, but it - // appears to be a somewhat unreliable indicator in - // IE6 (false even after load event) - if (!img || img.width <= 0 || img.height <= 0) - continue; - - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - // if the anchor is at the center of the pixel, expand the - // image by 1/2 pixel in each direction - if (series.images.anchor == "center") { - tmp = 0.5 * (x2-x1) / (img.width - 1); - x1 -= tmp; - x2 += tmp; - tmp = 0.5 * (y2-y1) / (img.height - 1); - y1 -= tmp; - y2 += tmp; - } - - // clip - if (x1 == x2 || y1 == y2 || - x1 >= xaxis.max || x2 <= xaxis.min || - y1 >= yaxis.max || y2 <= yaxis.min) - continue; - - var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; - if (x1 < xaxis.min) { - sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); - x1 = xaxis.min; - } - - if (x2 > xaxis.max) { - sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); - x2 = xaxis.max; - } - - if (y1 < yaxis.min) { - sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); - y1 = yaxis.min; - } - - if (y2 > yaxis.max) { - sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); - y2 = yaxis.max; - } - - x1 = xaxis.p2c(x1); - x2 = xaxis.p2c(x2); - y1 = yaxis.p2c(y1); - y2 = yaxis.p2c(y2); - - // the transformation may have swapped us - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - tmp = ctx.globalAlpha; - ctx.globalAlpha *= series.images.alpha; - ctx.drawImage(img, - sx1, sy1, sx2 - sx1, sy2 - sy1, - x1 + plotOffset.left, y1 + plotOffset.top, - x2 - x1, y2 - y1); - ctx.globalAlpha = tmp; - } - } - - function processRawData(plot, series, data, datapoints) { - if (!series.images.show) - return; - - // format is Image, x1, y1, x2, y2 (opposite corners) - datapoints.format = [ - { required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.drawSeries.push(drawSeries); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'image', - version: '1.1' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.js b/src/legacy/ui/public/flot-charts/jquery.flot.js deleted file mode 100644 index 43db1cc3d93db..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* JavaScript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of columns in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.log.js b/src/legacy/ui/public/flot-charts/jquery.flot.log.js deleted file mode 100644 index e32bf5cf7e817..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.log.js +++ /dev/null @@ -1,163 +0,0 @@ -/* @notice - * - * Pretty handling of logarithmic axes. - * Copyright (c) 2007-2014 IOLA and Ole Laursen. - * Licensed under the MIT license. - * Created by Arne de Laat - * Set axis.mode to "log" and make the axis logarithmic using transform: - * axis: { - * mode: 'log', - * transform: function(v) {v <= 0 ? Math.log(v) / Math.LN10 : null}, - * inverseTransform: function(v) {Math.pow(10, v)} - * } - * The transform filters negative and zero values, because those are - * invalid on logarithmic scales. - * This plugin tries to create good looking logarithmic ticks, using - * unicode superscript characters. If all data to be plotted is between two - * powers of ten then the default flot tick generator and renderer are - * used. Logarithmic ticks are places at powers of ten and at half those - * values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). - * For details, see https://github.com/flot/flot/pull/1328 -*/ - -(function($) { - - function log10(value) { - /* Get the Log10 of the value - */ - return Math.log(value) / Math.LN10; - } - - function floorAsLog10(value) { - /* Get power of the first power of 10 below the value - */ - return Math.floor(log10(value)); - } - - function ceilAsLog10(value) { - /* Get power of the first power of 10 above the value - */ - return Math.ceil(log10(value)); - } - - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - function getUnicodePower(power) { - var superscripts = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"], - result = "", - str_power = "" + power; - for (var i = 0; i < str_power.length; i++) { - if (str_power[i] === "+") { - } - else if (str_power[i] === "-") { - result += "⁻"; - } - else { - result += superscripts[str_power[i]]; - } - } - return result; - } - - function init(plot) { - plot.hooks.processOptions.push(function (plot) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode === "log") { - - axis.tickGenerator = function (axis) { - - var ticks = [], - end = ceilAsLog10(axis.max), - start = floorAsLog10(axis.min), - tick = Number.NaN, - i = 0; - - if (axis.min === null || axis.min <= 0) { - // Bad minimum, make ticks from 1 (10**0) to max - start = 0; - axis.min = 0.6; - } - - if (end <= start) { - // Start less than end?! - ticks = [1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, - 1e7, 1e8, 1e9]; - } - else if (log10(axis.max) - log10(axis.datamin) < 1) { - // Default flot generator incase no powers of 10 - // are between start and end - var prev; - start = floorInBase(axis.min, axis.tickSize); - do { - prev = tick; - tick = start + i * axis.tickSize; - ticks.push(tick); - ++i; - } while (tick < axis.max && tick !== prev); - } - else { - // Make ticks at each power of ten - for (; i <= (end - start); i++) { - tick = Math.pow(10, start + i); - ticks.push(tick); - } - - var length = ticks.length; - - // If not to many ticks also put a tick between - // the powers of ten - if (end - start < 6) { - for (var j = 1; j < length * 2 - 1; j += 2) { - tick = ticks[j - 1] * 5; - ticks.splice(j, 0, tick); - } - } - } - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - var formatted; - if (log10(axis.max) - log10(axis.datamin) < 1) { - // Default flot formatter - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - formatted = "" + Math.round(value * factor) / factor; - if (axis.tickDecimals !== null) { - var decimal = formatted.indexOf("."); - var precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - } - else { - var multiplier = "", - exponential = parseFloat(value).toExponential(0), - power = getUnicodePower(exponential.slice(2)); - if (exponential[0] !== "1") { - multiplier = exponential[0] + "x"; - } - formatted = multiplier + "10" + power; - } - return formatted; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - name: "log", - version: "0.9" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js b/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js deleted file mode 100644 index 13fb7f17d04b2..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.navigate.js +++ /dev/null @@ -1,346 +0,0 @@ -/* Flot plugin for adding the ability to pan and zoom the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The default behaviour is double click and scrollwheel up/down to zoom in, drag -to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and -plot.pan( offset ) so you easily can add custom controls. It also fires -"plotpan" and "plotzoom" events, useful for synchronizing plots. - -The plugin supports these options: - - zoom: { - interactive: false - trigger: "dblclick" // or "click" for single click - amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) - } - - pan: { - interactive: false - cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" - frameRate: 20 - } - - xaxis, yaxis, x2axis, y2axis: { - zoomRange: null // or [ number, number ] (min range, max range) or false - panRange: null // or [ number, number ] (min, max) or false - } - -"interactive" enables the built-in drag/click behaviour. If you enable -interactive for pan, then you'll have a basic plot that supports moving -around; the same for zoom. - -"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to -the current viewport. - -"cursor" is a standard CSS mouse cursor string used for visual feedback to the -user when dragging. - -"frameRate" specifies the maximum number of times per second the plot will -update itself while the user is panning around on it (set to null to disable -intermediate pans, the plot will then not update until the mouse button is -released). - -"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: -[1, 100] the zoom will never scale the axis so that the difference between min -and max is smaller than 1 or larger than 100. You can set either end to null -to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis -will be disabled. - -"panRange" confines the panning to stay within a range, e.g. with panRange: -[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can -be null, e.g. [-10, null]. If you set panRange to false, panning on that axis -will be disabled. - -Example API usage: - - plot = $.plot(...); - - // zoom default amount in on the pixel ( 10, 20 ) - plot.zoom({ center: { left: 10, top: 20 } }); - - // zoom out again - plot.zoomOut({ center: { left: 10, top: 20 } }); - - // zoom 200% in on the pixel (10, 20) - plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); - - // pan 100 pixels to the left and 20 down - plot.pan({ left: -100, top: 20 }) - -Here, "center" specifies where the center of the zooming should happen. Note -that this is defined in pixel space, not the space of the data points (you can -use the p2c helpers on the axes in Flot to help you convert between these). - -"amount" is the amount to zoom the viewport relative to the current range, so -1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You -can set the default in the options. - -*/ - -// First two dependencies, jquery.event.drag.js and -// jquery.mousewheel.js, we put them inline here to save people the -// effort of downloading them. - -/* -jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) -Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt -*/ -(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) { - // make sure min < max - var tmp = min; - min = max; - max = tmp; - } - - //Check that we are in panRange - if (pr) { - if (pr[0] != null && min < pr[0]) { - min = pr[0]; - } - if (pr[1] != null && max > pr[1]) { - max = pr[1]; - } - } - - var range = max - min; - if (zr && - ((zr[0] != null && range < zr[0] && amount >1) || - (zr[1] != null && range > zr[1] && amount <1))) - return; - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotzoom", [ plot, args ]); - }; - - plot.pan = function (args) { - var delta = { - x: +args.left, - y: +args.top - }; - - if (isNaN(delta.x)) - delta.x = 0; - if (isNaN(delta.y)) - delta.y = 0; - - $.each(plot.getAxes(), function (_, axis) { - var opts = axis.options, - min, max, d = delta[axis.direction]; - - min = axis.c2p(axis.p2c(axis.min) + d), - max = axis.c2p(axis.p2c(axis.max) + d); - - var pr = opts.panRange; - if (pr === false) // no panning on this axis - return; - - if (pr) { - // check whether we hit the wall - if (pr[0] != null && pr[0] > min) { - d = pr[0] - min; - min += d; - max += d; - } - - if (pr[1] != null && pr[1] < max) { - d = pr[1] - max; - min += d; - max += d; - } - } - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotpan", [ plot, args ]); - }; - - function shutdown(plot, eventHolder) { - eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); - eventHolder.unbind("mousewheel", onMouseWheel); - eventHolder.unbind("dragstart", onDragStart); - eventHolder.unbind("drag", onDrag); - eventHolder.unbind("dragend", onDragEnd); - if (panTimeout) - clearTimeout(panTimeout); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'navigate', - version: '1.3' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.pie.js b/src/legacy/ui/public/flot-charts/jquery.flot.pie.js deleted file mode 100644 index 06f900bdc950f..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.pie.js +++ /dev/null @@ -1,824 +0,0 @@ -/* Flot plugin for rendering pie charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes that each series has a single data value, and that each -value is a positive integer or zero. Negative numbers don't make sense for a -pie chart, and have unpredictable results. The values do NOT need to be -passed in as percentages; the plugin will calculate the total and per-slice -percentages internally. - -* Created by Brian Medendorp - -* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars - -The plugin supports these options: - - series: { - pie: { - show: true/false - radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' - innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect - startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result - tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) - offset: { - top: integer value to move the pie up or down - left: integer value to move the pie left or right, or 'auto' - }, - stroke: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') - width: integer pixel width of the stroke - }, - label: { - show: true/false, or 'auto' - formatter: a user-defined function that modifies the text/style of the label text - radius: 0-1 for percentage of fullsize, or a specified pixel length - background: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') - opacity: 0-1 - }, - threshold: 0-1 for the percentage value at which to hide labels (if they're too small) - }, - combine: { - threshold: 0-1 for the percentage value at which to combine slices (if they're too small) - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined - label: any text value of what the combined slice should be labeled - } - highlight: { - opacity: 0-1 - } - } - } - -More detail and specific examples can be found in the included HTML file. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - // Maximum redraw attempts when fitting labels within the plot - - var REDRAW_ATTEMPTS = 10; - - // Factor by which to shrink the pie when fitting labels within the plot - - var REDRAW_SHRINK = 0.95; - - function init(plot) { - - var canvas = null, - target = null, - options = null, - maxRadius = null, - centerLeft = null, - centerTop = null, - processed = false, - ctx = null; - - // interactive variables - - var highlights = []; - - // add hook to determine if pie plugin in enabled, and then perform necessary operations - - plot.hooks.processOptions.push(function(plot, options) { - if (options.series.pie.show) { - - options.grid.show = false; - - // set labels.show - - if (options.series.pie.label.show == "auto") { - if (options.legend.show) { - options.series.pie.label.show = false; - } else { - options.series.pie.label.show = true; - } - } - - // set radius - - if (options.series.pie.radius == "auto") { - if (options.series.pie.label.show) { - options.series.pie.radius = 3/4; - } else { - options.series.pie.radius = 1; - } - } - - // ensure sane tilt - - if (options.series.pie.tilt > 1) { - options.series.pie.tilt = 1; - } else if (options.series.pie.tilt < 0) { - options.series.pie.tilt = 0; - } - } - }); - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var options = plot.getOptions(); - if (options.series.pie.show) { - if (options.grid.hoverable) { - eventHolder.unbind("mousemove").mousemove(onMouseMove); - } - if (options.grid.clickable) { - eventHolder.unbind("click").click(onClick); - } - } - }); - - plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { - var options = plot.getOptions(); - if (options.series.pie.show) { - processDatapoints(plot, series, data, datapoints); - } - }); - - plot.hooks.drawOverlay.push(function(plot, octx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - drawOverlay(plot, octx); - } - }); - - plot.hooks.draw.push(function(plot, newCtx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - draw(plot, newCtx); - } - }); - - function processDatapoints(plot, series, datapoints) { - if (!processed) { - processed = true; - canvas = plot.getCanvas(); - target = $(canvas).parent(); - options = plot.getOptions(); - plot.setData(combine(plot.getData())); - } - } - - function combine(data) { - - var total = 0, - combined = 0, - numCombined = 0, - color = options.series.pie.combine.color, - newdata = []; - - // Fix up the raw data from Flot, ensuring the data is numeric - - for (var i = 0; i < data.length; ++i) { - - var value = data[i].data; - - // If the data is an array, we'll assume that it's a standard - // Flot x-y pair, and are concerned only with the second value. - - // Note how we use the original array, rather than creating a - // new one; this is more efficient and preserves any extra data - // that the user may have stored in higher indexes. - - if ($.isArray(value) && value.length == 1) { - value = value[0]; - } - - if ($.isArray(value)) { - // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 - if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { - value[1] = +value[1]; - } else { - value[1] = 0; - } - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - value = [1, +value]; - } else { - value = [1, 0]; - } - - data[i].data = [value]; - } - - // Sum up all the slices, so we can calculate percentages for each - - for (var i = 0; i < data.length; ++i) { - total += data[i].data[0][1]; - } - - // Count the number of slices with percentages below the combine - // threshold; if it turns out to be just one, we won't combine. - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (value / total <= options.series.pie.combine.threshold) { - combined += value; - numCombined++; - if (!color) { - color = data[i].color; - } - } - } - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { - newdata.push( - $.extend(data[i], { /* extend to allow keeping all other original data values - and using them e.g. in labelFormatter. */ - data: [[1, value]], - color: data[i].color, - label: data[i].label, - angle: value * Math.PI * 2 / total, - percent: value / (total / 100) - }) - ); - } - } - - if (numCombined > 1) { - newdata.push({ - data: [[1, combined]], - color: color, - label: options.series.pie.combine.label, - angle: combined * Math.PI * 2 / total, - percent: combined / (total / 100) - }); - } - - return newdata; - } - - function draw(plot, newCtx) { - - if (!target) { - return; // if no series were passed - } - - var canvasWidth = plot.getPlaceholder().width(), - canvasHeight = plot.getPlaceholder().height(), - legendWidth = target.children().filter(".legend").children().width() || 0; - - ctx = newCtx; - - // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! - - // When combining smaller slices into an 'other' slice, we need to - // add a new series. Since Flot gives plugins no way to modify the - // list of series, the pie plugin uses a hack where the first call - // to processDatapoints results in a call to setData with the new - // list of series, then subsequent processDatapoints do nothing. - - // The plugin-global 'processed' flag is used to control this hack; - // it starts out false, and is set to true after the first call to - // processDatapoints. - - // Unfortunately this turns future setData calls into no-ops; they - // call processDatapoints, the flag is true, and nothing happens. - - // To fix this we'll set the flag back to false here in draw, when - // all series have been processed, so the next sequence of calls to - // processDatapoints once again starts out with a slice-combine. - // This is really a hack; in 0.9 we need to give plugins a proper - // way to modify series before any processing begins. - - processed = false; - - // calculate maximum radius and center point - - maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; - centerTop = canvasHeight / 2 + options.series.pie.offset.top; - centerLeft = canvasWidth / 2; - - if (options.series.pie.offset.left == "auto") { - if (options.legend.position.match("w")) { - centerLeft += legendWidth / 2; - } else { - centerLeft -= legendWidth / 2; - } - if (centerLeft < maxRadius) { - centerLeft = maxRadius; - } else if (centerLeft > canvasWidth - maxRadius) { - centerLeft = canvasWidth - maxRadius; - } - } else { - centerLeft += options.series.pie.offset.left; - } - - var slices = plot.getData(), - attempts = 0; - - // Keep shrinking the pie's radius until drawPie returns true, - // indicating that all the labels fit, or we try too many times. - - do { - if (attempts > 0) { - maxRadius *= REDRAW_SHRINK; - } - attempts += 1; - clear(); - if (options.series.pie.tilt <= 0.8) { - drawShadow(); - } - } while (!drawPie() && attempts < REDRAW_ATTEMPTS) - - if (attempts >= REDRAW_ATTEMPTS) { - clear(); - const errorMessage = i18n.translate('common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage', { - defaultMessage: 'Could not draw pie with labels contained inside canvas', - }); - target.prepend(`
${errorMessage}
`); - } - - if (plot.setSeries && plot.insertLegend) { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - target.children().filter(".pieLabel, .pieLabelBackground").remove(); - } - - function drawShadow() { - - var shadowLeft = options.series.pie.shadow.left; - var shadowTop = options.series.pie.shadow.top; - var edge = 10; - var alpha = options.series.pie.shadow.alpha; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { - return; // shadow would be outside canvas, so don't draw it - } - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = "#000"; - - // center and rotate to starting position - - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - - for (var i = 1; i <= edge; i++) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2, false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() { - - var startAngle = Math.PI * options.series.pie.startAngle; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - // center and rotate to starting position - - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - - if (options.series.pie.stroke.width > 0) { - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - } - ctx.restore(); - } - - // draw donut hole - - drawDonutHole(ctx); - - ctx.restore(); - - // Draw the labels, returning true if they fit within the plot - - if (options.series.pie.label.show) { - return drawLabels(); - } else return true; - - function drawSlice(angle, color, fill) { - - if (angle <= 0 || isNaN(angle)) { - return; - } - - if (fill) { - ctx.fillStyle = color; - } else { - ctx.strokeStyle = color; - ctx.lineJoin = "round"; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI * 2) > 0.000000001) { - ctx.moveTo(0, 0); // Center of the pie - } - - //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera - ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); - ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) { - ctx.fill(); - } else { - ctx.stroke(); - } - } - - function drawLabels() { - - var currentAngle = startAngle; - var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) { - if (slices[i].percent >= options.series.pie.label.threshold * 100) { - if (!drawLabel(slices[i], currentAngle, i)) { - return false; - } - } - currentAngle += slices[i].angle; - } - - return true; - - function drawLabel(slice, startAngle, index) { - - if (slice.data[0][1] == 0) { - return true; - } - - // format label text - - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - - if (lf) { - text = lf(slice.label, slice); - } else { - text = slice.label; - } - - if (plf) { - text = plf(text, slice); - } - - var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = "" + text + ""; - target.append(html); - - var label = target.children("#pieLabel" + index); - var labelTop = (y - label.height() / 2); - var labelLeft = (x - label.width() / 2); - - label.css("top", labelTop); - label.css("left", labelLeft); - - // check to make sure that the label is not outside the canvas - - if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { - return false; - } - - if (options.series.pie.label.background.opacity != 0) { - - // put in the transparent background separately to avoid blended labels and label boxes - - var c = options.series.pie.label.background.color; - - if (c == null) { - c = slice.color; - } - - var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; - $("
") - .css("opacity", options.series.pie.label.background.opacity) - .insertBefore(label); - } - - return true; - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - - function drawDonutHole(layer) { - if (options.series.pie.innerRadius > 0) { - - // subtract the center - - layer.save(); - var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.stroke(); - layer.closePath(); - layer.restore(); - - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) { - - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, - x, y; - - for (var i = 0; i < slices.length; ++i) { - - var s = slices[i]; - - if (s.pie.show) { - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); - ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); - ctx.closePath(); - x = mouseX - centerLeft; - y = mouseY - centerTop; - - if (ctx.isPointInPath) { - if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } else { - - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - - var p1X = radius * Math.cos(s.startAngle), - p1Y = radius * Math.sin(s.startAngle), - p2X = radius * Math.cos(s.startAngle + s.angle / 4), - p2Y = radius * Math.sin(s.startAngle + s.angle / 4), - p3X = radius * Math.cos(s.startAngle + s.angle / 2), - p3Y = radius * Math.sin(s.startAngle + s.angle / 2), - p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), - p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), - p5X = radius * Math.cos(s.startAngle + s.angle), - p5Y = radius * Math.sin(s.startAngle + s.angle), - arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], - arrPoint = [x, y]; - - // TODO: perhaps do some mathematical trickery here with the Y-coordinate to compensate for pie tilt? - - if (isPointInPoly(arrPoly, arrPoint)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } - - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) { - triggerClickHoverEvent("plothover", e); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - - function triggerClickHoverEvent(eventname, e) { - - var offset = plot.offset(); - var canvasX = parseInt(e.pageX - offset.left); - var canvasY = parseInt(e.pageY - offset.top); - var item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) { - - // clear auto-highlights - - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) { - unhighlight(h.series); - } - } - } - - // highlight the slice - - if (item) { - highlight(item.series, eventname); - } - - // trigger any hover bind events - - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [pos, item]); - } - - function highlight(s, auto) { - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i == -1) { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } else if (!auto) { - highlights[i].auto = false; - } - } - - function unhighlight(s) { - if (s == null) { - highlights = []; - plot.triggerRedrawOverlay(); - } - - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i != -1) { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) { - - var options = plot.getOptions(); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (var i = 0; i < highlights.length; ++i) { - drawHighlight(highlights[i].series); - } - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) { - - if (series.angle <= 0 || isNaN(series.angle)) { - return; - } - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor - octx.beginPath(); - if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { - octx.moveTo(0, 0); // Center of the pie - } - octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); - octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); - octx.closePath(); - octx.fill(); - } - } - } // end init (plugin body) - - // define pie specific options and their default values - - var options = { - series: { - pie: { - show: false, - radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius: 0, /* for donut */ - startAngle: 3/2, - tilt: 1, - shadow: { - left: 5, // shadow left offset - top: 15, // shadow top offset - alpha: 0.02 // shadow alpha - }, - offset: { - top: 0, - left: "auto" - }, - stroke: { - color: "#fff", - width: 1 - }, - label: { - show: "auto", - formatter: function(label, slice) { - return "
" + label + "
" + Math.round(slice.percent) + "%
"; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: "Other" // label to give the new slice - }, - highlight: { - //color: "#fff", // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.1" - }); - -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.resize.js b/src/legacy/ui/public/flot-charts/jquery.flot.resize.js deleted file mode 100644 index 8a626dda0addb..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.resize.js +++ /dev/null @@ -1,59 +0,0 @@ -/* Flot plugin for automatically redrawing plots as the placeholder resizes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -It works by listening for changes on the placeholder div (through the jQuery -resize event plugin) - if the size changes, it will redraw the plot. - -There are no options. If you need to disable the plugin for some plots, you -can just fix the size of their placeholders. - -*/ - -/* Inline dependency: - * jQuery resize event - v1.1 - 3/14/2010 - * http://benalman.com/projects/jquery-resize-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ -(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); - -(function ($) { - var options = { }; // no options - - function init(plot) { - function onResize() { - var placeholder = plot.getPlaceholder(); - - // somebody might have hidden us and we can't plot - // when we don't have the dimensions - if (placeholder.width() == 0 || placeholder.height() == 0) - return; - - plot.resize(); - plot.setupGrid(); - plot.draw(); - } - - function bindEvents(plot, eventHolder) { - plot.getPlaceholder().resize(onResize); - } - - function shutdown(plot, eventHolder) { - plot.getPlaceholder().unbind("resize", onResize); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'resize', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.selection.js b/src/legacy/ui/public/flot-charts/jquery.flot.selection.js deleted file mode 100644 index c8707b30f4e6f..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin also adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.stack.js b/src/legacy/ui/public/flot-charts/jquery.flot.stack.js deleted file mode 100644 index 0d91c0f3c0160..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlaying them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js b/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js b/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js deleted file mode 100644 index 8c99c401d87e5..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.threshold.js +++ /dev/null @@ -1,142 +0,0 @@ -/* Flot plugin for thresholding data. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - series: { - threshold: { - below: number - color: colorspec - } - } - -It can also be applied to a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - threshold: { ... } - }]) - -An array can be passed for multiple thresholding, like this: - - threshold: [{ - below: number1 - color: color1 - },{ - below: number2 - color: color2 - }] - -These multiple threshold objects can be passed in any order since they are -sorted by the processing function. - -The data points below "below" are drawn with the specified color. This makes -it easy to mark points below 0, e.g. for budget data. - -Internally, the plugin works by splitting the data into two series, above and -below the threshold. The extra series below the threshold will have its label -cleared and the special "originSeries" attribute set to the original series. -You may need to check for this in hover events. - -*/ - -(function ($) { - var options = { - series: { threshold: null } // or { below: number, color: color spec} - }; - - function init(plot) { - function thresholdData(plot, s, datapoints, below, color) { - var ps = datapoints.pointsize, i, x, y, p, prevp, - thresholded = $.extend({}, s); // note: shallow copy - - thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; - thresholded.label = null; - thresholded.color = color; - thresholded.threshold = null; - thresholded.originSeries = s; - thresholded.data = []; - - var origpoints = datapoints.points, - addCrossingPoints = s.lines.show; - - var threspoints = []; - var newpoints = []; - var m; - - for (i = 0; i < origpoints.length; i += ps) { - x = origpoints[i]; - y = origpoints[i + 1]; - - prevp = p; - if (y < below) - p = threspoints; - else - p = newpoints; - - if (addCrossingPoints && prevp != p && x != null - && i > 0 && origpoints[i - ps] != null) { - var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); - prevp.push(interx); - prevp.push(below); - for (m = 2; m < ps; ++m) - prevp.push(origpoints[i + m]); - - p.push(null); // start new segment - p.push(null); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - p.push(interx); - p.push(below); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - p.push(x); - p.push(y); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - datapoints.points = newpoints; - thresholded.datapoints.points = threspoints; - - if (thresholded.datapoints.points.length > 0) { - var origIndex = $.inArray(s, plot.getData()); - // Insert newly-generated series right after original one (to prevent it from becoming top-most) - plot.getData().splice(origIndex + 1, 0, thresholded); - } - - // FIXME: there are probably some edge cases left in bars - } - - function processThresholds(plot, s, datapoints) { - if (!s.threshold) - return; - - if (s.threshold instanceof Array) { - s.threshold.sort(function(a, b) { - return a.below - b.below; - }); - - $(s.threshold).each(function(i, th) { - thresholdData(plot, s, datapoints, th.below, th.color); - }); - } - else { - thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); - } - } - - plot.hooks.processDatapoints.push(processThresholds); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'threshold', - version: '1.2' - }); -})(jQuery); diff --git a/src/legacy/ui/public/flot-charts/jquery.flot.time.js b/src/legacy/ui/public/flot-charts/jquery.flot.time.js deleted file mode 100644 index 7612a03302764..0000000000000 --- a/src/legacy/ui/public/flot-charts/jquery.flot.time.js +++ /dev/null @@ -1,473 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = [ - i18n.translate('common.ui.flotCharts.janLabel', { - defaultMessage: 'Jan', - }), i18n.translate('common.ui.flotCharts.febLabel', { - defaultMessage: 'Feb', - }), i18n.translate('common.ui.flotCharts.marLabel', { - defaultMessage: 'Mar', - }), i18n.translate('common.ui.flotCharts.aprLabel', { - defaultMessage: 'Apr', - }), i18n.translate('common.ui.flotCharts.mayLabel', { - defaultMessage: 'May', - }), i18n.translate('common.ui.flotCharts.junLabel', { - defaultMessage: 'Jun', - }), i18n.translate('common.ui.flotCharts.julLabel', { - defaultMessage: 'Jul', - }), i18n.translate('common.ui.flotCharts.augLabel', { - defaultMessage: 'Aug', - }), i18n.translate('common.ui.flotCharts.sepLabel', { - defaultMessage: 'Sep', - }), i18n.translate('common.ui.flotCharts.octLabel', { - defaultMessage: 'Oct', - }), i18n.translate('common.ui.flotCharts.novLabel', { - defaultMessage: 'Nov', - }), i18n.translate('common.ui.flotCharts.decLabel', { - defaultMessage: 'Dec', - })]; - } - - if (dayNames == null) { - dayNames = [i18n.translate('common.ui.flotCharts.sunLabel', { - defaultMessage: 'Sun', - }), i18n.translate('common.ui.flotCharts.monLabel', { - defaultMessage: 'Mon', - }), i18n.translate('common.ui.flotCharts.tueLabel', { - defaultMessage: 'Tue', - }), i18n.translate('common.ui.flotCharts.wedLabel', { - defaultMessage: 'Wed', - }), i18n.translate('common.ui.flotCharts.thuLabel', { - defaultMessage: 'Thu', - }), i18n.translate('common.ui.flotCharts.friLabel', { - defaultMessage: 'Fri', - }), i18n.translate('common.ui.flotCharts.satLabel', { - defaultMessage: 'Sat', - })]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap b/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap deleted file mode 100644 index fd6a0a07ba39c..0000000000000 --- a/src/legacy/ui/public/i18n/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ui/i18n renders children and forwards properties 1`] = ` -
- Context: - - Child: some prop:100500 - -
-`; diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx deleted file mode 100644 index be8ab4cf8d696..0000000000000 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ /dev/null @@ -1,61 +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 { render } from 'enzyme'; -import PropTypes from 'prop-types'; -import React from 'react'; - -jest.mock('angular-sanitize', () => {}); -jest.mock('ui/new_platform', () => ({ - npStart: { - core: { - i18n: { Context: ({ children }: any) =>
Context: {children}
}, - }, - }, -})); - -import { wrapInI18nContext } from '.'; - -describe('ui/i18n', () => { - test('renders children and forwards properties', () => { - const mockPropTypes = { - stringProp: PropTypes.string.isRequired, - numberProp: PropTypes.number, - }; - - const WrappedComponent = wrapInI18nContext( - class extends React.PureComponent<{ [P in keyof typeof mockPropTypes]: unknown }> { - public static propTypes = mockPropTypes; - - public render() { - return ( - - Child: {this.props.stringProp}:{this.props.numberProp} - - ); - } - } - ); - - expect(WrappedComponent.propTypes).toBe(mockPropTypes); - expect( - render() - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx deleted file mode 100644 index 290e82a1334b9..0000000000000 --- a/src/legacy/ui/public/i18n/index.tsx +++ /dev/null @@ -1,52 +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 React from 'react'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; - -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -export const I18nContext = npStart.core.i18n.Context; - -export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { - const ContextWrapper: React.FC

= (props) => { - return ( - - - - ); - }; - - // Original propTypes from the wrapped component should be re-exposed - // since it will be used by reactDirective Angular service - // that will rely on propTypes to watch attributes with these names - ContextWrapper.propTypes = ComponentToWrap.propTypes; - - return ContextWrapper; -} - -uiModules - .get('i18n', ['ngSanitize']) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); diff --git a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js b/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js deleted file mode 100644 index df96a58a6e99f..0000000000000 --- a/src/legacy/ui/public/indexed_array/__tests__/indexed_array.js +++ /dev/null @@ -1,208 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import { IndexedArray } from '..'; - -// this is generally a data-structure that IndexedArray is good for managing -const users = [ - { name: 'John', id: 6, username: 'beast', group: 'admins' }, - { name: 'Anon', id: 0, username: 'shhhh', group: 'secret' }, - { name: 'Fern', id: 42, username: 'kitty', group: 'editor' }, - { name: 'Mary', id: 55, username: 'sheep', group: 'editor' }, -]; - -// this is how we used to accomplish this, before IndexedArray -users.byName = _.keyBy(users, 'name'); -users.byUsername = _.keyBy(users, 'username'); -users.byGroup = _.groupBy(users, 'group'); -users.inIdOrder = _.sortBy(users, 'id'); - -// then things started becoming unruly... so IndexedArray! - -describe('IndexedArray', function () { - describe('Basics', function () { - let reg; - - beforeEach(function () { - reg = new IndexedArray(); - }); - - it('Extends Array', function () { - expect(reg).to.be.a(Array); - }); - - it('fails basic lodash check', function () { - expect(Array.isArray(reg)).to.be(false); - }); - - it('clones to an object', function () { - expect(_.isObject(_.clone(reg))).to.be(true); - expect(Array.isArray(_.clone(reg))).to.be(false); - }); - }); - - describe('Indexing', function () { - it('provides the initial set', function () { - const reg = new IndexedArray({ - initialSet: [1, 2, 3], - }); - - expect(reg).to.have.length(3); - - reg.forEach(function (v, i) { - expect(v).to.eql(i + 1); - }); - }); - - it('indexes the initial set', function () { - const reg = new IndexedArray({ - index: ['username'], - initialSet: users, - }); - - expect(reg).to.have.property('byUsername'); - expect(reg.byUsername).to.eql(users.byUsername); - }); - - it('updates indices after values are added', function () { - // split up the user list, and add it in chunks - const firstUser = users.slice(0, 1).pop(); - const otherUsers = users.slice(1); - - // start off with all but the first - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: otherUsers, - }); - - // add the first - reg.push(firstUser); - - // end up with the same structure that is in the users fixture - expect(Object.keys(reg.byGroup).length).to.be(Object.keys(users.byGroup).length); - for (const group of Object.keys(reg.byGroup)) { - expect(reg.byGroup[group].toJSON()).to.eql(users.byGroup[group]); - } - - expect(reg.inIdOrder).to.eql(users.inIdOrder); - }); - - it('updates indices after values are removed', function () { - // start off with all - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users, - }); - - // remove the last - reg.pop(); - - const expectedCount = users.length - 1; - // indexed lists should be updated - expect(reg).to.have.length(expectedCount); - - const sumOfGroups = _.reduce( - reg.byGroup, - function (note, group) { - return note + group.length; - }, - 0 - ); - expect(sumOfGroups).to.eql(expectedCount); - }); - - it('removes items based on a predicate', function () { - const reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users, - }); - - reg.remove({ name: 'John' }); - - expect(_.isEqual(reg.raw, reg.slice(0))).to.be(true); - expect(reg.length).to.be(3); - expect(reg[0].name).to.be('Anon'); - }); - - it('updates indices after values are re-ordered', function () { - const rawUsers = users.slice(0); - - // collect and shuffle the ids available - let ids = []; - _.times(rawUsers.length, function (i) { - ids.push(i); - }); - ids = _.shuffle(ids); - - // move something here - const toI = ids.shift(); - // from here - const fromI = ids.shift(); - // do the move - const move = function (arr) { - arr.splice(toI, 0, arr.splice(fromI, 1)[0]); - }; - - const reg = new IndexedArray({ - index: ['username'], - initialSet: rawUsers, - }); - - const index = reg.byUsername; - - move(reg); - - expect(reg.byUsername).to.eql(index); - expect(reg.byUsername).to.not.be(index); - }); - }); - - describe('Ordering', function () { - it('ordering is case insensitive', function () { - const reg = new IndexedArray({ - index: ['title'], - order: ['title'], - initialSet: [{ title: 'APM' }, { title: 'Advanced Settings' }], - }); - - const ordered = reg.inTitleOrder; - expect(ordered[0].title).to.be('Advanced Settings'); - expect(ordered[1].title).to.be('APM'); - }); - - it('ordering handles numbers', function () { - const reg = new IndexedArray({ - index: ['id'], - order: ['id'], - initialSet: users, - }); - - const ordered = reg.inIdOrder; - expect(ordered[0].id).to.be(0); - expect(ordered[1].id).to.be(6); - expect(ordered[2].id).to.be(42); - expect(ordered[3].id).to.be(55); - }); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/__tests__/inflector.js b/src/legacy/ui/public/indexed_array/__tests__/inflector.js deleted file mode 100644 index 49ac79094e501..0000000000000 --- a/src/legacy/ui/public/indexed_array/__tests__/inflector.js +++ /dev/null @@ -1,63 +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 { inflector } from '../inflector'; -import expect from '@kbn/expect'; - -describe('IndexedArray Inflector', function () { - it('returns a function', function () { - const getter = inflector(); - expect(getter).to.be.a('function'); - }); - - describe('fn', function () { - it('prepends a prefix', function () { - const inflect = inflector('my'); - - expect(inflect('Family')).to.be('myFamily'); - expect(inflect('family')).to.be('myFamily'); - expect(inflect('fAmIlY')).to.be('myFAmIlY'); - }); - - it('adds both a prefix and suffix', function () { - const inflect = inflector('foo', 'Bar'); - - expect(inflect('box')).to.be('fooBoxBar'); - expect(inflect('box.car.MAX')).to.be('fooBoxCarMaxBar'); - expect(inflect('BaZzY')).to.be('fooBaZzYBar'); - }); - - it('ignores prefix if it is already at the end of the inflected string', function () { - const inflect = inflector('foo', 'Bar'); - expect(inflect('fooBox')).to.be('fooBoxBar'); - expect(inflect('FooBox')).to.be('FooBoxBar'); - }); - - it('ignores postfix if it is already at the end of the inflected string', function () { - const inflect = inflector('foo', 'Bar'); - expect(inflect('bar')).to.be('fooBar'); - expect(inflect('showBoxBar')).to.be('fooShowBoxBar'); - }); - - it('works with "name"', function () { - const inflect = inflector('in', 'Order'); - expect(inflect('name')).to.be('inNameOrder'); - }); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts deleted file mode 100644 index fc4ca8469382a..0000000000000 --- a/src/legacy/ui/public/indexed_array/helpers/organize_by.test.ts +++ /dev/null @@ -1,63 +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 { groupBy } from 'lodash'; -import { organizeBy } from './organize_by'; - -describe('organizeBy', () => { - test('it works', () => { - const col = [ - { - name: 'one', - roles: ['user', 'admin', 'owner'], - }, - { - name: 'two', - roles: ['user'], - }, - { - name: 'three', - roles: ['user'], - }, - { - name: 'four', - roles: ['user', 'admin'], - }, - ]; - - const resp = organizeBy(col, 'roles'); - expect(resp).toHaveProperty('user'); - expect(resp.user.length).toBe(4); - - expect(resp).toHaveProperty('admin'); - expect(resp.admin.length).toBe(2); - - expect(resp).toHaveProperty('owner'); - expect(resp.owner.length).toBe(1); - }); - - test('behaves just like groupBy in normal scenarios', () => { - const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }]; - - const orgs = organizeBy(col, 'name'); - const groups = groupBy(col, 'name'); - - expect(orgs).toEqual(groups); - }); -}); diff --git a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts b/src/legacy/ui/public/indexed_array/helpers/organize_by.ts deleted file mode 100644 index e923767c892cd..0000000000000 --- a/src/legacy/ui/public/indexed_array/helpers/organize_by.ts +++ /dev/null @@ -1,61 +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 { each, isFunction } from 'lodash'; - -/** - * Like _.groupBy, but allows specifying multiple groups for a - * single object. - * - * organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]} - * - * _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a') - * // Object {'1,2,3': Array[1], '1,4': Array[1]} - * - * @param {array} collection - the list of values to organize - * @param {Function} callback - either a property name, or a callback. - * @return {object} - */ -export function organizeBy(collection: object[], callback: ((obj: object) => string) | string) { - const buckets: { [key: string]: object[] } = {}; - - function add(key: string, obj: object) { - if (!buckets[key]) { - buckets[key] = []; - } - buckets[key].push(obj); - } - - each(collection, (obj: Record) => { - const keys = isFunction(callback) ? callback(obj) : obj[callback]; - - if (!Array.isArray(keys)) { - add(keys, obj); - return; - } - - let length = keys.length; - while (length-- > 0) { - add(keys[length], obj); - } - }); - - return buckets; -} diff --git a/src/legacy/ui/public/indexed_array/index.d.ts b/src/legacy/ui/public/indexed_array/index.d.ts deleted file mode 100644 index 21c0a818731ac..0000000000000 --- a/src/legacy/ui/public/indexed_array/index.d.ts +++ /dev/null @@ -1,44 +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 { ListIterator } from 'lodash'; - -interface IndexedArrayConfig { - index?: string[]; - group?: string[]; - order?: string[]; - initialSet?: T[]; - immutable?: boolean; -} - -declare class IndexedArray extends Array { - public immutable: boolean; - public raw: T[]; - - // These may not actually be present, as they are dynamically defined - public inOrder: T[]; - public byType: Record; - public byName: Record; - - constructor(config: IndexedArrayConfig); - - public remove(predicate: ListIterator): T[]; - - public toJSON(): T[]; -} diff --git a/src/legacy/ui/public/indexed_array/index.js b/src/legacy/ui/public/indexed_array/index.js deleted file mode 100644 index 6a42961c9e680..0000000000000 --- a/src/legacy/ui/public/indexed_array/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { IndexedArray } from './indexed_array'; diff --git a/src/legacy/ui/public/indexed_array/indexed_array.js b/src/legacy/ui/public/indexed_array/indexed_array.js deleted file mode 100644 index b9a427b8da7ad..0000000000000 --- a/src/legacy/ui/public/indexed_array/indexed_array.js +++ /dev/null @@ -1,235 +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 _ from 'lodash'; -import { inflector } from './inflector'; -import { organizeBy } from './helpers/organize_by'; - -const pathGetter = _(_.get).rearg(1, 0).ary(2); -const inflectIndex = inflector('by'); -const inflectOrder = inflector('in', 'Order'); - -const CLEAR_CACHE = {}; -const OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable']; - -/** - * Generic extension of Array class, which will index (and reindex) the - * objects it contains based on their properties. - * - * @param {Object} config describes the properties of this registry object - * @param {Array} [config.index] a list of props/paths that should be used to index the docs. - * @param {Array} [config.group] a list of keys/paths to group docs by. - * @param {Array} [config.order] a list of keys/paths to order the keys by. - * @param {Array} [config.initialSet] the initial dataset the IndexedArray should contain. - * @param {boolean} [config.immutable] a flag that hints to people reading the implementation that this IndexedArray - * should not be modified - */ - -export class IndexedArray { - static OPT_NAMES = OPT_NAMES; - - constructor(config) { - config = _.pick(config || {}, OPT_NAMES); - - // use defineProperty so that value can't be changed - Object.defineProperty(this, 'raw', { value: [] }); - - this._indexNames = _.union( - this._setupIndex(config.group, inflectIndex, organizeByIndexedArray(config)), - this._setupIndex(config.index, inflectIndex, _.keyBy), - this._setupIndex(config.order, inflectOrder, (raw, pluckValue) => { - return [...raw].sort((itemA, itemB) => { - const a = pluckValue(itemA); - const b = pluckValue(itemB); - if (typeof a === 'number' && typeof b === 'number') { - return a - b; - } - return String(a).toLowerCase().localeCompare(String(b).toLowerCase()); - }); - }) - ); - - if (config.initialSet) { - this.push.apply(this, config.initialSet); - } - - Object.defineProperty(this, 'immutable', { value: !!config.immutable }); - } - - /** - * Remove items from this based on a predicate - * @param {Function|Object|string} predicate - the predicate used to decide what is removed - * @return {array} - the removed data - */ - remove(predicate) { - this._assertMutable('remove'); - const out = _.remove(this, predicate); - _.remove(this.raw, predicate); - this._clearIndices(); - return out; - } - - /** - * provide a hook for the JSON serializer - * @return {array} - a plain, vanilla array with our same data - */ - toJSON() { - return this.raw; - } - - // wrappers for mutable Array methods - copyWithin(...args) { - return this._mutation('copyWithin', args); - } - fill(...args) { - return this._mutation('fill', args); - } - pop(...args) { - return this._mutation('pop', args); - } - push(...args) { - return this._mutation('push', args); - } - reverse(...args) { - return this._mutation('reverse', args); - } - shift(...args) { - return this._mutation('shift', args); - } - sort(...args) { - return this._mutation('sort', args); - } - splice(...args) { - return this._mutation('splice', args); - } - unshift(...args) { - return this._mutation('unshift', args); - } - - /** - * If this instance of IndexedArray is not mutable, throw an error - * @private - * @param {String} methodName - user facing method name, for error message - * @return {undefined} - */ - _assertMutable(methodName) { - if (this.immutable) { - throw new Error(`${methodName}() is not allowed on immutable IndexedArray instances`); - } - } - - /** - * Execute some mutable method from the Array prototype - * on the IndexedArray and this.raw - * - * @private - * @param {string} methodName - * @param {Array} args - * @return {any} - */ - _mutation(methodName, args) { - this._assertMutable(methodName); - super[methodName].apply(this, args); - this._clearIndices(); - return super[methodName].apply(this.raw, args); - } - - /** - * Create indices for a group of object properties. getters and setters are used to - * read and control the indices. - * @private - * @param {string[]} props - the properties that should be used to index docs - * @param {function} inflect - a function that will be called with a property name, and - * creates the public property at which the index will be exposed - * @param {function} op - the function that will be used to create the indices, it is passed - * the raw representation of the registry, and a getter for reading the - * right prop - * - * @returns {string[]} - the public keys of all indices created - */ - _setupIndex(props, inflect, op) { - // shortcut for empty props - if (!props || props.length === 0) return; - - return props.map((prop) => { - const indexName = inflect(prop); - const getIndexValueFromItem = pathGetter.partial(prop).value(); - let cache; - - Object.defineProperty(this, indexName, { - enumerable: false, - configurable: false, - - set: (val) => { - // can't set any value other than the CLEAR_CACHE constant - if (val === CLEAR_CACHE) { - cache = false; - } else { - throw new TypeError(indexName + ' can not be set, it is a computed index of values'); - } - }, - get: () => { - if (!cache) { - cache = op(this.raw, getIndexValueFromItem); - } - - return cache; - }, - }); - - return indexName; - }); - } - - /** - * Clear cached index/group/order caches so they will be recreated - * on next access - * @private - * @return {undefined} - */ - _clearIndices() { - this._indexNames.forEach((name) => { - this[name] = CLEAR_CACHE; - }); - } -} - -// using traditional `extends Array` syntax doesn't work with babel -// See https://babeljs.io/docs/usage/caveats/ -Object.setPrototypeOf(IndexedArray.prototype, Array.prototype); - -// Similar to `organizeBy` but returns IndexedArrays instead of normal Arrays. -function organizeByIndexedArray(config) { - return (...args) => { - const grouped = organizeBy(...args); - - return _.reduce( - grouped, - (acc, value, group) => { - acc[group] = new IndexedArray({ - ...config, - initialSet: value, - }); - - return acc; - }, - {} - ); - }; -} diff --git a/src/legacy/ui/public/indexed_array/inflector.js b/src/legacy/ui/public/indexed_array/inflector.js deleted file mode 100644 index e034146f5f62f..0000000000000 --- a/src/legacy/ui/public/indexed_array/inflector.js +++ /dev/null @@ -1,60 +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. - */ - -function upFirst(str, total) { - return str.charAt(0).toUpperCase() + (total ? str.substr(1).toLowerCase() : str.substr(1)); -} - -function startsWith(str, test) { - return str.substr(0, test.length).toLowerCase() === test.toLowerCase(); -} - -function endsWith(str, test) { - const tooShort = str.length < test.length; - if (tooShort) return; - - return str.substr(str.length - test.length).toLowerCase() === test.toLowerCase(); -} - -export function inflector(prefix, postfix) { - return function inflect(key) { - let inflected; - - if (key.indexOf('.') !== -1) { - inflected = key - .split('.') - .map(function (step, i) { - return i === 0 ? step : upFirst(step, true); - }) - .join(''); - } else { - inflected = key; - } - - if (prefix && !startsWith(key, prefix)) { - inflected = prefix + upFirst(inflected); - } - - if (postfix && !endsWith(key, postfix)) { - inflected = inflected + postfix; - } - - return inflected; - }; -} diff --git a/src/legacy/ui/public/kfetch/__mocks__/index.ts b/src/legacy/ui/public/kfetch/__mocks__/index.ts deleted file mode 100644 index 1a128e2b85260..0000000000000 --- a/src/legacy/ui/public/kfetch/__mocks__/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const kfetch = () => Promise.resolve(); diff --git a/src/legacy/ui/public/kfetch/_import_objects.ndjson b/src/legacy/ui/public/kfetch/_import_objects.ndjson deleted file mode 100644 index 3511fb44cdfb2..0000000000000 --- a/src/legacy/ui/public/kfetch/_import_objects.ndjson +++ /dev/null @@ -1 +0,0 @@ -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"filter\":[],\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"},"title":"Log Agents","uiStateJSON":"{}","visState":"{\"title\":\"Log Agents\",\"type\":\"area\",\"params\":{\"type\":\"area\",\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"type\":\"category\",\"position\":\"bottom\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\"},\"labels\":{\"show\":true,\"truncate\":100},\"title\":{\"text\":\"agent.raw: Descending\"}}],\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"name\":\"LeftAxis-1\",\"type\":\"value\",\"position\":\"left\",\"show\":true,\"style\":{},\"scale\":{\"type\":\"linear\",\"mode\":\"normal\"},\"labels\":{\"show\":true,\"rotate\":0,\"filter\":false,\"truncate\":100},\"title\":{\"text\":\"Count\"}}],\"seriesParams\":[{\"show\":\"true\",\"type\":\"area\",\"mode\":\"stacked\",\"data\":{\"label\":\"Count\",\"id\":\"1\"},\"drawLinesBetweenPoints\":true,\"showCircles\":true,\"interpolate\":\"linear\",\"valueAxis\":\"ValueAxis-1\"}],\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"times\":[],\"addTimeMarker\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"agent.raw\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\"}}]}"},"id":"082f1d60-a2e7-11e7-bb30-233be9be6a15","migrationVersion":{"visualization":"7.0.0"},"references":[{"id":"f1e4c910-a2e6-11e7-bb30-233be9be6a15","name":"kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"}],"type":"visualization","version":1} diff --git a/src/legacy/ui/public/kfetch/index.ts b/src/legacy/ui/public/kfetch/index.ts deleted file mode 100644 index 105df171ad370..0000000000000 --- a/src/legacy/ui/public/kfetch/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { npSetup } from 'ui/new_platform'; -import { createKfetch, KFetchKibanaOptions, KFetchOptions } from './kfetch'; -export { addInterceptor, KFetchOptions, KFetchQuery } from './kfetch'; - -const kfetchInstance = createKfetch(npSetup.core.http); - -export const kfetch = (options: KFetchOptions, kfetchOptions?: KFetchKibanaOptions) => { - return kfetchInstance(options, kfetchOptions); -}; diff --git a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts b/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts deleted file mode 100644 index ea066b3623f13..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.test.mocks.ts +++ /dev/null @@ -1,26 +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 { setup } from '../../../../test_utils/public/http_test_setup'; - -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: setup(), - }, -})); diff --git a/src/legacy/ui/public/kfetch/kfetch.test.ts b/src/legacy/ui/public/kfetch/kfetch.test.ts deleted file mode 100644 index c45a142d54e9b..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.test.ts +++ /dev/null @@ -1,487 +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. - */ - -// @ts-ignore -import fetchMock from 'fetch-mock/es5/client'; -import './kfetch.test.mocks'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { addInterceptor, kfetch, KFetchOptions } from '.'; -import { Interceptor, resetInterceptors, withDefaultOptions } from './kfetch'; -import { KFetchError } from './kfetch_error'; - -describe('kfetch', () => { - afterEach(() => { - fetchMock.restore(); - resetInterceptors(); - }); - - it('should use supplied request method', async () => { - fetchMock.post('*', {}); - await kfetch({ pathname: '/my/path', method: 'POST' }); - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should use supplied Content-Type', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path', headers: { 'Content-Type': 'CustomContentType' } }); - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - }); - - it('should use supplied pathname and querystring', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path', query: { a: 'b' } }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); - }); - - it('should use supplied headers', async () => { - fetchMock.get('*', {}); - await kfetch({ - pathname: '/my/path', - headers: { myHeader: 'foo' }, - }); - - expect(fetchMock.lastOptions()!.headers).toEqual({ - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - myheader: 'foo', - }); - }); - - it('should return response', async () => { - fetchMock.get('*', { foo: 'bar' }); - const res = await kfetch({ pathname: '/my/path' }); - expect(res).toEqual({ foo: 'bar' }); - }); - - it('should prepend url with basepath by default', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should not prepend url with basepath when disabled', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }, { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); - }); - - it('should make request with defaults', async () => { - fetchMock.get('*', {}); - await kfetch({ pathname: '/my/path' }); - - expect(fetchMock.lastCall()!.request.credentials).toBe('same-origin'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - headers: { - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - }, - }); - }); - - it('should make requests for NDJSON content', async () => { - const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); - - fetchMock.post('*', { - body: content, - headers: { 'Content-Type': 'application/ndjson' }, - }); - - const data = await kfetch({ - method: 'POST', - pathname: '/my/path', - body: content, - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - expect(data).toBeInstanceOf(Blob); - - const ndjson = await new Response(data).text(); - - expect(ndjson).toEqual(content); - }); - - it('should reject on network error', async () => { - expect.assertions(1); - fetchMock.get('*', { status: 500 }); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - expect(e.message).toBe('Internal Server Error'); - } - }); - - describe('when throwing response error (KFetchError)', () => { - let error: KFetchError; - beforeEach(async () => { - fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should contain error message', () => { - expect(error.message).toBe('Not Found'); - }); - - it('should return response body', () => { - expect(error.body).toEqual({ foo: 'bar' }); - }); - - it('should contain response properties', () => { - expect(error.res.status).toBe(404); - expect(error.res.url).toBe('http://localhost/myBase/my/path'); - }); - }); - - describe('when all interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([{}, {}, {}]); - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'Request #1', - 'Response #1', - 'Response #2', - 'Response #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should return response', () => { - expect(resp).toEqual({ foo: 'bar' }); - }); - }); - - describe('when a request interceptor throws; and the next requestError interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - { requestError: () => ({ pathname: '/my/path' } as KFetchOptions) }, - { request: () => Promise.reject(new Error('Error in request')) }, - {}, - ]); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'Response #1', - 'Response #2', - 'Response #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should return response', () => { - expect(resp).toEqual({ foo: 'bar' }); - }); - }); - - describe('when a request interceptor throws', () => { - let error: Error; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - {}, - { request: () => Promise.reject(new Error('Error in request')) }, - {}, - ]); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should call interceptors in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'ResponseError #1', - 'ResponseError #2', - 'ResponseError #3', - ]); - }); - - it('should not make request', () => { - expect(fetchMock.called()).toBe(false); - }); - - it('should throw error', () => { - expect(error.message).toEqual('Error in request'); - }); - }); - - describe('when a response interceptor throws', () => { - let error: Error; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - { response: () => Promise.reject(new Error('Error in response')) }, - {}, - {}, - ]); - - try { - await kfetch({ pathname: '/my/path' }); - } catch (e) { - error = e; - } - }); - - it('should call in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'Request #1', - 'Response #1', - 'ResponseError #2', - 'ResponseError #3', - ]); - }); - - it('should make request', () => { - expect(fetchMock.called()).toBe(true); - }); - - it('should throw error', () => { - expect(error.message).toEqual('Error in response'); - }); - }); - - describe('when request interceptor throws; and a responseError interceptor resolves', () => { - let resp: any; - let interceptorCalls: string[]; - - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - - interceptorCalls = mockInterceptorCalls([ - {}, - { - request: () => { - throw new Error('My request error'); - }, - responseError: () => { - return { custom: 'response' }; - }, - }, - {}, - ]); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should call in correct order', () => { - expect(interceptorCalls).toEqual([ - 'Request #3', - 'Request #2', - 'RequestError #1', - 'ResponseError #1', - 'ResponseError #2', - 'Response #3', - ]); - }); - - it('should not make request', () => { - expect(fetchMock.called()).toBe(false); - }); - - it('should resolve', () => { - expect(resp).toEqual({ custom: 'response' }); - }); - }); - - describe('when interceptors return synchronously', () => { - let resp: any; - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - addInterceptor({ - request: (config) => ({ - ...config, - pathname: '/my/intercepted-route', - }), - response: (res) => ({ - ...res, - addedByResponseInterceptor: true, - }), - }); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should modify request', () => { - expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - }); - }); - - it('should modify response', () => { - expect(resp).toEqual({ - addedByResponseInterceptor: true, - foo: 'bar', - }); - }); - }); - - describe('when interceptors return promise', () => { - let resp: any; - beforeEach(async () => { - fetchMock.get('*', { foo: 'bar' }); - addInterceptor({ - request: (config) => - Promise.resolve({ - ...config, - pathname: '/my/intercepted-route', - }), - response: (res) => - Promise.resolve({ - ...res, - addedByResponseInterceptor: true, - }), - }); - - resp = await kfetch({ pathname: '/my/path' }); - }); - - it('should modify request', () => { - expect(fetchMock.lastUrl()).toContain('/my/intercepted-route'); - expect(fetchMock.lastOptions()!).toMatchObject({ - method: 'GET', - }); - }); - - it('should modify response', () => { - expect(resp).toEqual({ - addedByResponseInterceptor: true, - foo: 'bar', - }); - }); - }); -}); - -function mockInterceptorCalls(interceptors: Interceptor[]) { - const interceptorCalls: string[] = []; - interceptors.forEach((interceptor, i) => { - addInterceptor({ - request: (config) => { - interceptorCalls.push(`Request #${i + 1}`); - - if (interceptor.request) { - return interceptor.request(config); - } - - return config; - }, - requestError: (e) => { - interceptorCalls.push(`RequestError #${i + 1}`); - if (interceptor.requestError) { - return interceptor.requestError(e); - } - - throw e; - }, - response: (res) => { - interceptorCalls.push(`Response #${i + 1}`); - - if (interceptor.response) { - return interceptor.response(res); - } - - return res; - }, - responseError: (e) => { - interceptorCalls.push(`ResponseError #${i + 1}`); - - if (interceptor.responseError) { - return interceptor.responseError(e); - } - - throw e; - }, - }); - }); - - return interceptorCalls; -} - -describe('withDefaultOptions', () => { - it('should remove undefined query params', () => { - const { query } = withDefaultOptions({ - pathname: '/withDefaultOptions', - query: { - foo: 'bar', - param1: (undefined as any) as string, - param2: (null as any) as string, - param3: '', - }, - }); - expect(query).toEqual({ foo: 'bar', param2: null, param3: '' }); - }); - - it('should add default options', () => { - expect(withDefaultOptions({ pathname: '/addDefaultOptions' })).toEqual({ - pathname: '/addDefaultOptions', - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - method: 'GET', - }); - }); -}); diff --git a/src/legacy/ui/public/kfetch/kfetch.ts b/src/legacy/ui/public/kfetch/kfetch.ts deleted file mode 100644 index 4eb7149931575..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch.ts +++ /dev/null @@ -1,110 +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 { merge } from 'lodash'; -// @ts-ignore not really worth typing -import { KFetchError } from './kfetch_error'; - -import { HttpSetup } from '../../../../core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { HttpRequestInit } from '../../../../core/public/http/types'; - -export interface KFetchQuery { - [key: string]: string | number | boolean | undefined; -} - -export interface KFetchOptions extends HttpRequestInit { - pathname: string; - query?: KFetchQuery; - asSystemRequest?: boolean; -} - -export interface KFetchKibanaOptions { - prependBasePath?: boolean; -} - -export interface Interceptor { - request?: (config: KFetchOptions) => Promise | KFetchOptions; - requestError?: (e: any) => Promise | KFetchOptions; - response?: (res: any) => any; - responseError?: (e: any) => any; -} - -const interceptors: Interceptor[] = []; -export const resetInterceptors = () => (interceptors.length = 0); -export const addInterceptor = (interceptor: Interceptor) => interceptors.push(interceptor); - -export function createKfetch(http: HttpSetup) { - return function kfetch( - options: KFetchOptions, - { prependBasePath = true }: KFetchKibanaOptions = {} - ) { - return responseInterceptors( - requestInterceptors(withDefaultOptions(options)) - .then(({ pathname, ...restOptions }) => - http.fetch(pathname, { ...restOptions, prependBasePath }) - ) - .catch((err) => { - throw new KFetchError(err.response || { statusText: err.message }, err.body); - }) - ); - }; -} - -// Request/response interceptors are called in opposite orders. -// Request hooks start from the newest interceptor and end with the oldest. -function requestInterceptors(config: KFetchOptions): Promise { - return interceptors.reduceRight((acc, interceptor) => { - return acc.then(interceptor.request, interceptor.requestError); - }, Promise.resolve(config)); -} - -// Response hooks start from the oldest interceptor and end with the newest. -function responseInterceptors(responsePromise: Promise) { - return interceptors.reduce((acc, interceptor) => { - return acc.then(interceptor.response, interceptor.responseError); - }, responsePromise); -} - -export function withDefaultOptions(options?: KFetchOptions): KFetchOptions { - const withDefaults = merge( - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - }, - }, - options - ) as KFetchOptions; - - if ( - options && - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - // TS thinks headers could be undefined here, but that isn't possible because - // of the merge above. - // @ts-ignore - withDefaults.headers['Content-Type'] = undefined; - } - - return withDefaults; -} diff --git a/src/legacy/ui/public/kfetch/kfetch_error.ts b/src/legacy/ui/public/kfetch/kfetch_error.ts deleted file mode 100644 index f351959e624b8..0000000000000 --- a/src/legacy/ui/public/kfetch/kfetch_error.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export class KFetchError extends Error { - constructor(public readonly res: Response, public readonly body?: any) { - super(res.statusText); - - // captureStackTrace is only available in the V8 engine, so any browser using - // a different JS engine won't have access to this method. - if (Error.captureStackTrace) { - Error.captureStackTrace(this, KFetchError); - } - } -} diff --git a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js deleted file mode 100644 index efcfb77997265..0000000000000 --- a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js +++ /dev/null @@ -1,149 +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 $ from 'jquery'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import ngMock from 'ng_mock'; - -import { $setupXsrfRequestInterceptor } from '../../../../../plugins/kibana_legacy/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { version } from '../../../../../core/server/utils/package_json'; - -const xsrfHeader = 'kbn-version'; - -describe('chrome xsrf apis', function () { - const sandbox = sinon.createSandbox(); - - afterEach(function () { - sandbox.restore(); - }); - - describe('jQuery support', function () { - it('adds a global jQuery prefilter', function () { - sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(version); - expect($.ajaxPrefilter.callCount).to.be(1); - }); - - describe('jQuery prefilter', function () { - let prefilter; - - beforeEach(function () { - sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(version); - prefilter = $.ajaxPrefilter.args[0][0]; - }); - - it(`sets the ${xsrfHeader} header`, function () { - const setHeader = sinon.stub(); - prefilter({}, {}, { setRequestHeader: setHeader }); - - expect(setHeader.callCount).to.be(1); - expect(setHeader.args[0]).to.eql([xsrfHeader, version]); - }); - - it('can be canceled by setting the kbnXsrfToken option', function () { - const setHeader = sinon.stub(); - prefilter({ kbnXsrfToken: false }, {}, { setRequestHeader: setHeader }); - expect(setHeader.callCount).to.be(0); - }); - }); - - describe('Angular support', function () { - let $http; - let $httpBackend; - - beforeEach(function () { - sandbox.stub($, 'ajaxPrefilter'); - ngMock.module($setupXsrfRequestInterceptor(version)); - }); - - beforeEach( - ngMock.inject(function ($injector) { - $http = $injector.get('$http'); - $httpBackend = $injector.get('$httpBackend'); - - $httpBackend.when('POST', '/api/test').respond('ok'); - }) - ); - - afterEach(function () { - $httpBackend.verifyNoOutstandingExpectation(); - $httpBackend.verifyNoOutstandingRequest(); - }); - - it(`injects a ${xsrfHeader} header on every request`, function () { - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === version; - }) - .respond(200, ''); - - $http.post('/api/test'); - $httpBackend.flush(); - }); - - it('skips requests with the kbnXsrfToken set falsy', function () { - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return !(xsrfHeader in headers); - }) - .respond(200, ''); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: 0, - }); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: '', - }); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: false, - }); - - $httpBackend.flush(); - }); - - it('treats the kbnXsrfToken option as boolean-y', function () { - const customToken = `custom:${version}`; - $httpBackend - .expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === version; - }) - .respond(200, ''); - - $http({ - method: 'POST', - url: '/api/test', - kbnXsrfToken: customToken, - }); - - $httpBackend.flush(); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/legacy_compat/index.ts b/src/legacy/ui/public/legacy_compat/index.ts deleted file mode 100644 index 2067fa6489304..0000000000000 --- a/src/legacy/ui/public/legacy_compat/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { configureAppAngularModule } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/metadata.ts b/src/legacy/ui/public/metadata.ts deleted file mode 100644 index fade0f0d8629a..0000000000000 --- a/src/legacy/ui/public/metadata.ts +++ /dev/null @@ -1,25 +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 { npSetup } from 'ui/new_platform'; - -export const metadata: { - branch: string; - version: string; -} = npSetup.core.injectedMetadata.getLegacyMetadata(); diff --git a/src/legacy/ui/public/modules.js b/src/legacy/ui/public/modules.js deleted file mode 100644 index bb1c8aead1c34..0000000000000 --- a/src/legacy/ui/public/modules.js +++ /dev/null @@ -1,142 +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 angular from 'angular'; -import _ from 'lodash'; -/** - * This module is used by Kibana to create and reuse angular modules. Angular modules - * can only be created once and need to have their dependencies at creation. This is - * hard/impossible to do in require.js since all of the dependencies for a module are - * loaded before it is. - * - * Here is an example: - * - * In the scenario below, require.js would load directive.js first because it is a - * dependency of app.js. This would cause the call to `angular.module('app')` to - * execute before the module is actually created. This causes angular to throw an - * error. This effect is magnified when app.js links off to many different modules. - * - * This is normally solved by creating unique modules per file, listed as the 1st - * alternate solution below. Unfortunately this solution would have required that - * we replicate our require statements. - * - * app.js - * ``` - * angular.module('app', ['ui.bootstrap']) - * .controller('AppController', function () { ... }); - * - * require('./directive'); - * ``` - * - * directive.js - * ``` - * angular.module('app') - * .directive('someDirective', function () { ... }); - * ``` - * - * Before taking this approach we saw three possible solutions: - * 1. replicate our js modules in angular modules/use a different module per file - * 2. create a single module outside of our js modules and share it - * 3. use a helper lib to dynamically create modules as needed. - * - * We decided to go with #3 - * - * This ends up working by creating a list of modules that the code base creates by - * calling `modules.get(name)` with different names, and then before bootstrapping - * the application kibana uses `modules.link()` to set the dependencies of the "kibana" - * module to include every defined module. This guarantees that kibana can always find - * any angular dependency defined in the kibana code base. This **also** means that - * Private modules are able to find any dependency, since they are injected using the - * "kibana" module's injector. - * - */ -const existingModules = {}; -const links = []; - -/** - * Take an angular module and extends the dependencies for that module to include all of the modules - * created using `ui/modules` - * - * @param {AngularModule} module - the module to extend - * @return {undefined} - */ -export function link(module) { - // as modules are defined they will be set as requirements for this app - links.push(module); - - // merge in the existing modules - module.requires = _.union(module.requires, _.keys(existingModules)); -} - -/** - * The primary means of interacting with `ui/modules`. Returns an angular module. If the module already - * exists the existing version will be returned. `dependencies` are either set as or merged into the - * modules total dependencies. - * - * This is in contrast to the `angular.module(name, [dependencies])` function which will only - * create a module if the `dependencies` list is passed and get an existing module if no dependencies - * are passed. This requires knowing the order that your files will load, which we can't guarantee. - * - * @param {string} moduleName - the unique name for this module - * @param {array[string]} [requires=[]] - the other modules this module requires - * @return {AngularModule} - */ -export function get(moduleName, requires) { - let module = existingModules[moduleName]; - - if (module === void 0) { - // create the module - module = existingModules[moduleName] = angular.module(moduleName, []); - - module.close = _.partial(close, moduleName); - - // ensure that it is required by linked modules - _.each(links, function (app) { - if (!~app.requires.indexOf(moduleName)) app.requires.push(moduleName); - }); - } - - if (requires) { - // update requires list with possibly new requirements - module.requires = _.union(module.requires, requires); - } - - return module; -} - -export function close(moduleName) { - const module = existingModules[moduleName]; - - // already closed - if (!module) return; - - // if the module is currently linked, unlink it - const i = links.indexOf(module); - if (i > -1) links.splice(i, 1); - - // remove from linked modules list of required modules - _.each(links, function (app) { - _.pull(app.requires, moduleName); - }); - - // remove module from existingModules - delete existingModules[moduleName]; -} - -export const uiModules = { link, get, close }; diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts deleted file mode 100644 index 35aa8e4830428..0000000000000 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ /dev/null @@ -1,81 +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 { coreMock } from '../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { embeddablePluginMock } from '../../../../../plugins/embeddable/public/mocks'; -import { navigationPluginMock } from '../../../../../plugins/navigation/public/mocks'; -import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; -import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks'; -import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; -import { managementPluginMock } from '../../../../../plugins/management/public/mocks'; -import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; -import { kibanaLegacyPluginMock } from '../../../../../plugins/kibana_legacy/public/mocks'; -import { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; -import { advancedSettingsMock } from '../../../../../plugins/advanced_settings/public/mocks'; -import { savedObjectsManagementPluginMock } from '../../../../../plugins/saved_objects_management/public/mocks'; -import { visualizationsPluginMock } from '../../../../../plugins/visualizations/public/mocks'; -import { discoverPluginMock } from '../../../../../plugins/discover/public/mocks'; - -export const pluginsMock = { - createSetup: () => ({ - data: dataPluginMock.createSetupContract(), - charts: chartPluginMock.createSetupContract(), - navigation: navigationPluginMock.createSetupContract(), - embeddable: embeddablePluginMock.createSetupContract(), - inspector: inspectorPluginMock.createSetupContract(), - expressions: expressionsPluginMock.createSetupContract(), - uiActions: uiActionsPluginMock.createSetupContract(), - usageCollection: usageCollectionPluginMock.createSetupContract(), - advancedSettings: advancedSettingsMock.createSetupContract(), - visualizations: visualizationsPluginMock.createSetupContract(), - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), - savedObjectsManagement: savedObjectsManagementPluginMock.createSetupContract(), - discover: discoverPluginMock.createSetupContract(), - }), - createStart: () => ({ - data: dataPluginMock.createStartContract(), - charts: chartPluginMock.createStartContract(), - navigation: navigationPluginMock.createStartContract(), - embeddable: embeddablePluginMock.createStartContract(), - inspector: inspectorPluginMock.createStartContract(), - expressions: expressionsPluginMock.createStartContract(), - uiActions: uiActionsPluginMock.createStartContract(), - management: managementPluginMock.createStartContract(), - advancedSettings: advancedSettingsMock.createStartContract(), - visualizations: visualizationsPluginMock.createStartContract(), - kibanaLegacy: kibanaLegacyPluginMock.createStartContract(), - savedObjectsManagement: savedObjectsManagementPluginMock.createStartContract(), - discover: discoverPluginMock.createStartContract(), - }), -}; - -export const createUiNewPlatformMock = () => { - const mock = { - npSetup: { - core: coreMock.createSetup(), - plugins: pluginsMock.createSetup(), - }, - npStart: { - core: coreMock.createStart(), - plugins: pluginsMock.createStart(), - }, - }; - return mock; -}; diff --git a/src/legacy/ui/public/new_platform/__mocks__/index.ts b/src/legacy/ui/public/new_platform/__mocks__/index.ts deleted file mode 100644 index d469960ddd7b7..0000000000000 --- a/src/legacy/ui/public/new_platform/__mocks__/index.ts +++ /dev/null @@ -1,23 +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 { createUiNewPlatformMock } from './helpers'; - -const { npSetup, npStart } = createUiNewPlatformMock(); -export { npSetup, npStart }; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js deleted file mode 100644 index b8d48b784dba7..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ /dev/null @@ -1,550 +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 sinon from 'sinon'; -import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { setSetupServices, setStartServices } from './set_services'; -import { - AggTypesRegistry, - getAggTypes, - AggConfigs, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../src/plugins/data/common/search/aggs'; -import { ComponentRegistry } from '../../../../../src/plugins/advanced_settings/public/'; -import { UI_SETTINGS } from '../../../../../src/plugins/data/public/'; -import { - CSV_SEPARATOR_SETTING, - CSV_QUOTE_VALUES_SETTING, -} from '../../../../../src/plugins/share/public'; - -const mockObservable = () => { - return { - subscribe: () => {}, - pipe: () => { - return { - subscribe: () => {}, - }; - }, - }; -}; - -const mockComponent = () => { - return null; -}; - -let refreshInterval = undefined; -let isTimeRangeSelectorEnabled = true; -let isAutoRefreshSelectorEnabled = true; - -export const mockUiSettings = { - get: (item, defaultValue) => { - const defaultValues = { - dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - 'dateFormat:tz': 'UTC', - [UI_SETTINGS.SHORT_DOTS_ENABLE]: true, - [UI_SETTINGS.COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX]: true, - [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: true, - [UI_SETTINGS.QUERY_STRING_OPTIONS]: {}, - [UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN]: '0,0.[000]', - [UI_SETTINGS.FORMAT_PERCENT_DEFAULT_PATTERN]: '0,0.[000]%', - [UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE]: 'en', - [UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP]: {}, - [CSV_SEPARATOR_SETTING]: ',', - [CSV_QUOTE_VALUES_SETTING]: true, - [UI_SETTINGS.SEARCH_QUERY_LANGUAGE]: 'kuery', - 'state:storeInSessionStorage': false, - }; - - return defaultValues[item] || defaultValue; - }, - getUpdate$: () => ({ - subscribe: sinon.fake(), - }), - isDefault: sinon.fake(), -}; - -const mockCoreSetup = { - chrome: {}, - http: { - basePath: { - get: sinon.fake.returns(''), - }, - }, - injectedMetadata: {}, - uiSettings: mockUiSettings, -}; - -const mockCoreStart = { - application: { - capabilities: {}, - }, - chrome: { - overlays: { - openModal: sinon.fake(), - }, - }, - http: { - basePath: { - get: sinon.fake.returns(''), - }, - }, - notifications: { - toasts: {}, - }, - i18n: {}, - overlays: {}, - savedObjects: { - client: {}, - }, - uiSettings: mockUiSettings, -}; - -const querySetup = { - state$: mockObservable(), - filterManager: { - getFetches$: sinon.fake(), - getFilters: sinon.fake(), - getAppFilters: sinon.fake(), - getGlobalFilters: sinon.fake(), - removeFilter: sinon.fake(), - addFilters: sinon.fake(), - setFilters: sinon.fake(), - removeAll: sinon.fake(), - getUpdates$: mockObservable, - }, - timefilter: { - timefilter: { - getFetch$: mockObservable, - getAutoRefreshFetch$: mockObservable, - getEnabledUpdated$: mockObservable, - getTimeUpdate$: mockObservable, - getRefreshIntervalUpdate$: mockObservable, - isTimeRangeSelectorEnabled: () => { - return isTimeRangeSelectorEnabled; - }, - isAutoRefreshSelectorEnabled: () => { - return isAutoRefreshSelectorEnabled; - }, - disableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = false; - }, - enableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = true; - }, - getRefreshInterval: () => { - return refreshInterval; - }, - setRefreshInterval: (interval) => { - refreshInterval = interval; - }, - enableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = true; - }, - disableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = false; - }, - getTime: sinon.fake(), - setTime: sinon.fake(), - getActiveBounds: sinon.fake(), - getBounds: sinon.fake(), - calculateBounds: sinon.fake(), - createFilter: sinon.fake(), - }, - history: sinon.fake(), - }, - savedQueries: { - saveQuery: sinon.fake(), - getAllSavedQueries: sinon.fake(), - findSavedQueries: sinon.fake(), - getSavedQuery: sinon.fake(), - deleteSavedQuery: sinon.fake(), - getSavedQueryCount: sinon.fake(), - }, -}; - -const mockAggTypesRegistry = () => { - const registry = new AggTypesRegistry(); - const registrySetup = registry.setup(); - const aggTypes = getAggTypes({ - calculateBounds: sinon.fake(), - getConfig: sinon.fake(), - getFieldFormatsStart: () => ({ - deserialize: sinon.fake(), - getDefaultInstance: sinon.fake(), - }), - isDefaultTimezone: () => true, - }); - aggTypes.buckets.forEach((type) => registrySetup.registerBucket(type)); - aggTypes.metrics.forEach((type) => registrySetup.registerMetric(type)); - - return registry; -}; - -const aggTypesRegistry = mockAggTypesRegistry(); - -export const npSetup = { - core: mockCoreSetup, - plugins: { - advancedSettings: { - component: { - register: sinon.fake(), - componentType: ComponentRegistry.componentType, - }, - }, - usageCollection: { - allowTrackUserAgent: sinon.fake(), - reportUiStats: sinon.fake(), - METRIC_TYPE, - }, - embeddable: { - registerEmbeddableFactory: sinon.fake(), - }, - expressions: { - registerFunction: sinon.fake(), - registerRenderer: sinon.fake(), - registerType: sinon.fake(), - }, - data: { - autocomplete: { - addProvider: sinon.fake(), - getProvider: sinon.fake(), - }, - query: querySetup, - search: { - aggs: { - types: aggTypesRegistry.setup(), - }, - __LEGACY: { - esClient: { - search: sinon.fake(), - msearch: sinon.fake(), - }, - }, - }, - fieldFormats: getFieldFormatsRegistry(mockCoreSetup), - }, - share: { - register: () => {}, - urlGenerators: { - registerUrlGenerator: () => {}, - }, - }, - devTools: { - register: () => {}, - }, - kibanaLegacy: { - registerLegacyApp: () => {}, - forwardApp: () => {}, - config: { - defaultAppId: 'home', - }, - }, - inspector: { - registerView: () => undefined, - __LEGACY: { - views: { - register: () => undefined, - }, - }, - }, - uiActions: { - attachAction: sinon.fake(), - registerAction: sinon.fake(), - registerTrigger: sinon.fake(), - }, - home: { - featureCatalogue: { - register: sinon.fake(), - }, - environment: { - update: sinon.fake(), - }, - config: { - disableWelcomeScreen: false, - }, - tutorials: { - setVariable: sinon.fake(), - }, - }, - charts: { - theme: { - chartsTheme$: mockObservable, - useChartsTheme: sinon.fake(), - }, - colors: { - seedColors: ['white', 'black'], - }, - }, - management: { - sections: { - getSection: () => ({ - registerApp: sinon.fake(), - }), - }, - }, - indexPatternManagement: { - list: { addListConfig: sinon.fake() }, - creation: { addCreationConfig: sinon.fake() }, - }, - discover: { - docViews: { - addDocView: sinon.fake(), - setAngularInjectorGetter: sinon.fake(), - }, - }, - visTypeVega: { - config: sinon.fake(), - }, - visualizations: { - createBaseVisualization: sinon.fake(), - createReactVisualization: sinon.fake(), - registerAlias: sinon.fake(), - hideTypes: sinon.fake(), - }, - - mapsLegacy: { - serviceSettings: sinon.fake(), - getPrecision: sinon.fake(), - getZoomPrecision: sinon.fake(), - }, - }, -}; - -export const npStart = { - core: mockCoreStart, - plugins: { - management: { - legacy: { - getSection: () => ({ - register: sinon.fake(), - deregister: sinon.fake(), - hasItem: sinon.fake(), - }), - }, - sections: { - getSection: () => ({ - registerApp: sinon.fake(), - }), - }, - }, - indexPatternManagement: { - list: { - getType: sinon.fake(), - getIndexPatternCreationOptions: sinon.fake(), - }, - creation: { - getIndexPatternTags: sinon.fake(), - getFieldInfo: sinon.fake(), - areScriptedFieldsEnabled: sinon.fake(), - }, - }, - embeddable: { - getEmbeddableFactory: sinon.fake(), - getEmbeddableFactories: sinon.fake(), - registerEmbeddableFactory: sinon.fake(), - }, - expressions: { - registerFunction: sinon.fake(), - registerRenderer: sinon.fake(), - registerType: sinon.fake(), - }, - kibanaLegacy: { - getForwards: () => [], - loadFontAwesome: () => {}, - config: { - defaultAppId: 'home', - }, - dashboardConfig: { - turnHideWriteControlsOn: sinon.fake(), - getHideWriteControls: sinon.fake(), - }, - }, - dashboard: { - getSavedDashboardLoader: sinon.fake(), - }, - data: { - actions: { - createFiltersFromValueClickAction: Promise.resolve(['yes']), - createFiltersFromRangeSelectAction: sinon.fake(), - }, - autocomplete: { - getProvider: sinon.fake(), - }, - getSuggestions: sinon.fake(), - indexPatterns: { - get: sinon.spy((indexPatternId) => - Promise.resolve({ - id: indexPatternId, - isTimeNanosBased: () => false, - popularizeField: () => {}, - }) - ), - }, - ui: { - IndexPatternSelect: mockComponent, - SearchBar: mockComponent, - }, - query: { - filterManager: { - getFetches$: sinon.fake(), - getFilters: sinon.fake(), - getAppFilters: sinon.fake(), - getGlobalFilters: sinon.fake(), - removeFilter: sinon.fake(), - addFilters: sinon.fake(), - setFilters: sinon.fake(), - removeAll: sinon.fake(), - getUpdates$: mockObservable, - }, - timefilter: { - timefilter: { - getFetch$: mockObservable, - getAutoRefreshFetch$: mockObservable, - getEnabledUpdated$: mockObservable, - getTimeUpdate$: mockObservable, - getRefreshIntervalUpdate$: mockObservable, - isTimeRangeSelectorEnabled: () => { - return isTimeRangeSelectorEnabled; - }, - isAutoRefreshSelectorEnabled: () => { - return isAutoRefreshSelectorEnabled; - }, - disableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = false; - }, - enableAutoRefreshSelector: () => { - isAutoRefreshSelectorEnabled = true; - }, - getRefreshInterval: () => { - return refreshInterval; - }, - setRefreshInterval: (interval) => { - refreshInterval = interval; - }, - enableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = true; - }, - disableTimeRangeSelector: () => { - isTimeRangeSelectorEnabled = false; - }, - getTime: sinon.fake(), - setTime: sinon.fake(), - getActiveBounds: sinon.fake(), - getBounds: sinon.fake(), - calculateBounds: sinon.fake(), - createFilter: sinon.fake(), - }, - history: sinon.fake(), - }, - }, - search: { - aggs: { - calculateAutoTimeExpression: sinon.fake(), - createAggConfigs: (indexPattern, configStates = []) => { - return new AggConfigs(indexPattern, configStates, { - typesRegistry: aggTypesRegistry.start(), - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - }); - }, - types: aggTypesRegistry.start(), - }, - __LEGACY: { - esClient: { - search: sinon.fake(), - msearch: sinon.fake(), - }, - }, - }, - fieldFormats: getFieldFormatsRegistry(mockCoreStart), - }, - share: { - toggleShareContextMenu: () => {}, - }, - inspector: { - isAvailable: () => false, - open: () => ({ - onClose: Promise.resolve(undefined), - close: () => Promise.resolve(undefined), - }), - }, - uiActions: { - attachAction: sinon.fake(), - registerAction: sinon.fake(), - registerTrigger: sinon.fake(), - detachAction: sinon.fake(), - executeTriggerActions: sinon.fake(), - getTrigger: sinon.fake(), - getTriggerActions: sinon.fake(), - getTriggerCompatibleActions: sinon.fake(), - }, - visualizations: { - get: sinon.fake(), - all: sinon.fake(), - getAliases: sinon.fake(), - savedVisualizationsLoader: {}, - showNewVisModal: sinon.fake(), - createVis: sinon.fake(), - convertFromSerializedVis: sinon.fake(), - convertToSerializedVis: sinon.fake(), - }, - navigation: { - ui: { - TopNavMenu: mockComponent, - }, - }, - charts: { - theme: { - chartsTheme$: mockObservable, - useChartsTheme: sinon.fake(), - }, - }, - discover: { - docViews: { - DocViewer: () => null, - }, - savedSearchLoader: {}, - }, - }, -}; - -export function __setup__(coreSetup) { - npSetup.core = coreSetup; - - // no-op application register calls (this is overwritten to - // bootstrap an LP plugin outside of tests) - npSetup.core.application.register = () => {}; - - npSetup.core.uiSettings.get = mockUiSettings.get; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setSetupServices(npSetup); -} - -export function __start__(coreStart) { - npStart.core = coreStart; - - npStart.core.uiSettings.get = mockUiSettings.get; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setStartServices(npStart); -} diff --git a/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts b/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts deleted file mode 100644 index f44efe17ef8ee..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.mocks.ts +++ /dev/null @@ -1,31 +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 { scopedHistoryMock } from '../../../../core/public/mocks'; - -export const setRootControllerMock = jest.fn(); - -jest.doMock('ui/chrome', () => ({ - setRootController: setRootControllerMock, -})); - -export const historyMock = scopedHistoryMock.create(); -jest.doMock('../../../../core/public', () => ({ - ScopedHistory: jest.fn(() => historyMock), -})); diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts deleted file mode 100644 index d515c348ca440..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ /dev/null @@ -1,124 +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. - */ - -jest.mock('history'); - -import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; -import { legacyAppRegister, __reset__, __setup__, __start__ } from './new_platform'; -import { coreMock } from '../../../../core/public/mocks'; -import { AppMount } from '../../../../core/public'; - -describe('ui/new_platform', () => { - describe('legacyAppRegister', () => { - beforeEach(() => { - setRootControllerMock.mockReset(); - __reset__(); - __setup__(coreMock.createSetup({ basePath: '/test/base/path' }) as any, {} as any); - }); - - const registerApp = () => { - const unmountMock = jest.fn(); - const mountMock = jest.fn, Parameters>(() => unmountMock); - legacyAppRegister({ - id: 'test', - title: 'Test', - mount: mountMock, - }); - return { mountMock, unmountMock }; - }; - - test('sets ui/chrome root controller', () => { - registerApp(); - expect(setRootControllerMock).toHaveBeenCalledWith('test', expect.any(Function)); - }); - - test('throws if called more than once', () => { - registerApp(); - expect(registerApp).toThrowErrorMatchingInlineSnapshot( - `"core.application.register may only be called once for legacy plugins."` - ); - }); - - test('controller calls app.mount when invoked', () => { - const { mountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - expect(mountMock).toHaveBeenCalledWith({ - element: expect.any(HTMLElement), - appBasePath: '/test/base/path/app/test', - onAppLeave: expect.any(Function), - history: historyMock, - }); - }); - - test('app is mounted in new div inside containing element', () => { - const { mountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - - const { element } = mountMock.mock.calls[0][0]; - expect(element.parentElement).toEqual(elementMock[0]); - }); - - test('controller calls deprecated context app.mount when invoked', () => { - const unmountMock = jest.fn(); - // Two arguments changes how this is called. - const mountMock = jest.fn((context, params) => unmountMock); - legacyAppRegister({ - id: 'test', - title: 'Test', - mount: mountMock, - }); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { - element: expect.any(HTMLElement), - appBasePath: '/test/base/path/app/test', - onAppLeave: expect.any(Function), - history: historyMock, - }); - }); - - test('controller calls unmount when $scope.$destroy', async () => { - const { unmountMock } = registerApp(); - const controller = setRootControllerMock.mock.calls[0][1]; - const scopeMock = { $on: jest.fn() }; - const elementMock = [document.createElement('div')]; - - controller(scopeMock, elementMock); - // Flush promise queue. Must be done this way because the controller cannot return a Promise without breaking - // angular. - await new Promise((resolve) => setTimeout(resolve, 1)); - - const [event, eventHandler] = scopeMock.$on.mock.calls[0]; - expect(event).toEqual('$destroy'); - eventHandler(); - expect(unmountMock).toHaveBeenCalled(); - }); - }); -}); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts deleted file mode 100644 index 37787ffbde4fc..0000000000000 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ /dev/null @@ -1,208 +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 { IScope } from 'angular'; - -import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; -import { createBrowserHistory } from 'history'; -import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; -import { DashboardStart } from '../../../../plugins/dashboard/public'; -import { setSetupServices, setStartServices } from './set_services'; -import { - LegacyCoreSetup, - LegacyCoreStart, - App, - AppMountDeprecated, - ScopedHistory, -} from '../../../../core/public'; -import { Plugin as DataPlugin } from '../../../../plugins/data/public'; -import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; -import { - Setup as InspectorSetup, - Start as InspectorStart, -} from '../../../../plugins/inspector/public'; -import { ChartsPluginSetup, ChartsPluginStart } from '../../../../plugins/charts/public'; -import { DevToolsSetup } from '../../../../plugins/dev_tools/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; -import { HomePublicPluginSetup } from '../../../../plugins/home/public'; -import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; -import { - AdvancedSettingsSetup, - AdvancedSettingsStart, -} from '../../../../plugins/advanced_settings/public'; -import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; -import { - IndexPatternManagementSetup, - IndexPatternManagementStart, -} from '../../../../plugins/index_pattern_management/public'; -import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; -import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; -import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; -import { - NavigationPublicPluginSetup, - NavigationPublicPluginStart, -} from '../../../../plugins/navigation/public'; -import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; -import { - SavedObjectsManagementPluginSetup, - SavedObjectsManagementPluginStart, -} from '../../../../plugins/saved_objects_management/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { VisTypeTimelionPluginStart } from '../../../../plugins/vis_type_timelion/public'; -import { MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; - -export interface PluginsSetup { - bfetch: BfetchPublicSetup; - charts: ChartsPluginSetup; - data: ReturnType; - embeddable: EmbeddableSetup; - expressions: ReturnType; - home: HomePublicPluginSetup; - inspector: InspectorSetup; - uiActions: UiActionsSetup; - navigation: NavigationPublicPluginSetup; - devTools: DevToolsSetup; - kibanaLegacy: KibanaLegacySetup; - share: SharePluginSetup; - usageCollection: UsageCollectionSetup; - advancedSettings: AdvancedSettingsSetup; - management: ManagementSetup; - discover: DiscoverSetup; - visualizations: VisualizationsSetup; - telemetry?: TelemetryPluginSetup; - savedObjectsManagement: SavedObjectsManagementPluginSetup; - mapsLegacy: MapsLegacyPluginSetup; - indexPatternManagement: IndexPatternManagementSetup; - visTypeXy?: VisTypeXyPluginSetup; -} - -export interface PluginsStart { - bfetch: BfetchPublicStart; - charts: ChartsPluginStart; - data: ReturnType; - embeddable: EmbeddableStart; - expressions: ReturnType; - inspector: InspectorStart; - uiActions: UiActionsStart; - navigation: NavigationPublicPluginStart; - kibanaLegacy: KibanaLegacyStart; - share: SharePluginStart; - management: ManagementStart; - advancedSettings: AdvancedSettingsStart; - discover: DiscoverStart; - visualizations: VisualizationsStart; - telemetry?: TelemetryPluginStart; - dashboard: DashboardStart; - savedObjectsManagement: SavedObjectsManagementPluginStart; - visTypeTimelion: VisTypeTimelionPluginStart; - indexPatternManagement: IndexPatternManagementStart; -} - -export const npSetup = { - core: (null as unknown) as LegacyCoreSetup, - plugins: {} as PluginsSetup, -}; - -export const npStart = { - core: (null as unknown) as LegacyCoreStart, - plugins: {} as PluginsStart, -}; - -/** - * Only used by unit tests - * @internal - */ -export function __reset__() { - npSetup.core = (null as unknown) as LegacyCoreSetup; - npSetup.plugins = {} as any; - npStart.core = (null as unknown) as LegacyCoreStart; - npStart.plugins = {} as any; - legacyAppRegistered = false; -} - -export function __setup__(coreSetup: LegacyCoreSetup, plugins: PluginsSetup) { - npSetup.core = coreSetup; - npSetup.plugins = plugins; - - // Setup compatibility layer for AppService in legacy platform - npSetup.core.application.register = legacyAppRegister; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setSetupServices(npSetup); -} - -export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { - npStart.core = coreStart; - npStart.plugins = plugins; - - // Services that need to be set in the legacy platform since the legacy data - // & vis plugins which previously provided them have been removed. - setStartServices(npStart); -} - -/** Flag used to ensure `legacyAppRegister` is only called once. */ -let legacyAppRegistered = false; - -/** - * Exported for testing only. Use `npSetup.core.application.register` in legacy apps. - * @internal - */ -export const legacyAppRegister = (app: App) => { - if (legacyAppRegistered) { - throw new Error(`core.application.register may only be called once for legacy plugins.`); - } - legacyAppRegistered = true; - - // eslint-disable-next-line @typescript-eslint/no-var-requires - require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = document.createElement('div'); - $element[0].appendChild(element); - - // Root controller cannot return a Promise so use an internal async function and call it immediately - (async () => { - const appRoute = app.appRoute || `/app/${app.id}`; - const appBasePath = npSetup.core.http.basePath.prepend(appRoute); - const params = { - element, - appBasePath, - history: new ScopedHistory( - createBrowserHistory({ basename: npSetup.core.http.basePath.get() }), - appRoute - ), - onAppLeave: () => undefined, - }; - const unmount = isAppMountDeprecated(app.mount) - ? await app.mount({ core: npStart.core }, params) - : await app.mount(params); - $scope.$on('$destroy', () => { - unmount(); - }); - })(); - }); -}; - -function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { - // Mount functions with two arguments are assumed to expect deprecated `context` object. - return mount.length === 2; -} diff --git a/src/legacy/ui/public/new_platform/set_services.test.ts b/src/legacy/ui/public/new_platform/set_services.test.ts deleted file mode 100644 index 74e789ec220b1..0000000000000 --- a/src/legacy/ui/public/new_platform/set_services.test.ts +++ /dev/null @@ -1,51 +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 { __reset__, __setup__, __start__, PluginsSetup, PluginsStart } from './new_platform'; -import * as dataServices from '../../../../plugins/data/public/services'; -import * as visualizationsServices from '../../../../plugins/visualizations/public/services'; -import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; -import { coreMock } from '../../../../core/public/mocks'; -import { npSetup, npStart } from './__mocks__'; - -describe('ui/new_platform', () => { - describe('set service getters', () => { - const testServiceGetters = (name: string, services: Record) => { - const getters = Object.keys(services).filter((k) => k.substring(0, 3) === 'get'); - getters.forEach((g) => { - it(`ui/new_platform sets a value for ${name} getter ${g}`, () => { - __reset__(); - __setup__( - (coreMock.createSetup() as unknown) as LegacyCoreSetup, - (npSetup.plugins as unknown) as PluginsSetup - ); - __start__( - (coreMock.createStart() as unknown) as LegacyCoreStart, - (npStart.plugins as unknown) as PluginsStart - ); - - expect(services[g]()).toBeDefined(); - }); - }); - }; - - testServiceGetters('data', dataServices); - testServiceGetters('visualizations', visualizationsServices); - }); -}); diff --git a/src/legacy/ui/public/new_platform/set_services.ts b/src/legacy/ui/public/new_platform/set_services.ts deleted file mode 100644 index 036157a9f3fbc..0000000000000 --- a/src/legacy/ui/public/new_platform/set_services.ts +++ /dev/null @@ -1,87 +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 { pick } from 'lodash'; - -import { PluginsSetup, PluginsStart } from './new_platform'; -import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import * as dataServices from '../../../../plugins/data/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import * as visualizationsServices from '../../../../plugins/visualizations/public/services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createSavedVisLoader } from '../../../../plugins/visualizations/public/saved_visualizations/saved_visualizations'; - -interface NpSetup { - core: LegacyCoreSetup; - plugins: PluginsSetup; -} - -interface NpStart { - core: LegacyCoreStart; - plugins: PluginsStart; -} - -export function setSetupServices(npSetup: NpSetup) { - // Services that need to be set in the legacy platform since the legacy data plugin - // which previously provided them has been removed. - visualizationsServices.setUISettings(npSetup.core.uiSettings); - visualizationsServices.setUsageCollector(npSetup.plugins.usageCollection); -} - -export function setStartServices(npStart: NpStart) { - // Services that need to be set in the legacy platform since the legacy data plugin - // which previously provided them has been removed. - dataServices.setNotifications(npStart.core.notifications); - dataServices.setOverlays(npStart.core.overlays); - dataServices.setUiSettings(npStart.core.uiSettings); - dataServices.setFieldFormats(npStart.plugins.data.fieldFormats); - dataServices.setIndexPatterns(npStart.plugins.data.indexPatterns); - dataServices.setQueryService(npStart.plugins.data.query); - dataServices.setSearchService(npStart.plugins.data.search); - - visualizationsServices.setI18n(npStart.core.i18n); - visualizationsServices.setTypes( - pick(npStart.plugins.visualizations, ['get', 'all', 'getAliases']) - ); - visualizationsServices.setCapabilities(npStart.core.application.capabilities); - visualizationsServices.setHttp(npStart.core.http); - visualizationsServices.setApplication(npStart.core.application); - visualizationsServices.setEmbeddable(npStart.plugins.embeddable); - visualizationsServices.setSavedObjects(npStart.core.savedObjects); - visualizationsServices.setIndexPatterns(npStart.plugins.data.indexPatterns); - visualizationsServices.setFilterManager(npStart.plugins.data.query.filterManager); - visualizationsServices.setExpressions(npStart.plugins.expressions); - visualizationsServices.setUiActions(npStart.plugins.uiActions); - visualizationsServices.setTimeFilter(npStart.plugins.data.query.timefilter.timefilter); - visualizationsServices.setAggs(npStart.plugins.data.search.aggs); - visualizationsServices.setOverlays(npStart.core.overlays); - visualizationsServices.setChrome(npStart.core.chrome); - visualizationsServices.setSearch(npStart.plugins.data.search); - const savedVisualizationsLoader = createSavedVisLoader({ - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - search: npStart.plugins.data.search, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, - visualizationTypes: visualizationsServices.getTypes(), - }); - visualizationsServices.setSavedVisualizationsLoader(savedVisualizationsLoader); - visualizationsServices.setSavedSearchLoader(npStart.plugins.discover.savedSearchLoader); -} diff --git a/src/legacy/ui/public/notify/banners/BANNERS.md b/src/legacy/ui/public/notify/banners/BANNERS.md deleted file mode 100644 index fc6bddc3aa4ed..0000000000000 --- a/src/legacy/ui/public/notify/banners/BANNERS.md +++ /dev/null @@ -1,360 +0,0 @@ -# Banners - -Use this service to surface banners at the top of the screen. The expectation is that the banner will used an -`` to render, but that is not a requirement. See [the EUI docs](https://elastic.github.io/eui/) for -more information on banners and their role within the UI. - -Banners should be considered with respect to their lifecycle. Most banners are best served by using the `add` and -`remove` functions. - -## Importing the module - -```js -import { banners } from 'ui/notify'; -``` - -## Interface - -There are three methods defined to manipulate the list of banners: `add`, `set`, and `remove`. A fourth method, -`onChange` exists to listen to changes made via `add`, `set`, and `remove`. - -### `add()` - -This is the preferred way to add banners because it implies the best usage of the banner: added once during a page's -lifecycle. For other usages, consider *not* using a banner. - -#### Syntax - -```js -const bannerId = banners.add({ - // required: - component, - // optional: - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -To add a banner, you only need to define the `component` field. - -The `priority` sorts in descending order. Items sharing the same priority are sorted from oldest to newest. For example: - -```js -const banner1 = banners.add({ component: }); -const banner2 = banners.add({ component: , priority: 0 }); -const banner3 = banners.add({ component: , priority: 1 }); -``` - -That would be displayed as: - -``` -[ fake3 ] -[ fake1 ] -[ fake2 ] -``` - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example includes buttons that allow the user to remove the banner. In some cases, you may not want any buttons -and in other cases you will want an action to proceed the banner's removal (e.g., apply an Advanced Setting). - -This makes the most sense to use when a banner is added at the beginning of the page life cycle and not expected to -be touched, except by its own buttons triggering an action or navigating away. - -```js -const bannerId = banners.add({ - component: ( - - - - banners.remove(bannerId)} - > - Dismiss - - - - window.alert('Do Something Else')} - > - Do Something Else - - - - - ), -}); -``` - -### `remove()` - -Unlike toast notifications, banners stick around until they are explicitly removed. Using the `add` example above,you can remove it by calling `remove`. - -Note: They will stick around as long as the scope is remembered by whatever set it; navigating away won't remove it -unless the scope is forgotten (e.g., when the "app" changes)! - -#### Syntax - -```js -const removed = banners.remove(bannerId); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `id` | String | ID of a banner. | - -##### Returns - -| Type | Description | -|------|-------------| -| Boolean | `true` if the ID was recognized and the banner was removed. `false` otherwise. | - -#### Example - -To remove a banner, you need to pass the `id` of the banner. - -```js -if (banners.remove(bannerId)) { - // removed; otherwise it didn't exist (maybe it was already removed) -} -``` - -#### Scheduled removal - -Like toast notifications do automatically, you can have a banner automatically removed after a set of time, by -setting a timer: - -```js -setTimeout(() => banners.remove(bannerId), 15000); -``` - -Note: It is safe to remove a banner more than once as unknown IDs will be ignored. - -### `set()` - -Banners can be replaced once added by supplying their `id`. If one is supplied, then the ID will be used to replace -any banner with the same ID and a **new** `id` will be returned. - -You should only consider using `set` when the banner is manipulated frequently in the lifecycle of the page, where -maintaining the banner's `id` can be a burden. It is easier to allow `banners` to create the ID for you in most -situations where a banner is useful (e.g., set once), which safely avoids any chance to have an ID-based collision, -which happens automatically with `add`. - -Usage of `set` can imply that your use case is abusing the banner system. - -Note: `set` will only trigger the callback once for both the implicit remove and add operation. - -#### Syntax - -```js -const id = banners.set({ - // required: - component, - // optional: - id, - priority, -}); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | Optional ID used to remove an existing banner. | -| `priority` | Number | Optional priority, which defaults to `0` used to place the banner. | - -The `id` is optional because it follows the same semantics as the `remove` method: unknown IDs are ignored. This -is useful when first creating a banner so that you do not have to call `add` instead. - -##### Returns - -| Type | Description | -|------|-------------| -| String | A newly generated ID. | - -#### Example - -This example does not include any way for the user to clear the banner directly. Instead, it is cleared based on -time. Related to it being cleared by time, it can also reappear within the same page life cycle by navigating between -different paths that need it displayed. Instead of adding a new banner for every navigation, you should replace any -existing banner. - -```js -let bannerId; -let timeoutId; - -function displayBanner() { - clearTimeout(timeoutId); - - bannerId = banners.set({ - id: bannerId, // the first time it will be undefined, but reused as long as this is in the same lifecycle - component: ( - - ) - }); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - banner.timeoutId = setTimeout(() => { - banners.remove(bannerId); - timeoutId = undefined; - }, 6000); -} -``` - -### `onChange()` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js -// inside your React component -banners.onChange(() => this.forceUpdate()); -``` - -##### Parameters - -| Field | Type | Description | -|-------|------|-------------| -| `callback` | Function | The function to invoke whenever the internal banner list is changed. | - -Every new `callback` replaces the previous callback. So calling this with `null` or `undefined` will unset the -callback. - -##### Returns - -Nothing. - -#### Example - -This can be used inside of a React component to trigger a re-`render` of the banners. - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -### `list` - -For React components that intend to display the banners, it is not enough to simply `render` the `banners.list` -values. Because they can change after being rendered, the React component that renders the list must be alerted -to changes to the list. - -#### Syntax - -```js - -``` - -##### Returns - -| Type | Description | -|------|-------------| -| Array | The array of banner objects. | - -Banner objects are sorted in descending order based on their `priority`, in the form: - -```js -{ - id: 'banner-123', - component: , - priority: 12, -} -``` - -| Field | Type | Description | -|-------|------|-------------| -| `component` | Any | The value displayed as the banner. | -| `id` | String | The ID of the banner, which can be used as a React "key". | -| `priority` | Number | The priority of the banner. | - -#### Example - -This can be used to supply the banners to the `GlobalBannerList` React component (which is done for you). - -```js -import { GlobalBannerList } from 'ui/notify'; - - -``` - -## Use in functional tests - -Functional tests are commonly used to verify that an action yielded a successful outcome. You can place a -`data-test-subj` attribute on the banner and use it to check if the banner exists inside of your functional test. -This acts as a proxy for verifying the successful outcome. Any unrecognized field will be added as a property of the -containing element. - -```js -banners.add({ - component: ( - - ), - data-test-subj: 'my-tested-banner', -}); -``` - -This will apply the `data-test-subj` to the element containing the `component`, so the inner HTML of that element -will exclusively be the specified `component`. - -Given that `component` is expected to be a React component, you could also add the `data-test-subj` directly to it: - -```js -banners.add({ - component: ( - - ), -}); -``` \ No newline at end of file diff --git a/src/legacy/ui/public/notify/banners/banners.tsx b/src/legacy/ui/public/notify/banners/banners.tsx deleted file mode 100644 index 777e408b8dfc4..0000000000000 --- a/src/legacy/ui/public/notify/banners/banners.tsx +++ /dev/null @@ -1,85 +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 React from 'react'; -import ReactDOM from 'react-dom'; -import { npStart } from 'ui/new_platform'; -import { I18nProvider } from '@kbn/i18n/react'; - -const npBanners = npStart.core.overlays.banners; - -/** compatibility layer for new platform */ -const mountForComponent = (component: React.ReactElement) => (element: HTMLElement) => { - ReactDOM.render({component}, element); - return () => ReactDOM.unmountComponentAtNode(element); -}; - -/** - * Banners represents a prioritized list of displayed components. - */ -export class Banners { - /** - * Add a new banner. - * - * @param {Object} component The React component to display. - * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. - * @return {String} A newly generated ID. This value can be used to remove/replace the banner. - */ - add = ({ component, priority }: { component: React.ReactElement; priority?: number }) => { - return npBanners.add(mountForComponent(component), priority); - }; - - /** - * Remove an existing banner. - * - * @param {String} id The ID of the banner to remove. - * @return {Boolean} {@code true} if the ID is recognized and the banner is removed. {@code false} otherwise. - */ - remove = (id: string): boolean => { - return npBanners.remove(id); - }; - - /** - * Replace an existing banner by removing it, if it exists, and adding a new one in its place. - * - * This is similar to calling banners.remove, followed by banners.add, except that it only notifies the listener - * after adding. - * - * @param {Object} component The React component to display. - * @param {String} id The ID of the Banner to remove. - * @param {Number} priority The optional priority order to display this banner. Higher priority values are shown first. - * @return {String} A newly generated ID. This value can be used to remove/replace the banner. - */ - set = ({ - component, - id, - priority = 0, - }: { - component: React.ReactElement; - id: string; - priority?: number; - }): string => { - return npBanners.replace(id, mountForComponent(component), priority); - }; -} - -/** - * A singleton instance meant to represent all Kibana banners. - */ -export const banners = new Banners(); diff --git a/src/legacy/ui/public/notify/banners/index.ts b/src/legacy/ui/public/notify/banners/index.ts deleted file mode 100644 index 9221f95074cd9..0000000000000 --- a/src/legacy/ui/public/notify/banners/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/fatal_error.ts b/src/legacy/ui/public/notify/fatal_error.ts deleted file mode 100644 index 5614ffea7913e..0000000000000 --- a/src/legacy/ui/public/notify/fatal_error.ts +++ /dev/null @@ -1,25 +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 { npSetup } from 'ui/new_platform'; -import { AngularHttpError, addFatalError } from '../../../../plugins/kibana_legacy/public'; - -export function fatalError(error: AngularHttpError | Error | string, location?: string) { - addFatalError(npSetup.core.fatalErrors, error, location); -} diff --git a/src/legacy/ui/public/notify/index.d.ts b/src/legacy/ui/public/notify/index.d.ts deleted file mode 100644 index 3c1a7ba02db72..0000000000000 --- a/src/legacy/ui/public/notify/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { toastNotifications } from './toasts'; -export { fatalError } from './fatal_error'; diff --git a/src/legacy/ui/public/notify/index.js b/src/legacy/ui/public/notify/index.js deleted file mode 100644 index 51394033e4d2e..0000000000000 --- a/src/legacy/ui/public/notify/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { fatalError } from './fatal_error'; -export { toastNotifications } from './toasts'; -export { banners } from './banners'; diff --git a/src/legacy/ui/public/notify/toasts/index.ts b/src/legacy/ui/public/notify/toasts/index.ts deleted file mode 100644 index c5e8e3b7ca7a3..0000000000000 --- a/src/legacy/ui/public/notify/toasts/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -export { ToastNotifications } from './toast_notifications'; -export { toastNotifications } from './toasts'; diff --git a/src/legacy/ui/public/notify/toasts/toasts.ts b/src/legacy/ui/public/notify/toasts/toasts.ts deleted file mode 100644 index 15be7a7891553..0000000000000 --- a/src/legacy/ui/public/notify/toasts/toasts.ts +++ /dev/null @@ -1,22 +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 { npSetup } from 'ui/new_platform'; -import { ToastNotifications } from './toast_notifications'; -export const toastNotifications = new ToastNotifications(npSetup.core.notifications.toasts); diff --git a/src/legacy/ui/public/private/__tests__/private.js b/src/legacy/ui/public/private/__tests__/private.js deleted file mode 100644 index 1f9d696bb440f..0000000000000 --- a/src/legacy/ui/public/private/__tests__/private.js +++ /dev/null @@ -1,144 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -describe('Private module loader', function () { - let Private; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - }) - ); - - it('accepts a provider that will be called to init a module', function () { - const football = {}; - function Provider() { - return football; - } - - const instance = Private(Provider); - expect(instance).to.be(football); - }); - - it('injects angular dependencies into the Provider', function () { - function Provider(Private) { - return Private; - } - - const instance = Private(Provider); - expect(instance).to.be(Private); - }); - - it('detects circular dependencies', function () { - expect(function () { - function Provider1() { - Private(Provider2); - } - - function Provider2() { - Private(Provider1); - } - - Private(Provider1); - }).to.throwException(/circular/i); - }); - - it('always provides the same instance form the Provider', function () { - function Provider() { - return {}; - } - - expect(Private(Provider)).to.be(Private(Provider)); - }); - - describe('#stub', function () { - it('accepts a replacement instance for a Provider', function () { - const replaced = {}; - const replacement = {}; - - function Provider() { - return replaced; - } - - const instance = Private(Provider); - expect(instance).to.be(replaced); - - Private.stub(Provider, replacement); - - const instance2 = Private(Provider); - expect(instance2).to.be(replacement); - - Private.stub(Provider, replaced); - - const instance3 = Private(Provider); - expect(instance3).to.be(replaced); - }); - }); - - describe('#swap', function () { - it('accepts a new Provider that should replace an existing Provider', function () { - function Provider1() { - return {}; - } - - function Provider2() { - return {}; - } - - const instance1 = Private(Provider1); - expect(instance1).to.be.an('object'); - - Private.swap(Provider1, Provider2); - - const instance2 = Private(Provider1); - expect(instance2).to.be.an('object'); - expect(instance2).to.not.be(instance1); - - Private.swap(Provider1, Provider1); - - const instance3 = Private(Provider1); - expect(instance3).to.be(instance1); - }); - - it('gives the new Provider access to the Provider it replaced via an injectable dependency called $decorate', function () { - function Provider1() { - return {}; - } - - function Provider2($decorate) { - return { - instance1: $decorate(), - }; - } - - const instance1 = Private(Provider1); - expect(instance1).to.be.an('object'); - - Private.swap(Provider1, Provider2); - - const instance2 = Private(Provider1); - expect(instance2).to.have.property('instance1'); - expect(instance2.instance1).to.be(instance1); - }); - }); -}); diff --git a/src/legacy/ui/public/private/index.d.ts b/src/legacy/ui/public/private/index.d.ts deleted file mode 100644 index 3b692ba58cbe1..0000000000000 --- a/src/legacy/ui/public/private/index.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { IPrivate } from '../../../../plugins/kibana_legacy/public/'; diff --git a/src/legacy/ui/public/private/index.js b/src/legacy/ui/public/private/index.js deleted file mode 100644 index 3815f230df466..0000000000000 --- a/src/legacy/ui/public/private/index.js +++ /dev/null @@ -1,20 +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 './private'; diff --git a/src/legacy/ui/public/private/private.js b/src/legacy/ui/public/private/private.js deleted file mode 100644 index 7a0751959417e..0000000000000 --- a/src/legacy/ui/public/private/private.js +++ /dev/null @@ -1,23 +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 { uiModules } from '../modules'; -import { PrivateProvider } from '../../../../plugins/kibana_legacy/public'; - -uiModules.get('kibana/private').provider('Private', PrivateProvider); diff --git a/src/legacy/ui/public/promises/__tests__/promises.js b/src/legacy/ui/public/promises/__tests__/promises.js deleted file mode 100644 index 7041aa2993376..0000000000000 --- a/src/legacy/ui/public/promises/__tests__/promises.js +++ /dev/null @@ -1,277 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; - -describe('Promise service', () => { - let Promise; - let $rootScope; - - const sandbox = sinon.createSandbox(); - function tick(ms = 0) { - sandbox.clock.tick(ms); - - // Ugly, but necessary for promises to resolve: https://github.com/angular/angular.js/issues/12555 - $rootScope.$apply(); - } - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(($injector) => { - sandbox.useFakeTimers(); - - Promise = $injector.get('Promise'); - $rootScope = $injector.get('$rootScope'); - }) - ); - - afterEach(() => sandbox.restore()); - - describe('Constructor', () => { - it('provides resolve and reject function', () => { - const executor = sinon.stub(); - new Promise(executor); - - sinon.assert.calledOnce(executor); - sinon.assert.calledWithExactly(executor, sinon.match.func, sinon.match.func); - }); - }); - - it('Promise.resolve', () => { - const onResolve = sinon.stub(); - Promise.resolve(true).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, true); - }); - - describe('Promise.fromNode', () => { - it('creates a callback that controls a promise', () => { - const callback = sinon.stub(); - Promise.fromNode(callback); - - tick(); - - sinon.assert.calledOnce(callback); - sinon.assert.calledWithExactly(callback, sinon.match.func); - }); - - it('rejects if the callback receives an error', () => { - const err = new Error(); - const onReject = sinon.stub(); - Promise.fromNode(sinon.stub().yields(err)).catch(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(err)); - }); - - it('resolves with the second argument', () => { - const result = {}; - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, result)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, sinon.match.same(result)); - }); - - it('resolves with an array if multiple arguments are received', () => { - const result1 = {}; - const result2 = {}; - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, result1, result2)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, [ - sinon.match.same(result1), - sinon.match.same(result2), - ]); - }); - - it('resolves with an array if multiple undefined are received', () => { - const onResolve = sinon.stub(); - Promise.fromNode(sinon.stub().yields(null, undefined, undefined)).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, [undefined, undefined]); - }); - }); - - describe('Promise.race()', () => { - it(`resolves with the first resolved promise's value`, () => { - const p1 = new Promise((resolve) => setTimeout(resolve, 100, 1)); - const p2 = new Promise((resolve) => setTimeout(resolve, 200, 2)); - const onResolve = sinon.stub(); - Promise.race([p1, p2]).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it(`rejects with the first rejected promise's rejection reason`, () => { - const p1Error = new Error('1'); - const p1 = new Promise((r, reject) => setTimeout(reject, 200, p1Error)); - - const p2Error = new Error('2'); - const p2 = new Promise((r, reject) => setTimeout(reject, 100, p2Error)); - - const onReject = sinon.stub(); - Promise.race([p1, p2]).catch(onReject); - - tick(200); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(p2Error)); - }); - - it('does not wait for subsequent promises to resolve/reject', () => { - const onP1Resolve = sinon.stub(); - const p1 = new Promise((resolve) => setTimeout(resolve, 100)).then(onP1Resolve); - - const onP2Resolve = sinon.stub(); - const p2 = new Promise((resolve) => setTimeout(resolve, 101)).then(onP2Resolve); - - const onResolve = sinon.stub(); - Promise.race([p1, p2]).then(onResolve); - - tick(100); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledOnce(onP1Resolve); - sinon.assert.callOrder(onP1Resolve, onResolve); - sinon.assert.notCalled(onP2Resolve); - }); - - it('allows non-promises in the array', () => { - const onResolve = sinon.stub(); - Promise.race([1, 2, 3]).then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - describe('argument is undefined', () => { - it('rejects the promise', () => { - const football = {}; - const onReject = sinon.stub(); - Promise.race() - .catch(() => football) - .then(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(football)); - }); - }); - - describe('argument is a string', () => { - it(`resolves with the first character`, () => { - const onResolve = sinon.stub(); - Promise.race('abc').then(onResolve); - - tick(); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 'a'); - }); - }); - - describe('argument is a non-iterable object', () => { - it('reject the promise', () => { - const football = {}; - const onReject = sinon.stub(); - Promise.race({}) - .catch(() => football) - .then(onReject); - - tick(); - - sinon.assert.calledOnce(onReject); - sinon.assert.calledWithExactly(onReject, sinon.match.same(football)); - }); - }); - - describe('argument is a generator', () => { - it('resolves with the first resolved value', () => { - function* gen() { - yield new Promise((resolve) => setTimeout(resolve, 100, 1)); - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it('resolves with the first non-promise value', () => { - function* gen() { - yield 1; - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - }); - - it('iterates all values from the generator, even if one is already "resolved"', () => { - let yieldCount = 0; - function* gen() { - yieldCount += 1; - yield 1; - yieldCount += 1; - yield new Promise((resolve) => setTimeout(resolve, 200, 2)); - } - - const onResolve = sinon.stub(); - Promise.race(gen()).then(onResolve); - - tick(200); - - sinon.assert.calledOnce(onResolve); - sinon.assert.calledWithExactly(onResolve, 1); - expect(yieldCount).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/promises/defer.ts b/src/legacy/ui/public/promises/defer.ts deleted file mode 100644 index 3d435f2ba8dfd..0000000000000 --- a/src/legacy/ui/public/promises/defer.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface Defer { - promise: Promise; - resolve(value: T): void; - reject(reason: Error): void; -} - -export function createDefer(Class: typeof Promise): Defer { - const defer: Partial> = {}; - defer.promise = new Class((resolve, reject) => { - defer.resolve = resolve; - defer.reject = reject; - }); - return defer as Defer; -} diff --git a/src/legacy/ui/public/promises/index.ts b/src/legacy/ui/public/promises/index.ts deleted file mode 100644 index 07bb2554c5eda..0000000000000 --- a/src/legacy/ui/public/promises/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PromiseServiceCreator } from '../../../../plugins/kibana_legacy/public'; -export { createDefer } from './defer'; -// @ts-ignore -import { uiModules } from '../modules'; - -const module = uiModules.get('kibana'); -// Provides a tiny subset of the excellent API from -// bluebird, reimplemented using the $q service -module.service('Promise', PromiseServiceCreator); diff --git a/src/legacy/ui/public/react_components.js b/src/legacy/ui/public/react_components.js deleted file mode 100644 index 21fb53d6407fa..0000000000000 --- a/src/legacy/ui/public/react_components.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 'ngreact'; - -import { EuiIcon, EuiIconTip } from '@elastic/eui'; - -import { uiModules } from './modules'; - -const app = uiModules.get('app/kibana', ['react']); - -app.directive('icon', (reactDirective) => reactDirective(EuiIcon)); - -app.directive('iconTip', (reactDirective) => - reactDirective(EuiIconTip, ['content', 'type', 'position', 'title', 'color']) -); diff --git a/src/legacy/ui/public/registry/__tests__/registry.js b/src/legacy/ui/public/registry/__tests__/registry.js deleted file mode 100644 index 10b2423abc0e3..0000000000000 --- a/src/legacy/ui/public/registry/__tests__/registry.js +++ /dev/null @@ -1,168 +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 { uiRegistry } from '../_registry'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -describe('Registry', function () { - let Private; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - }) - ); - - it('is technically a function', function () { - const reg = uiRegistry(); - expect(reg).to.be.a('function'); - }); - - describe('#register', function () { - it('accepts a Private module', function () { - const reg = uiRegistry(); - const mod = function SomePrivateModule() {}; - - reg.register(mod); - // modules are not exposed, so this is the most that we can test - }); - - it('applies the filter function if one is specified', function () { - const reg = uiRegistry({ - filter: (item) => item.value % 2 === 0, // register only even numbers - }); - - reg.register(() => ({ value: 17 })); - reg.register(() => ({ value: 18 })); // only this one should get registered - reg.register(() => ({ value: 19 })); - - const modules = Private(reg); - expect(modules).to.have.length(1); - expect(modules[0].value).to.be(18); - }); - }); - - describe('as a module', function () { - it('exposes the list of registered modules', function () { - const reg = uiRegistry(); - const mod = function SomePrivateModule(Private) { - this.PrivateModuleLoader = Private; - }; - - reg.register(mod); - const modules = Private(reg); - expect(modules).to.have.length(1); - expect(modules[0]).to.have.property('PrivateModuleLoader', Private); - }); - }); - - describe('spec', function () { - it('executes with the module list as "this", and can override it', function () { - let self; - - const reg = uiRegistry({ - constructor: function () { - return { mods: (self = this) }; - }, - }); - - const modules = Private(reg); - expect(modules).to.be.an('object'); - expect(modules).to.have.property('mods', self); - }); - }); - - describe('spec.name', function () { - it('sets the displayName of the registry and the name param on the final instance', function () { - const reg = uiRegistry({ - name: 'visTypes', - }); - - expect(reg).to.have.property('displayName', '[registry visTypes]'); - expect(Private(reg)).to.have.property('name', 'visTypes'); - }); - }); - - describe('spec.constructor', function () { - it('executes before the modules are returned', function () { - let i = 0; - - const reg = uiRegistry({ - constructor: function () { - i = i + 1; - }, - }); - - Private(reg); - expect(i).to.be(1); - }); - - it('executes with the module list as "this", and can override it', function () { - let self; - - const reg = uiRegistry({ - constructor: function () { - return { mods: (self = this) }; - }, - }); - - const modules = Private(reg); - expect(modules).to.be.an('object'); - expect(modules).to.have.property('mods', self); - }); - }); - - describe('spec.invokeProviders', () => { - it('is called with the registered providers and defines the initial set of values in the registry', () => { - const reg = uiRegistry({ - invokeProviders(providers) { - return providers.map((i) => i * 1000); - }, - }); - - reg.register(1); - reg.register(2); - reg.register(3); - expect(Private(reg).toJSON()).to.eql([1000, 2000, 3000]); - }); - it('does not get assigned as a property of the registry', () => { - expect( - uiRegistry({ - invokeProviders() {}, - }) - ).to.not.have.property('invokeProviders'); - }); - }); - - describe('spec[any]', function () { - it('mixes the extra properties into the module list', function () { - const reg = uiRegistry({ - someMethod: function () { - return this; - }, - }); - - const modules = Private(reg); - expect(modules).to.have.property('someMethod'); - expect(modules.someMethod()).to.be(modules); - }); - }); -}); diff --git a/src/legacy/ui/public/registry/_registry.d.ts b/src/legacy/ui/public/registry/_registry.d.ts deleted file mode 100644 index 42f1bb521763c..0000000000000 --- a/src/legacy/ui/public/registry/_registry.d.ts +++ /dev/null @@ -1,40 +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 { IndexedArray, IndexedArrayConfig } from '../indexed_array'; - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface UIRegistry extends IndexedArray {} - -interface UIRegistrySpec extends IndexedArrayConfig { - name: string; - filter?(item: T): boolean; -} - -/** - * Creates a new UiRegistry (See js method for detailed documentation) - * The generic type T is the type of objects which are stored in the registry. - * The generic type A is an interface of accessors which depend on the - * fields of the objects stored in the registry. - * Example: if there is a string field "name" in type T, then A should be - * `{ byName: { [typeName: string]: T }; }` - */ -declare function uiRegistry( - spec: UIRegistrySpec -): { (): UIRegistry & A; register(privateModule: T): UIRegistry & A }; diff --git a/src/legacy/ui/public/registry/_registry.js b/src/legacy/ui/public/registry/_registry.js deleted file mode 100644 index 85aa1d9f2eca8..0000000000000 --- a/src/legacy/ui/public/registry/_registry.js +++ /dev/null @@ -1,141 +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 _ from 'lodash'; -import { IndexedArray } from '../indexed_array'; - -const notPropsOptNames = IndexedArray.OPT_NAMES.concat('constructor', 'invokeProviders'); - -/** - * Create a registry, which is just a Private module provider. - * - * The registry allows modifying the values it will provide - * using the #register method. - * - * To access these modules, pass the registry to the Private - * module loader. - * - * # Examples - * - * + register a module - * ```js - * let registry = require('ui/registry/vis_types'); - * registry.add(function InjectablePrivateModule($http, Promise) { - * ... - * }) - * ``` - * - * + get all registered modules - * ```js - * let visTypes = Private(RegistryVisTypesProvider); - * ``` - * - * - * @param {object} [spec] - an object describing the properties of - * the registry to create. Any property specified - * that is not listed below will be mixed into the - * final IndexedArray object. - * - * # init - * @param {Function} [spec.constructor] - an injectable function that is called when - * the registry is first instantiated by the app. - * @param {boolean} [spec.filter] - function that will be used to filter items before - * registering them. Function will called on each item and - * should return true to keep the item (register it) or - * skip it (don't register it) - * - * # IndexedArray params - * @param {array[String]} [spec.index] - passed to the IndexedArray constructor - * @param {array[String]} [spec.group] - passed to the IndexedArray constructor - * @param {array[String]} [spec.order] - passed to the IndexedArray constructor - * @param {array[String]} [spec.initialSet] - passed to the IndexedArray constructor - * @param {array[String]} [spec.immutable] - passed to the IndexedArray constructor - * - * @return {[type]} [description] - */ -export function uiRegistry(spec) { - spec = spec || {}; - - const constructor = _.has(spec, 'constructor') && spec.constructor; - const filter = _.has(spec, 'filter') && spec.filter; - const invokeProviders = _.has(spec, 'invokeProviders') && spec.invokeProviders; - const iaOpts = _.defaults(_.pick(spec, IndexedArray.OPT_NAMES), { index: ['name'] }); - const props = _.omit(spec, notPropsOptNames); - const providers = []; - - let isInstantiated = false; - let getInvokedProviders; - let modules; - - /** - * This is the Private module that will be instantiated by - * - * @tag:PrivateModule - * @return {IndexedArray} - an indexed array containing the values - * that were registered, the registry spec - * defines how things will be indexed. - */ - const registry = function (Private, $injector) { - getInvokedProviders = function (newProviders) { - let set = invokeProviders - ? $injector.invoke(invokeProviders, undefined, { providers: newProviders }) - : newProviders.map(Private); - - if (filter && _.isFunction(filter)) { - set = set.filter((item) => filter(item)); - } - - return set; - }; - - iaOpts.initialSet = getInvokedProviders(providers); - - modules = new IndexedArray(iaOpts); - - // mixin other props - _.assign(modules, props); - - // construct - if (constructor) { - modules = $injector.invoke(constructor, modules) || modules; - } - - isInstantiated = true; - - return modules; - }; - - registry.displayName = '[registry ' + props.name + ']'; - - registry.register = function (privateModule) { - providers.push(privateModule); - - if (isInstantiated) { - const [provider] = getInvokedProviders([privateModule]); - - if (provider) { - modules.push(provider); - } - } - - return registry; - }; - - return registry; -} diff --git a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts b/src/legacy/ui/public/registry/chrome_header_nav_controls.ts deleted file mode 100644 index 7e6c80692cd63..0000000000000 --- a/src/legacy/ui/public/registry/chrome_header_nav_controls.ts +++ /dev/null @@ -1,49 +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 { IndexedArray } from '../indexed_array'; -import { uiRegistry, UIRegistry } from './_registry'; - -interface ChromeHeaderNavControlsRegistryAccessors { - bySide: { [typeName: string]: IndexedArray }; -} - -export enum NavControlSide { - Left = 'left', - Right = 'right', -} - -export interface NavControl { - name: string; - order: number; - side: NavControlSide; - render: (targetDomElement: HTMLDivElement) => () => void; -} - -export type ChromeHeaderNavControlsRegistry = UIRegistry & - ChromeHeaderNavControlsRegistryAccessors; - -export const chromeHeaderNavControlsRegistry = uiRegistry< - NavControl, - ChromeHeaderNavControlsRegistryAccessors ->({ - name: 'chromeHeaderNavControls', - order: ['order'], - group: ['side'], -}); diff --git a/src/legacy/ui/public/registry/field_format_editors.ts b/src/legacy/ui/public/registry/field_format_editors.ts deleted file mode 100644 index 5489ce9250e04..0000000000000 --- a/src/legacy/ui/public/registry/field_format_editors.ts +++ /dev/null @@ -1,25 +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 { uiRegistry } from './_registry'; - -export const RegistryFieldFormatEditorsProvider = uiRegistry({ - name: 'fieldFormatEditors', - index: ['formatId'], -}); diff --git a/src/legacy/ui/public/routes/__tests__/_route_manager.js b/src/legacy/ui/public/routes/__tests__/_route_manager.js deleted file mode 100644 index eb47a3e9ace70..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/_route_manager.js +++ /dev/null @@ -1,218 +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 _ from 'lodash'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import RouteManager from '../route_manager'; -import expect from '@kbn/expect'; - -let routes; // will contain an new instance of RouteManager for each test -const chainableMethods = [ - { name: 'when', args: ['', {}] }, - { name: 'otherwise', args: [{}] }, - { name: 'defaults', args: [/regexp/, {}] }, -]; - -let $rp; -describe('routes/route_manager', function () { - beforeEach( - ngMock.module('kibana', function ($routeProvider) { - $rp = $routeProvider; - sinon.stub($rp, 'otherwise'); - sinon.stub($rp, 'when'); - }) - ); - - beforeEach( - ngMock.inject(function () { - routes = new RouteManager(); - }) - ); - - it('should have chainable methods: ' + _.map(chainableMethods, 'name').join(', '), function () { - chainableMethods.forEach(function (meth) { - expect(routes[meth.name].apply(routes, _.clone(meth.args))).to.be(routes); - }); - }); - - describe('#otherwise', function () { - it('should forward the last otherwise route', function () { - const otherRoute = {}; - routes.otherwise({}); - routes.otherwise(otherRoute); - - routes.config($rp); - - expect($rp.otherwise.callCount).to.be(1); - expect($rp.otherwise.getCall(0).args[0]).to.be(otherRoute); - }); - }); - - describe('#when', function () { - it('should merge the additions into the when() defined routes', function () { - routes.when('/some/route'); - routes.when('/some/other/route'); - - // add the addition resolve to every route - routes.defaults(/.*/, { - resolve: { - addition: function () {}, - }, - }); - - routes.config($rp); - - // should have run once for each when route - expect($rp.when.callCount).to.be(2); - expect($rp.otherwise.callCount).to.be(0); - - // every route should have the "addition" resolve - expect($rp.when.getCall(0).args[1].resolve.addition).to.be.a('function'); - expect($rp.when.getCall(1).args[1].resolve.addition).to.be.a('function'); - }); - }); - - describe('#config', function () { - it('should add defined routes to the global $routeProvider service in order', function () { - const args = [ - ['/one', {}], - ['/two', {}], - ]; - - args.forEach(function (a) { - routes.when(a[0], a[1]); - }); - - routes.config($rp); - - expect($rp.when.callCount).to.be(args.length); - _.times(args.length, function (i) { - const call = $rp.when.getCall(i); - const a = args.shift(); - - expect(call.args[0]).to.be(a[0]); - expect(call.args[1]).to.be(a[1]); - }); - }); - - it('sets route.reloadOnSearch to false by default', function () { - routes.when('/nothing-set'); - routes.when('/no-reload', { reloadOnSearch: false }); - routes.when('/always-reload', { reloadOnSearch: true }); - routes.config($rp); - - expect($rp.when.callCount).to.be(3); - expect($rp.when.firstCall.args[1]).to.have.property('reloadOnSearch', false); - expect($rp.when.secondCall.args[1]).to.have.property('reloadOnSearch', false); - expect($rp.when.lastCall.args[1]).to.have.property('reloadOnSearch', true); - }); - }); - - describe('#defaults()', () => { - it('adds defaults to routes with matching paths', () => { - routes.when('/foo', { name: 'foo' }); - routes.when('/bar', { name: 'bar' }); - routes.when('/baz', { name: 'baz' }); - routes.defaults(/^\/ba/, { - withDefaults: true, - }); - routes.config($rp); - - sinon.assert.calledWithExactly( - $rp.when, - '/foo', - sinon.match({ name: 'foo', withDefaults: undefined }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/bar', - sinon.match({ name: 'bar', withDefaults: true }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/baz', - sinon.match({ name: 'baz', withDefaults: true }) - ); - }); - - it('does not override values specified in the route', () => { - routes.when('/foo', { name: 'foo' }); - routes.defaults(/./, { name: 'bar' }); - routes.config($rp); - - sinon.assert.calledWithExactly($rp.when, '/foo', sinon.match({ name: 'foo' })); - }); - - // See https://github.com/elastic/kibana/issues/13294 - it('does not assign defaults by reference, to prevent accidentally merging unrelated defaults together', () => { - routes.when('/foo', { name: 'foo' }); - routes.when('/bar', { name: 'bar' }); - routes.when('/baz', { name: 'baz', funcs: { bazFunc() {} } }); - - // multiple defaults must be defined that, when applied correctly, will - // create a new object property on all routes that is unique to all of them - routes.defaults(/./, { funcs: { all() {} } }); - routes.defaults(/^\/foo/, { funcs: { fooFunc() {} } }); - routes.defaults(/^\/bar/, { funcs: { barFunc() {} } }); - routes.config($rp); - - sinon.assert.calledThrice($rp.when); - sinon.assert.calledWithExactly( - $rp.when, - '/foo', - sinon.match({ - name: 'foo', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: sinon.match.func, - barFunc: undefined, - bazFunc: undefined, - }), - }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/bar', - sinon.match({ - name: 'bar', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: undefined, - barFunc: sinon.match.func, - bazFunc: undefined, - }), - }) - ); - sinon.assert.calledWithExactly( - $rp.when, - '/baz', - sinon.match({ - name: 'baz', - funcs: sinon.match({ - all: sinon.match.func, - fooFunc: undefined, - barFunc: undefined, - bazFunc: sinon.match.func, - }), - }) - ); - }); - }); -}); diff --git a/src/legacy/ui/public/routes/__tests__/_work_queue.js b/src/legacy/ui/public/routes/__tests__/_work_queue.js deleted file mode 100644 index 72891f7321fbd..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/_work_queue.js +++ /dev/null @@ -1,116 +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 _ from 'lodash'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { WorkQueue } from '../work_queue'; -import sinon from 'sinon'; -import { createDefer } from 'ui/promises'; - -describe('work queue', function () { - let queue; - let Promise; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_Promise_) { - Promise = _Promise_; - }) - ); - beforeEach(function () { - queue = new WorkQueue(); - }); - afterEach(function () { - queue.empty(); - }); - - describe('#push', function () { - it('adds to the interval queue', function () { - queue.push(createDefer(Promise)); - expect(queue).to.have.length(1); - }); - }); - - describe('#resolveWhenFull', function () { - it('resolves requests waiting for the queue to fill when appropriate', function () { - const size = _.random(5, 50); - queue.limit = size; - - const whenFull = createDefer(Promise); - sinon.stub(whenFull, 'resolve'); - queue.resolveWhenFull(whenFull); - - // push all but one into the queue - _.times(size - 1, function () { - queue.push(createDefer(Promise)); - }); - - expect(whenFull.resolve.callCount).to.be(0); - queue.push(createDefer(Promise)); - expect(whenFull.resolve.callCount).to.be(1); - - queue.empty(); - }); - }); - - /** - * Fills the queue with a random number of work defers, but stubs all defer methods - * with the same stub and passed it back. - * - * @param {function} then - called with then(size, stub) so that the test - * can manipulate the filled queue - */ - function fillWithStubs(then) { - const size = _.random(5, 50); - const stub = sinon.stub(); - - _.times(size, function () { - const d = createDefer(Promise); - // overwrite the defer methods with the stub - d.resolve = stub; - d.reject = stub; - queue.push(d); - }); - - then(size, stub); - } - - describe('#doWork', function () { - it('flushes the queue and resolves all promises', function () { - fillWithStubs(function (size, stub) { - expect(queue).to.have.length(size); - queue.doWork(); - expect(queue).to.have.length(0); - expect(stub.callCount).to.be(size); - }); - }); - }); - - describe('#empty()', function () { - it('empties the internal queue WITHOUT resolving any promises', function () { - fillWithStubs(function (size, stub) { - expect(queue).to.have.length(size); - queue.empty(); - expect(queue).to.have.length(0); - expect(stub.callCount).to.be(0); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js b/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js deleted file mode 100644 index 8ae85fce591a1..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/_wrap_route_with_prep.js +++ /dev/null @@ -1,106 +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 RouteManager from '../route_manager'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import _ from 'lodash'; -import '../../private'; - -let routes; - -describe('wrapRouteWithPrep fn', function () { - require('test_utils/no_digest_promises').activateForSuite(); - - beforeEach(function () { - routes = new RouteManager(); - }); - - const SchedulingTest = function (opts) { - opts = opts || {}; - - const delaySetup = opts.delayUserWork ? 0 : 50; - const delayUserWork = opts.delayUserWork ? 50 : 0; - - return function () { - ngMock.module('kibana'); - let setupComplete = false; - let userWorkComplete = false; - let route; - let Promise; - let $injector; - - ngMock.inject(function (_Promise_, _$injector_) { - Promise = _Promise_; - $injector = _$injector_; - }); - - routes.addSetupWork(function () { - return new Promise(function (resolve) { - setTimeout(function () { - setupComplete = true; - resolve(); - }, delaySetup); - }); - }); - - routes - .when('/', { - resolve: { - test: function () { - expect(setupComplete).to.be(true); - userWorkComplete = true; - }, - }, - }) - .config({ - when: function (p, _r) { - route = _r; - }, - }); - - return new Promise(function (resolve, reject) { - setTimeout(function () { - Promise.all( - _.map(route.resolve, function (fn) { - return $injector.invoke(fn); - }) - ) - .then(function () { - expect(setupComplete).to.be(true); - expect(userWorkComplete).to.be(true); - }) - .then(resolve, reject); - }, delayUserWork); - }); - }; - }; - - it('always waits for setup to complete before calling user work', new SchedulingTest()); - - it('does not call user work when setup fails', new SchedulingTest({ failSetup: true })); - - it( - 'calls all user work even if it is not initialized until after setup is complete', - new SchedulingTest({ - delayUserWork: false, - }) - ); -}); diff --git a/src/legacy/ui/public/routes/__tests__/index.js b/src/legacy/ui/public/routes/__tests__/index.js deleted file mode 100644 index 30cedaaca3d19..0000000000000 --- a/src/legacy/ui/public/routes/__tests__/index.js +++ /dev/null @@ -1,23 +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 './_route_manager'; -import './_work_queue'; -import './_wrap_route_with_prep'; -describe('Custom Route Management', function () {}); diff --git a/src/legacy/ui/public/routes/breadcrumbs.js b/src/legacy/ui/public/routes/breadcrumbs.js deleted file mode 100644 index 7917ffbd7c6e6..0000000000000 --- a/src/legacy/ui/public/routes/breadcrumbs.js +++ /dev/null @@ -1,44 +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 { trim, startCase } from 'lodash'; - -/** - * Take a path (from $location.path() usually) and parse - * it's segments into a list of breadcrumbs - * - * @param {string} path - * @return {Array} - */ -export function parsePathToBreadcrumbs(path) { - return trim(path, '/') - .split('/') - .reduce( - (acc, id, i, parts) => [ - ...acc, - { - id, - display: startCase(id), - href: i === 0 ? `#/${id}` : `${acc[i - 1].href}/${id}`, - current: i === parts.length - 1, - }, - ], - [] - ); -} diff --git a/src/legacy/ui/public/routes/index.d.ts b/src/legacy/ui/public/routes/index.d.ts deleted file mode 100644 index 84e17b0a69452..0000000000000 --- a/src/legacy/ui/public/routes/index.d.ts +++ /dev/null @@ -1,24 +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 { uiRoutes, UIRoutes } from 'ui/routes/routes'; - -// eslint-disable-next-line import/no-default-export -export default uiRoutes; -export { UIRoutes }; diff --git a/src/legacy/ui/public/routes/index.js b/src/legacy/ui/public/routes/index.js deleted file mode 100644 index 9c826ebee1230..0000000000000 --- a/src/legacy/ui/public/routes/index.js +++ /dev/null @@ -1,23 +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 { uiRoutes } from './routes'; - -// eslint-disable-next-line import/no-default-export -export default uiRoutes; diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts deleted file mode 100644 index a5261a7c8ee3a..0000000000000 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ /dev/null @@ -1,47 +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. - */ - -/** - * WARNING: these types are incomplete - */ - -import { ChromeBreadcrumb } from '../../../../core/public'; - -export interface RouteConfiguration { - controller?: string | ((...args: any[]) => void); - redirectTo?: string; - resolveRedirectTo?: (...args: any[]) => void; - reloadOnSearch?: boolean; - reloadOnUrl?: boolean; - outerAngularWrapperRoute?: boolean; - resolve?: object; - template?: string; - k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; - requireUICapability?: string; -} - -interface RouteManager { - addSetupWork(cb: (...args: any[]) => void): void; - when(path: string, routeConfiguration: RouteConfiguration): RouteManager; - otherwise(routeConfiguration: RouteConfiguration): RouteManager; - defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager; -} - -// eslint-disable-next-line import/no-default-export -export default RouteManager; diff --git a/src/legacy/ui/public/routes/route_manager.js b/src/legacy/ui/public/routes/route_manager.js deleted file mode 100644 index de8a541d1c50a..0000000000000 --- a/src/legacy/ui/public/routes/route_manager.js +++ /dev/null @@ -1,115 +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 { cloneDeep, defaultsDeep, wrap } from 'lodash'; - -import { wrapRouteWithPrep } from './wrap_route_with_prep'; -import { RouteSetupManager } from './route_setup_manager'; -import { parsePathToBreadcrumbs } from './breadcrumbs'; - -// eslint-disable-next-line import/no-default-export -export default function RouteManager() { - const self = this; - const setup = new RouteSetupManager(); - const when = []; - const defaults = []; - let otherwise; - - self.config = function ($routeProvider) { - when.forEach(function (args) { - const path = args[0]; - const route = args[1] || {}; - - defaults.forEach((def) => { - if (def.regex.test(path)) { - defaultsDeep(route, cloneDeep(def.value)); - } - }); - - if (route.reloadOnSearch == null) { - route.reloadOnSearch = false; - } - - wrapRouteWithPrep(route, setup); - $routeProvider.when(path, route); - }); - - if (otherwise) { - wrapRouteWithPrep(otherwise, setup); - $routeProvider.otherwise(otherwise); - } - }; - - self.run = function ($location, $route, $injector, $rootScope) { - if (window.elasticApm && typeof window.elasticApm.startTransaction === 'function') { - /** - * capture route-change events as transactions which happens after - * the browser's on load event. - * - * In Kibana app, this logic would run after the boostrap js files gets - * downloaded and get associated with the page-load transaction - */ - $rootScope.$on('$routeChangeStart', (_, nextRoute) => { - if (nextRoute.$$route) { - const name = nextRoute.$$route.originalPath; - window.elasticApm.startTransaction(name, 'route-change'); - } - }); - } - - self.getBreadcrumbs = () => { - const breadcrumbs = parsePathToBreadcrumbs($location.path()); - const map = $route.current.mapBreadcrumbs; - return map ? $injector.invoke(map, null, { breadcrumbs }) : breadcrumbs; - }; - }; - - const wrapSetupAndChain = (fn, ...args) => { - fn.apply(setup, args); - return this; - }; - - this.addSetupWork = wrap(setup.addSetupWork, wrapSetupAndChain); - this.afterSetupWork = wrap(setup.afterSetupWork, wrapSetupAndChain); - this.afterWork = wrap(setup.afterWork, wrapSetupAndChain); - - self.when = function (path, route) { - when.push([path, route]); - return self; - }; - - // before attaching the routes to the routeProvider, test the RE - // against the .when() path and add/override the resolves if there is a match - self.defaults = function (regex, value) { - defaults.push({ regex, value }); - return self; - }; - - self.otherwise = function (route) { - otherwise = route; - return self; - }; - - self.getBreadcrumbs = function () { - // overwritten in self.run(); - return []; - }; - - self.RouteManager = RouteManager; -} diff --git a/src/legacy/ui/public/routes/route_setup_manager.js b/src/legacy/ui/public/routes/route_setup_manager.js deleted file mode 100644 index a7a2f078f40fb..0000000000000 --- a/src/legacy/ui/public/routes/route_setup_manager.js +++ /dev/null @@ -1,112 +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 _ from 'lodash'; -import { createDefer } from 'ui/promises'; - -// Throw this inside of an Angular route resolver after calling `kbnUrl.change` -// so that the $router can observe the $location update. Otherwise, the location -// route setup work will resolve before the route update occurs. -export const WAIT_FOR_URL_CHANGE_TOKEN = new Error('WAIT_FOR_URL_CHANGE_TOKEN'); - -export class RouteSetupManager { - constructor() { - this.setupWork = []; - this.onSetupComplete = []; - this.onSetupError = []; - this.onWorkComplete = []; - this.onWorkError = []; - } - - addSetupWork(fn) { - this.setupWork.push(fn); - } - - afterSetupWork(onComplete, onError) { - this.onSetupComplete.push(onComplete); - this.onSetupError.push(onError); - } - - afterWork(onComplete, onError) { - this.onWorkComplete.push(onComplete); - this.onWorkError.push(onError); - } - - /** - * Do each setupWork function by injecting it with angular dependencies - * and accepting promises from it. - * @return {[type]} [description] - */ - doWork(Promise, $injector, userWork) { - const invokeEach = (arr, locals) => { - return Promise.map(arr, (fn) => { - if (!fn) return; - return $injector.invoke(fn, null, locals); - }); - }; - - // call each error handler in order, until one of them resolves - // or we run out of handlers - const callErrorHandlers = (handlers, origError) => { - if (!_.size(handlers)) throw origError; - - // clone so we don't discard handlers or loose them - handlers = handlers.slice(0); - - const next = (err) => { - if (!handlers.length) throw err; - - const handler = handlers.shift(); - if (!handler) return next(err); - - return Promise.try(function () { - return $injector.invoke(handler, null, { err }); - }).catch(next); - }; - - return next(origError); - }; - - return invokeEach(this.setupWork) - .then( - () => invokeEach(this.onSetupComplete), - (err) => callErrorHandlers(this.onSetupError, err) - ) - .then(() => { - // wait for the queue to fill up, then do all the work - const defer = createDefer(Promise); - userWork.resolveWhenFull(defer); - - return defer.promise.then(() => Promise.all(userWork.doWork())); - }) - .catch((error) => { - if (error === WAIT_FOR_URL_CHANGE_TOKEN) { - // prevent moving forward, return a promise that never resolves - // so that the $router can observe the $location update - return Promise.halt(); - } - - throw error; - }) - .then( - () => invokeEach(this.onWorkComplete), - (err) => callErrorHandlers(this.onWorkError, err) - ); - } -} diff --git a/src/legacy/ui/public/routes/routes.d.ts b/src/legacy/ui/public/routes/routes.d.ts deleted file mode 100644 index d48230e9d56f9..0000000000000 --- a/src/legacy/ui/public/routes/routes.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import RouteManager from 'ui/routes/route_manager'; - -interface DefaultRouteManager extends RouteManager { - WAIT_FOR_URL_CHANGE_TOKEN: string; - enable(): void; -} - -export const uiRoutes: DefaultRouteManager; -export type UIRoutes = DefaultRouteManager; diff --git a/src/legacy/ui/public/routes/routes.js b/src/legacy/ui/public/routes/routes.js deleted file mode 100644 index 2cfb7a608e853..0000000000000 --- a/src/legacy/ui/public/routes/routes.js +++ /dev/null @@ -1,39 +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 RouteManager from './route_manager'; -import 'angular-route/angular-route'; -import { uiModules } from '../modules'; -import { WAIT_FOR_URL_CHANGE_TOKEN } from './route_setup_manager'; -const defaultRouteManager = new RouteManager(); - -export const uiRoutes = Object.create(defaultRouteManager, { - WAIT_FOR_URL_CHANGE_TOKEN: { - value: WAIT_FOR_URL_CHANGE_TOKEN, - }, - - enable: { - value() { - uiModules - .get('kibana', ['ngRoute']) - .config(defaultRouteManager.config) - .run(defaultRouteManager.run); - }, - }, -}); diff --git a/src/legacy/ui/public/routes/work_queue.js b/src/legacy/ui/public/routes/work_queue.js deleted file mode 100644 index 5c4c83663c590..0000000000000 --- a/src/legacy/ui/public/routes/work_queue.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function WorkQueue() { - const q = this; - - const work = []; - const fullDefers = []; - - q.limit = 0; - Object.defineProperty(q, 'length', { - get: function () { - return work.length; - }, - }); - - const resolve = function (defers) { - return defers.splice(0).map(function (defer) { - return defer.resolve(); - }); - }; - - const checkIfFull = function () { - if (work.length >= q.limit && fullDefers.length) { - resolve(fullDefers); - } - }; - - q.resolveWhenFull = function (defer) { - fullDefers.push(defer); - checkIfFull(); - }; - - q.doWork = function () { - const resps = resolve(work); - checkIfFull(); - return resps; - }; - - q.empty = function () { - work.splice(0); - checkIfFull(); - }; - - q.push = function (defer) { - work.push(defer); - checkIfFull(); - }; -} diff --git a/src/legacy/ui/public/routes/wrap_route_with_prep.js b/src/legacy/ui/public/routes/wrap_route_with_prep.js deleted file mode 100644 index e9ed33148d9ac..0000000000000 --- a/src/legacy/ui/public/routes/wrap_route_with_prep.js +++ /dev/null @@ -1,52 +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 angular from 'angular'; -import _ from 'lodash'; -import { createDefer } from 'ui/promises'; -import { WorkQueue } from './work_queue'; - -export function wrapRouteWithPrep(route, setup) { - if (!route.resolve && route.redirectTo) return; - - const userWork = new WorkQueue(); - // the point at which we will consider the queue "full" - userWork.limit = _.keys(route.resolve).length; - - const resolve = { - __prep__: function ($injector) { - return $injector.invoke(setup.doWork, setup, { userWork }); - }, - }; - - // send each user resolve to the userWork queue, which will prevent it from running before the - // prep is complete - _.forOwn(route.resolve || {}, function (expr, name) { - resolve[name] = function ($injector, Promise) { - const defer = createDefer(Promise); - userWork.push(defer); - return defer.promise.then(function () { - return $injector[angular.isString(expr) ? 'get' : 'invoke'](expr); - }); - }; - }); - - // we're copied everything over so now overwrite - route.resolve = resolve; -} diff --git a/src/legacy/ui/public/state_management/__tests__/app_state.js b/src/legacy/ui/public/state_management/__tests__/app_state.js deleted file mode 100644 index c47fa3bb0417f..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/app_state.js +++ /dev/null @@ -1,76 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { AppStateProvider } from '../app_state'; - -describe('State Management', function () { - let $rootScope; - let AppState; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private) { - $rootScope = _$rootScope_; - AppState = Private(AppStateProvider); - }) - ); - - describe('App State', function () { - let appState; - - beforeEach(function () { - appState = new AppState(); - }); - - it('should have _urlParam of _a', function () { - expect(appState).to.have.property('_urlParam'); - expect(appState._urlParam).to.equal('_a'); - }); - - it('should use passed in params', function () { - const params = { - test: true, - mock: false, - }; - - appState = new AppState(params); - expect(appState).to.have.property('_defaults'); - - Object.keys(params).forEach(function (key) { - expect(appState._defaults).to.have.property(key); - expect(appState._defaults[key]).to.equal(params[key]); - }); - }); - - it('should have a destroy method', function () { - expect(appState).to.have.property('destroy'); - }); - - it('should be destroyed on $routeChangeStart', function () { - const destroySpy = sinon.spy(appState, 'destroy'); - - $rootScope.$emit('$routeChangeStart'); - - expect(destroySpy.callCount).to.be(1); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/config_provider.js b/src/legacy/ui/public/state_management/__tests__/config_provider.js deleted file mode 100644 index 9f756bc51dcb0..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/config_provider.js +++ /dev/null @@ -1,57 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../config_provider'; - -describe('State Management Config', function () { - let stateManagementConfig; - - describe('is enabled', () => { - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_stateManagementConfig_) { - stateManagementConfig = _stateManagementConfig_; - }) - ); - - it('should be enabled by default', () => { - expect(stateManagementConfig.enabled).to.be(true); - }); - }); - - describe('can be disabled', () => { - beforeEach( - ngMock.module('kibana', function (stateManagementConfigProvider) { - stateManagementConfigProvider.disable(); - }) - ); - - beforeEach( - ngMock.inject(function (_stateManagementConfig_) { - stateManagementConfig = _stateManagementConfig_; - }) - ); - - it('is disabled by config', () => { - expect(stateManagementConfig.enabled).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/global_state.js b/src/legacy/ui/public/state_management/__tests__/global_state.js deleted file mode 100644 index e9dae5880a8d1..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/global_state.js +++ /dev/null @@ -1,52 +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 expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import '../global_state'; - -describe('State Management', function () { - let $location; - let state; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$location_, globalState) { - $location = _$location_; - state = globalState; - }) - ); - - describe('Global State', function () { - it('should use previous state when not in URL', function () { - // set satte via URL - $location.search({ _g: '(foo:(bar:baz))' }); - state.fetch(); - expect(state.toObject()).to.eql({ foo: { bar: 'baz' } }); - - $location.search({ _g: '(fizz:buzz)' }); - state.fetch(); - expect(state.toObject()).to.eql({ fizz: 'buzz' }); - - $location.search({}); - state.fetch(); - expect(state.toObject()).to.eql({ fizz: 'buzz' }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js deleted file mode 100644 index b6c705e814509..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/state.js +++ /dev/null @@ -1,393 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { encode as encodeRison } from 'rison-node'; -import uiRoutes from 'ui/routes'; -import '../../private'; -import { toastNotifications } from '../../notify'; -import * as FatalErrorNS from '../../notify/fatal_error'; -import { StateProvider } from '../state'; -import { - unhashQuery, - createStateHash, - isStateHash, - HashedItemStore, -} from '../../../../../plugins/kibana_utils/public'; -import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; -import { EventsProvider } from '../../events'; - -describe('State Management', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - uiRoutes.enable(); - - describe('Enabled', () => { - let $rootScope; - let $location; - let Events; - let setup; - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private, config) { - const State = Private(StateProvider); - $location = _$location_; - $rootScope = _$rootScope_; - Events = Private(EventsProvider); - - setup = (opts) => { - const { param, initial, storeInHash } = opts || {}; - sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(!!storeInHash); - const store = new StubBrowserStorage(); - const hashedItemStore = new HashedItemStore(store); - const state = new State(param, initial, hashedItemStore); - - const getUnhashedSearch = () => unhashQuery($location.search()); - - return { store, hashedItemStore, state, getUnhashedSearch }; - }; - }) - ); - - describe('Provider', () => { - it('should reset the state to the defaults', () => { - const { state, getUnhashedSearch } = setup({ initial: { message: ['test'] } }); - state.reset(); - const search = getUnhashedSearch(state); - expect(search).to.have.property('_s'); - expect(search._s).to.equal('(message:!(test))'); - expect(state.message).to.eql(['test']); - }); - - it('should apply the defaults upon initialization', () => { - const { state } = setup({ initial: { message: 'test' } }); - expect(state).to.have.property('message', 'test'); - }); - - it('should inherit from Events', () => { - const { state } = setup(); - expect(state).to.be.an(Events); - }); - - it('should emit an event if reset with changes', (done) => { - const { state } = setup({ initial: { message: ['test'] } }); - state.on('reset_with_changes', (keys) => { - expect(keys).to.eql(['message']); - done(); - }); - state.save(); - state.message = 'foo'; - state.reset(); - $rootScope.$apply(); - }); - - it('should not emit an event if reset without changes', () => { - const { state } = setup({ initial: { message: 'test' } }); - state.on('reset_with_changes', () => { - expect().fail(); - }); - state.save(); - state.message = 'test'; - state.reset(); - $rootScope.$apply(); - }); - }); - - describe('Search', () => { - it('should save to $location.search()', () => { - const { state, getUnhashedSearch } = setup({ initial: { test: 'foo' } }); - state.save(); - const search = getUnhashedSearch(state); - expect(search).to.have.property('_s'); - expect(search._s).to.equal('(test:foo)'); - }); - - it('should emit an event if changes are saved', (done) => { - const { state, getUnhashedSearch } = setup(); - state.on('save_with_changes', (keys) => { - expect(keys).to.eql(['test']); - done(); - }); - state.test = 'foo'; - state.save(); - getUnhashedSearch(state); - $rootScope.$apply(); - }); - }); - - describe('Fetch', () => { - it('should emit an event if changes are fetched', (done) => { - const { state } = setup(); - state.on('fetch_with_changes', (keys) => { - expect(keys).to.eql(['foo']); - done(); - }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - $rootScope.$apply(); - }); - - it('should have events that attach to scope', (done) => { - const { state } = setup(); - state.on('test', (message) => { - expect(message).to.equal('foo'); - done(); - }); - state.emit('test', 'foo'); - $rootScope.$apply(); - }); - - it('should fire listeners for #onUpdate() on #fetch()', (done) => { - const { state } = setup(); - state.on('fetch_with_changes', (keys) => { - expect(keys).to.eql(['foo']); - done(); - }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - $rootScope.$apply(); - }); - - it('should apply defaults to fetches', () => { - const { state } = setup({ initial: { message: 'test' } }); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - expect(state).to.have.property('foo', 'bar'); - expect(state).to.have.property('message', 'test'); - }); - - it('should call fetch when $routeUpdate is fired on $rootScope', () => { - const { state } = setup(); - const spy = sinon.spy(state, 'fetch'); - $rootScope.$emit('$routeUpdate', 'test'); - sinon.assert.calledOnce(spy); - }); - - it('should clear state when missing form URL', () => { - let stateObj; - const { state } = setup(); - - // set state via URL - $location.search({ _s: '(foo:(bar:baz))' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({ foo: { bar: 'baz' } }); - - // ensure changing URL changes state - $location.search({ _s: '(one:two)' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({ one: 'two' }); - - // remove search, state should be empty - $location.search({}); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - }); - - it('should clear state when it is invalid', () => { - let stateObj; - const { state } = setup(); - - $location.search({ _s: '' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - - $location.search({ _s: '!n' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - - $location.search({ _s: 'alert(1)' }); - state.fetch(); - stateObj = state.toObject(); - expect(stateObj).to.eql({}); - }); - - it('does not replace the state value on read', () => { - const { state } = setup(); - sinon.stub($location, 'search').callsFake((newSearch) => { - if (newSearch) { - return $location; - } else { - return { - [state.getQueryParamName()]: '(a:1)', - }; - } - }); - const replaceStub = sinon.stub($location, 'replace').returns($location); - - state.fetch(); - sinon.assert.notCalled(replaceStub); - }); - }); - - describe('Hashing', () => { - it('stores state values in a hashedItemStore, writing the hash to the url', () => { - const { state, hashedItemStore } = setup({ storeInHash: true }); - state.foo = 'bar'; - state.save(); - const urlVal = $location.search()[state.getQueryParamName()]; - - expect(isStateHash(urlVal)).to.be(true); - expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify({ foo: 'bar' })); - }); - - it('should replace rison in the URL with a hash', () => { - const { state, hashedItemStore } = setup({ storeInHash: true }); - const obj = { foo: { bar: 'baz' } }; - const rison = encodeRison(obj); - - $location.search({ _s: rison }); - state.fetch(); - - const urlVal = $location.search()._s; - expect(urlVal).to.not.be(rison); - expect(isStateHash(urlVal)).to.be(true); - expect(hashedItemStore.getItem(urlVal)).to.eql(JSON.stringify(obj)); - }); - - describe('error handling', () => { - it('notifies the user when a hash value does not map to a stored value', () => { - // Ideally, state.js shouldn't be tightly coupled to toastNotifications. Instead, it - // should notify its consumer of this error state and the consumer should be responsible - // for notifying the user of the error. This test verifies the side effect of the error - // until we can remove this coupling. - - // Clear existing toasts. - toastNotifications.list.splice(0); - - const { state } = setup({ storeInHash: true }); - const search = $location.search(); - const badHash = createStateHash('{"a": "b"}', () => null); - - search[state.getQueryParamName()] = badHash; - $location.search(search); - - expect(toastNotifications.list).to.have.length(0); - state.fetch(); - expect(toastNotifications.list).to.have.length(1); - expect(toastNotifications.list[0].title).to.match(/use the share functionality/i); - }); - - it.skip('triggers fatal error linking to github when setting item fails', () => { - // NOTE: this test needs to be written in jest and removed from the browser ones - // More info could be read in the opened issue: - // https://github.com/elastic/kibana/issues/22751 - const { state, hashedItemStore } = setup({ storeInHash: true }); - const fatalErrorStub = sandbox.stub(); - Object.defineProperty(FatalErrorNS, 'fatalError', { - writable: true, - value: fatalErrorStub, - }); - - sandbox.stub(hashedItemStore, 'setItem').returns(false); - state.toQueryParam(); - sinon.assert.calledOnce(fatalErrorStub); - sinon.assert.calledWith( - fatalErrorStub, - sinon.match((error) => error instanceof Error && error.message.includes('github.com')) - ); - }); - - it('translateHashToRison should gracefully fallback if parameter can not be parsed', () => { - const { state, hashedItemStore } = setup({ storeInHash: false }); - - expect(state.translateHashToRison('(a:b)')).to.be('(a:b)'); - expect(state.translateHashToRison('')).to.be(''); - - const existingHash = createStateHash('{"a": "b"}', () => null); - hashedItemStore.setItem(existingHash, '{"a": "b"}'); - - const nonExistingHash = createStateHash('{"b": "c"}', () => null); - - expect(state.translateHashToRison(existingHash)).to.be('(a:b)'); - expect(state.translateHashToRison(nonExistingHash)).to.be('!n'); - }); - }); - }); - }); - - describe('Disabled with persisted state', () => { - let state; - let $location; - let $rootScope; - const stateParam = '_config_test'; - - const getLocationState = () => { - const search = $location.search(); - return search[stateParam]; - }; - - beforeEach( - ngMock.module('kibana', function (stateManagementConfigProvider) { - stateManagementConfigProvider.disable(); - }) - ); - beforeEach( - ngMock.inject(function (_$rootScope_, _$location_, Private, config) { - const State = Private(StateProvider); - $location = _$location_; - $rootScope = _$rootScope_; - - sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(false); - - class MockPersistedState extends State { - _persistAcrossApps = true; - } - - MockPersistedState.prototype._persistAcrossApps = true; - - state = new MockPersistedState(stateParam); - }) - ); - - describe('changing state', () => { - const methods = ['save', 'replace', 'reset']; - - methods.forEach((method) => { - it(`${method} should not change the URL`, () => { - $location.search({ _s: '(foo:bar)' }); - state[method](); - $rootScope.$apply(); - expect(getLocationState()).to.be(undefined); - }); - }); - }); - - describe('reading state', () => { - it('should not change the URL', () => { - const saveSpy = sinon.spy(state, 'save'); - $location.search({ _s: '(foo:bar)' }); - state.fetch(); - $rootScope.$apply(); - sinon.assert.notCalled(saveSpy); - expect(getLocationState()).to.be(undefined); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js b/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js deleted file mode 100644 index dc00d4e05e82f..0000000000000 --- a/src/legacy/ui/public/state_management/__tests__/state_monitor_factory.js +++ /dev/null @@ -1,265 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { EventEmitter } from 'events'; -import { cloneDeep } from 'lodash'; -import { stateMonitorFactory } from '../state_monitor_factory'; - -describe('stateMonitorFactory', function () { - const noop = () => {}; - const eventTypes = ['save_with_changes', 'reset_with_changes', 'fetch_with_changes']; - - let mockState; - - function setState(mockState, obj, emit = true) { - mockState.toJSON = () => cloneDeep(obj); - if (emit) mockState.emit(eventTypes[0]); - } - - function createMockState(state = {}) { - const mockState = new EventEmitter(); - setState(mockState, state, false); - return mockState; - } - - beforeEach(() => { - mockState = createMockState({}); - }); - - it('should have a create method', function () { - expect(stateMonitorFactory).to.have.property('create'); - expect(stateMonitorFactory.create).to.be.a('function'); - }); - - describe('factory creation', function () { - it('should not call onChange with only the state', function () { - const monitor = stateMonitorFactory.create(mockState); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should not call onChange with matching defaultState', function () { - const monitor = stateMonitorFactory.create(mockState, {}); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should call onChange with differing defaultState', function () { - const monitor = stateMonitorFactory.create(mockState, { test: true }); - const changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.calledOnce(changeStub); - }); - }); - - describe('instance', function () { - let monitor; - - beforeEach(() => { - monitor = stateMonitorFactory.create(mockState); - }); - - describe('onChange', function () { - it('should throw if not given a handler function', function () { - const fn = () => monitor.onChange('not a function'); - expect(fn).to.throwException(/must be a function/); - }); - - eventTypes.forEach((eventType) => { - describe(`when ${eventType} is emitted`, function () { - let handlerFn; - - beforeEach(() => { - handlerFn = sinon.stub(); - monitor.onChange(handlerFn); - sinon.assert.notCalled(handlerFn); - }); - - it('should get called', function () { - mockState.emit(eventType); - sinon.assert.calledOnce(handlerFn); - }); - - it('should be given the state status', function () { - mockState.emit(eventType); - const args = handlerFn.firstCall.args; - expect(args[0]).to.be.an('object'); - }); - - it('should be given the event type', function () { - mockState.emit(eventType); - const args = handlerFn.firstCall.args; - expect(args[1]).to.equal(eventType); - }); - - it('should be given the changed keys', function () { - const keys = ['one', 'two', 'three']; - mockState.emit(eventType, keys); - const args = handlerFn.firstCall.args; - expect(args[2]).to.equal(keys); - }); - }); - }); - }); - - describe('ignoreProps', function () { - it('should not set status to dirty when ignored properties change', function () { - let status; - const mockState = createMockState({ messages: { world: 'hello', foo: 'bar' } }); - const monitor = stateMonitorFactory.create(mockState); - const changeStub = sinon.stub(); - monitor.ignoreProps('messages.world'); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - - // update the ignored state prop - setState(mockState, { messages: { world: 'howdy', foo: 'bar' } }); - sinon.assert.calledOnce(changeStub); - status = changeStub.firstCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - - // update a prop that is not ignored - setState(mockState, { messages: { world: 'howdy', foo: 'baz' } }); - sinon.assert.calledTwice(changeStub); - status = changeStub.secondCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - }); - - describe('setInitialState', function () { - let changeStub; - - beforeEach(() => { - changeStub = sinon.stub(); - monitor.onChange(changeStub); - sinon.assert.notCalled(changeStub); - }); - - it('should throw if no state is provided', function () { - const fn = () => monitor.setInitialState(); - expect(fn).to.throwException(/must be an object/); - }); - - it('should throw if given the wrong type', function () { - const fn = () => monitor.setInitialState([]); - expect(fn).to.throwException(/must be an object/); - }); - - it('should trigger the onChange handler', function () { - monitor.setInitialState({ new: 'state' }); - sinon.assert.calledOnce(changeStub); - }); - - it('should change the status with differing state', function () { - monitor.setInitialState({ new: 'state' }); - sinon.assert.calledOnce(changeStub); - - const status = changeStub.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - - it('should not trigger the onChange handler without state change', function () { - monitor.setInitialState(cloneDeep(mockState.toJSON())); - sinon.assert.notCalled(changeStub); - }); - }); - - describe('status object', function () { - let handlerFn; - - beforeEach(() => { - handlerFn = sinon.stub(); - monitor.onChange(handlerFn); - }); - - it('should be clean by default', function () { - mockState.emit(eventTypes[0]); - const status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - }); - - it('should be dirty when state changes', function () { - setState(mockState, { message: 'i am dirty now' }); - const status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - }); - - it('should be clean when state is reset', function () { - const defaultState = { message: 'i am the original state' }; - const handlerFn = sinon.stub(); - - let status; - - // initial state and monitor setup - const mockState = createMockState(defaultState); - const monitor = stateMonitorFactory.create(mockState); - monitor.onChange(handlerFn); - sinon.assert.notCalled(handlerFn); - - // change the state and emit an event - setState(mockState, { message: 'i am dirty now' }); - sinon.assert.calledOnce(handlerFn); - status = handlerFn.firstCall.args[0]; - expect(status).to.have.property('clean', false); - expect(status).to.have.property('dirty', true); - - // reset the state and emit an event - setState(mockState, defaultState); - sinon.assert.calledTwice(handlerFn); - status = handlerFn.secondCall.args[0]; - expect(status).to.have.property('clean', true); - expect(status).to.have.property('dirty', false); - }); - }); - - describe('destroy', function () { - let stateSpy; - - beforeEach(() => { - stateSpy = sinon.spy(mockState, 'off'); - sinon.assert.notCalled(stateSpy); - }); - - it('should remove the listeners', function () { - monitor.onChange(noop); - monitor.destroy(); - sinon.assert.callCount(stateSpy, eventTypes.length); - eventTypes.forEach((eventType) => { - sinon.assert.calledWith(stateSpy, eventType); - }); - }); - - it('should stop the instance from being used any more', function () { - monitor.onChange(noop); - monitor.destroy(); - const fn = () => monitor.onChange(noop); - expect(fn).to.throwException(/has been destroyed/); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/state_management/app_state.d.ts b/src/legacy/ui/public/state_management/app_state.d.ts deleted file mode 100644 index ddd0b90710766..0000000000000 --- a/src/legacy/ui/public/state_management/app_state.d.ts +++ /dev/null @@ -1,26 +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 './state'; - -export class AppState extends State {} - -export type AppStateClass< - TAppState extends AppState = AppState, - TDefaults = { [key: string]: any } -> = new (defaults: TDefaults) => TAppState; diff --git a/src/legacy/ui/public/state_management/app_state.js b/src/legacy/ui/public/state_management/app_state.js deleted file mode 100644 index ec680d163b9da..0000000000000 --- a/src/legacy/ui/public/state_management/app_state.js +++ /dev/null @@ -1,137 +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. - */ - -/** - * @name AppState - * - * @extends State - * - * @description Inherits State, which inherits Events. This class seems to be - * concerned with mapping "props" to PersistedState instances, and surfacing the - * ability to destroy those mappings. - */ - -import { uiModules } from '../modules'; -import { StateProvider } from './state'; -import { PersistedState } from '../../../../plugins/visualizations/public'; -import { createLegacyClass } from '../utils/legacy_class'; - -const urlParam = '_a'; - -export function AppStateProvider(Private, $location) { - const State = Private(StateProvider); - - let persistedStates; - let eventUnsubscribers; - - createLegacyClass(AppState).inherits(State); - function AppState(defaults) { - // Initialize persistedStates. This object maps "prop" names to - // PersistedState instances. These are used to make properties "stateful". - persistedStates = {}; - - // Initialize eventUnsubscribers. These will be called in `destroy`, to - // remove handlers for the 'change' and 'fetch_with_changes' events which - // are dispatched via the rootScope. - eventUnsubscribers = []; - - AppState.Super.call(this, urlParam, defaults); - AppState.getAppState._set(this); - } - - // if the url param is missing, write it back - AppState.prototype._persistAcrossApps = false; - - AppState.prototype.destroy = function () { - AppState.Super.prototype.destroy.call(this); - AppState.getAppState._set(null); - - eventUnsubscribers.forEach((listener) => listener()); - }; - - /** - * @returns PersistedState instance. - */ - AppState.prototype.makeStateful = function (prop) { - if (persistedStates[prop]) return persistedStates[prop]; - const self = this; - - // set up the ui state - persistedStates[prop] = new PersistedState(); - - // update the app state when the stateful instance changes - const updateOnChange = function () { - const replaceState = false; // TODO: debouncing logic - self[prop] = persistedStates[prop].getChanges(); - // Save state to the URL. - self.save(replaceState); - }; - const handlerOnChange = (method) => persistedStates[prop][method]('change', updateOnChange); - handlerOnChange('on'); - eventUnsubscribers.push(() => handlerOnChange('off')); - - // update the stateful object when the app state changes - const persistOnChange = function (changes) { - if (!changes) return; - - if (changes.indexOf(prop) !== -1) { - persistedStates[prop].set(self[prop]); - } - }; - const handlePersist = (method) => this[method]('fetch_with_changes', persistOnChange); - handlePersist('on'); - eventUnsubscribers.push(() => handlePersist('off')); - - // if the thing we're making stateful has an appState value, write to persisted state - if (self[prop]) persistedStates[prop].setSilent(self[prop]); - - return persistedStates[prop]; - }; - - AppState.getAppState = (function () { - let currentAppState; - - function get() { - return currentAppState; - } - - // Checks to see if the appState might already exist, even if it hasn't been newed up - get.previouslyStored = function () { - const search = $location.search(); - return search[urlParam] ? true : false; - }; - - get._set = function (current) { - currentAppState = current; - }; - - return get; - })(); - - return AppState; -} - -uiModules - .get('kibana/global_state') - .factory('AppState', function (Private) { - return Private(AppStateProvider); - }) - .service('getAppState', function (Private) { - return Private(AppStateProvider).getAppState; - }); diff --git a/src/legacy/ui/public/state_management/config_provider.js b/src/legacy/ui/public/state_management/config_provider.js deleted file mode 100644 index 68de449a57a56..0000000000000 --- a/src/legacy/ui/public/state_management/config_provider.js +++ /dev/null @@ -1,48 +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. - */ - -/** - * @name stateManagementConfig - * - * @description Allows apps to configure state management - */ - -import { uiModules } from '../modules'; - -export class StateManagementConfigProvider { - _enabled = true; - - $get(/* inject stuff */) { - return { - enabled: this._enabled, - }; - } - - disable() { - this._enabled = false; - } - - enable() { - this._enabled = true; - } -} - -uiModules - .get('kibana/state_management') - .provider('stateManagementConfig', StateManagementConfigProvider); diff --git a/src/legacy/ui/public/state_management/global_state.d.ts b/src/legacy/ui/public/state_management/global_state.d.ts deleted file mode 100644 index 66a85d88956c7..0000000000000 --- a/src/legacy/ui/public/state_management/global_state.d.ts +++ /dev/null @@ -1,22 +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 './state'; - -export type GlobalState = State; diff --git a/src/legacy/ui/public/state_management/global_state.js b/src/legacy/ui/public/state_management/global_state.js deleted file mode 100644 index 0e8dfe40d5950..0000000000000 --- a/src/legacy/ui/public/state_management/global_state.js +++ /dev/null @@ -1,42 +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 { StateProvider } from './state'; -import { uiModules } from '../modules'; -import { createLegacyClass } from '../utils/legacy_class'; - -const module = uiModules.get('kibana/global_state'); - -export function GlobalStateProvider(Private) { - const State = Private(StateProvider); - - createLegacyClass(GlobalState).inherits(State); - function GlobalState(defaults) { - GlobalState.Super.call(this, '_g', defaults); - } - - // if the url param is missing, write it back - GlobalState.prototype._persistAcrossApps = true; - - return new GlobalState(); -} - -module.service('globalState', function (Private) { - return Private(GlobalStateProvider); -}); diff --git a/src/legacy/ui/public/state_management/state.d.ts b/src/legacy/ui/public/state_management/state.d.ts deleted file mode 100644 index b31862b681f55..0000000000000 --- a/src/legacy/ui/public/state_management/state.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export class State { - [key: string]: any; - translateHashToRison: (stateHashOrRison: string | string[]) => string | string[]; - getQueryParamName: () => string; -} diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js deleted file mode 100644 index d91834adb4a79..0000000000000 --- a/src/legacy/ui/public/state_management/state.js +++ /dev/null @@ -1,359 +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. - */ - -/** - * @name State - * - * @extends Events - * - * @description Persists generic "state" to and reads it from the URL. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; -import rison from 'rison-node'; -import { EventsProvider } from '../events'; -import { fatalError, toastNotifications } from '../notify'; -import './config_provider'; -import { createLegacyClass } from '../utils/legacy_class'; -import { - hashedItemStore, - isStateHash, - createStateHash, - applyDiff, -} from '../../../../plugins/kibana_utils/public'; - -export function StateProvider( - Private, - $rootScope, - $location, - stateManagementConfig, - config, - kbnUrl, - $injector -) { - const Events = Private(EventsProvider); - - const isDummyRoute = () => - $injector.has('$route') && - $injector.get('$route').current && - $injector.get('$route').current.outerAngularWrapperRoute; - - createLegacyClass(State).inherits(Events); - function State(urlParam, defaults, _hashedItemStore = hashedItemStore) { - State.Super.call(this); - - this.setDefaults(defaults); - this._urlParam = urlParam || '_s'; - this._hashedItemStore = _hashedItemStore; - - // When the URL updates we need to fetch the values from the URL - this._cleanUpListeners = [ - // partial route update, no app reload - $rootScope.$on('$routeUpdate', () => { - this.fetch(); - }), - - // beginning of full route update, new app will be initialized before - // $routeChangeSuccess or $routeChangeError - $rootScope.$on('$routeChangeStart', () => { - if (!this._persistAcrossApps) { - this.destroy(); - } - }), - - $rootScope.$on('$routeChangeSuccess', () => { - if (this._persistAcrossApps) { - this.fetch(); - } - }), - ]; - - // Initialize the State with fetch - this.fetch(); - } - - State.prototype._readFromURL = function () { - const search = $location.search(); - const urlVal = search[this._urlParam]; - - if (!urlVal) { - return null; - } - - if (isStateHash(urlVal)) { - return this._parseStateHash(urlVal); - } - - let risonEncoded; - let unableToParse; - try { - risonEncoded = rison.decode(urlVal); - } catch (e) { - unableToParse = true; - } - - if (unableToParse) { - toastNotifications.addDanger( - i18n.translate('common.ui.stateManagement.unableToParseUrlErrorMessage', { - defaultMessage: 'Unable to parse URL', - }) - ); - search[this._urlParam] = this.toQueryParam(this._defaults); - $location.search(search).replace(); - } - - if (!risonEncoded) { - return null; - } - - if (this.isHashingEnabled()) { - // RISON can find its way into the URL any number of ways, including the navbar links or - // shared urls with the entire state embedded. These values need to be translated into - // hashes and replaced in the browser history when state-hashing is enabled - search[this._urlParam] = this.toQueryParam(risonEncoded); - $location.search(search).replace(); - } - - return risonEncoded; - }; - - /** - * Fetches the state from the url - * @returns {void} - */ - State.prototype.fetch = function () { - if (!stateManagementConfig.enabled) { - return; - } - - let stash = this._readFromURL(); - - // nothing to read from the url? save if ordered to persist, but only if it's not on a wrapper route - if (stash === null) { - if (this._persistAcrossApps) { - return this.save(); - } else { - stash = {}; - } - } - - _.defaults(stash, this._defaults); - // apply diff to state from stash, will change state in place via side effect - const diffResults = applyDiff(this, stash); - - if (!isDummyRoute() && diffResults.keys.length) { - this.emit('fetch_with_changes', diffResults.keys); - } - }; - - /** - * Saves the state to the url - * @returns {void} - */ - State.prototype.save = function (replace) { - if (!stateManagementConfig.enabled) { - return; - } - - if (isDummyRoute()) { - return; - } - - let stash = this._readFromURL(); - const state = this.toObject(); - replace = replace || false; - - if (!stash) { - replace = true; - stash = {}; - } - - // apply diff to state from stash, will change state in place via side effect - const diffResults = applyDiff(stash, _.defaults({}, state, this._defaults)); - - if (diffResults.keys.length) { - this.emit('save_with_changes', diffResults.keys); - } - - // persist the state in the URL - const search = $location.search(); - search[this._urlParam] = this.toQueryParam(state); - if (replace) { - $location.search(search).replace(); - } else { - $location.search(search); - } - }; - - /** - * Calls save with a forced replace - * @returns {void} - */ - State.prototype.replace = function () { - if (!stateManagementConfig.enabled) { - return; - } - - this.save(true); - }; - - /** - * Resets the state to the defaults - * - * @returns {void} - */ - State.prototype.reset = function () { - if (!stateManagementConfig.enabled) { - return; - } - - kbnUrl.removeParam(this.getQueryParamName()); - // apply diff to attributes from defaults, this is side effecting so - // it will change the state in place. - const diffResults = applyDiff(this, this._defaults); - if (diffResults.keys.length) { - this.emit('reset_with_changes', diffResults.keys); - } - this.save(); - }; - - /** - * Cleans up the state object - * @returns {void} - */ - State.prototype.destroy = function () { - this.off(); // removes all listeners - - // Removes the $routeUpdate listener - this._cleanUpListeners.forEach((listener) => listener(this)); - }; - - State.prototype.setDefaults = function (defaults) { - this._defaults = defaults || {}; - }; - - /** - * Parse the state hash to it's unserialized value. Hashes are restored - * to their pre-hashed state. - * - * @param {string} stateHash - state hash value from the query string. - * @return {any} - the stored value, or null if hash does not resolve. - */ - State.prototype._parseStateHash = function (stateHash) { - const json = this._hashedItemStore.getItem(stateHash); - if (json === null) { - toastNotifications.addDanger( - i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { - defaultMessage: - 'Unable to completely restore the URL, be sure to use the share functionality.', - }) - ); - } - - return JSON.parse(json); - }; - - /** - * Lookup the value for a hash and return it's value in rison format or just - * return passed argument if it's not recognized as state hash. - * - * @param {string} stateHashOrRison - either state hash value or rison string. - * @return {string} rison - */ - State.prototype.translateHashToRison = function (stateHashOrRison) { - if (isStateHash(stateHashOrRison)) { - return rison.encode(this._parseStateHash(stateHashOrRison)); - } - - return stateHashOrRison; - }; - - State.prototype.isHashingEnabled = function () { - return !!config.get('state:storeInSessionStorage'); - }; - - /** - * Produce the hash version of the state in it's current position - * - * @return {string} - */ - State.prototype.toQueryParam = function (state = this.toObject()) { - if (!this.isHashingEnabled()) { - return rison.encode(state); - } - - // We need to strip out Angular-specific properties. - const json = angular.toJson(state); - const hash = createStateHash(json); - const isItemSet = this._hashedItemStore.setItem(hash, json); - - if (isItemSet) { - return hash; - } - - // If we ran out of space trying to persist the state, notify the user. - const message = i18n.translate( - 'common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage', - { - defaultMessage: - 'Kibana is unable to store history items in your session ' + - `because it is full and there don't seem to be items any items safe ` + - 'to delete.\n\n' + - 'This can usually be fixed by moving to a fresh tab, but could ' + - 'be caused by a larger issue. If you are seeing this message regularly, ' + - 'please file an issue at {gitHubIssuesUrl}.', - values: { gitHubIssuesUrl: 'https://github.com/elastic/kibana/issues' }, - } - ); - fatalError(new Error(message)); - }; - - /** - * Get the query string parameter name where this state writes and reads - * @return {string} - */ - State.prototype.getQueryParamName = function () { - return this._urlParam; - }; - - /** - * Returns an object with each property name and value corresponding to the entries in this collection - * excluding fields started from '$', '_' and all methods - * - * @return {object} - */ - State.prototype.toObject = function () { - return _.omitBy(this, (value, key) => { - return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); - }); - }; - - /** Alias for method 'toObject' - * - * @obsolete Please use 'toObject' method instead - * @return {object} - */ - State.prototype.toJSON = function () { - return this.toObject(); - }; - - return State; -} diff --git a/src/legacy/ui/public/state_management/state_monitor_factory.ts b/src/legacy/ui/public/state_management/state_monitor_factory.ts deleted file mode 100644 index 968ececfe3be5..0000000000000 --- a/src/legacy/ui/public/state_management/state_monitor_factory.ts +++ /dev/null @@ -1,157 +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 { set } from '@elastic/safer-lodash-set'; -import { cloneDeep, isEqual, isPlainObject } from 'lodash'; -import { State } from './state'; - -export const stateMonitorFactory = { - create: ( - state: State, - customInitialState: TStateDefault - ) => stateMonitor(state, customInitialState), -}; - -interface StateStatus { - clean: boolean; - dirty: boolean; -} - -export interface StateMonitor { - setInitialState: (innerCustomInitialState: TStateDefault) => void; - ignoreProps: (props: string[] | string) => void; - onChange: (callback: ChangeHandlerFn) => StateMonitor; - destroy: () => void; -} - -type ChangeHandlerFn = (status: StateStatus, type: string | null, keys: string[]) => void; - -function stateMonitor( - state: State, - customInitialState: TStateDefault -): StateMonitor { - let destroyed = false; - let ignoredProps: string[] = []; - let changeHandlers: ChangeHandlerFn[] | undefined = []; - let initialState: TStateDefault; - - setInitialState(customInitialState); - - function setInitialState(innerCustomInitialState: TStateDefault) { - // state.toJSON returns a reference, clone so we can mutate it safely - initialState = cloneDeep(innerCustomInitialState) || cloneDeep(state.toJSON()); - } - - function removeIgnoredProps(innerState: TStateDefault) { - ignoredProps.forEach((path) => { - set(innerState, path, true); - }); - return innerState; - } - - function getStatus(): StateStatus { - // state.toJSON returns a reference, clone so we can mutate it safely - const currentState = removeIgnoredProps(cloneDeep(state.toJSON())); - const isClean = isEqual(currentState, initialState); - - return { - clean: isClean, - dirty: !isClean, - }; - } - - function dispatchChange(type: string | null = null, keys: string[] = []) { - const status = getStatus(); - if (!changeHandlers) { - throw new Error('Change handlers is undefined, this object has been destroyed'); - } - changeHandlers.forEach((changeHandler) => { - changeHandler(status, type, keys); - }); - } - - function dispatchFetch(keys: string[]) { - dispatchChange('fetch_with_changes', keys); - } - - function dispatchSave(keys: string[]) { - dispatchChange('save_with_changes', keys); - } - - function dispatchReset(keys: string[]) { - dispatchChange('reset_with_changes', keys); - } - - return { - setInitialState(innerCustomInitialState: TStateDefault) { - if (!isPlainObject(innerCustomInitialState)) { - throw new TypeError('The default state must be an object'); - } - - // check the current status - const previousStatus = getStatus(); - - // update the initialState and apply ignoredProps - setInitialState(innerCustomInitialState); - removeIgnoredProps(initialState); - - // fire the change handler if the status has changed - if (!isEqual(previousStatus, getStatus())) { - dispatchChange(); - } - }, - - ignoreProps(props: string[] | string) { - ignoredProps = ignoredProps.concat(props); - removeIgnoredProps(initialState); - return this; - }, - - onChange(callback: ChangeHandlerFn) { - if (destroyed || !changeHandlers) { - throw new Error('Monitor has been destroyed'); - } - if (typeof callback !== 'function') { - throw new Error('onChange handler must be a function'); - } - - changeHandlers.push(callback); - - // Listen for state events. - state.on('fetch_with_changes', dispatchFetch); - state.on('save_with_changes', dispatchSave); - state.on('reset_with_changes', dispatchReset); - - // if the state is already dirty, fire the change handler immediately - const status = getStatus(); - if (status.dirty) { - dispatchChange(); - } - - return this; - }, - - destroy() { - destroyed = true; - changeHandlers = undefined; - state.off('fetch_with_changes', dispatchFetch); - state.off('save_with_changes', dispatchSave); - state.off('reset_with_changes', dispatchReset); - }, - }; -} diff --git a/src/legacy/ui/public/system_api/__tests__/system_api.js b/src/legacy/ui/public/system_api/__tests__/system_api.js deleted file mode 100644 index 816024f13f8b2..0000000000000 --- a/src/legacy/ui/public/system_api/__tests__/system_api.js +++ /dev/null @@ -1,68 +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 expect from '@kbn/expect'; -import { - addSystemApiHeader, - isSystemApiRequest, -} from '../../../../../plugins/kibana_legacy/public'; - -describe('system_api', () => { - describe('#addSystemApiHeader', () => { - it('adds the correct system API header', () => { - const headers = { - 'kbn-version': '4.6.0', - }; - const newHeaders = addSystemApiHeader(headers); - - expect(newHeaders).to.have.property('kbn-system-request'); - expect(newHeaders['kbn-system-request']).to.be(true); - - expect(newHeaders).to.have.property('kbn-version'); - expect(newHeaders['kbn-version']).to.be('4.6.0'); - }); - }); - - describe('#isSystemApiRequest', () => { - it('returns true for a system HTTP request', () => { - const mockRequest = { - headers: { - 'kbn-system-request': true, - }, - }; - expect(isSystemApiRequest(mockRequest)).to.be(true); - }); - - it('returns true for a legacy system API HTTP request', () => { - const mockRequest = { - headers: { - 'kbn-system-api': true, - }, - }; - expect(isSystemApiRequest(mockRequest)).to.be(true); - }); - - it('returns false for a non-system API HTTP request', () => { - const mockRequest = { - headers: {}, - }; - expect(isSystemApiRequest(mockRequest)).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/system_api/index.js b/src/legacy/ui/public/system_api/index.js deleted file mode 100644 index 6361c0ea6c69a..0000000000000 --- a/src/legacy/ui/public/system_api/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { addSystemApiHeader, isSystemApiRequest } from '../../../../plugins/kibana_legacy/public'; diff --git a/src/legacy/ui/public/timefilter/index.ts b/src/legacy/ui/public/timefilter/index.ts deleted file mode 100644 index 83795c73112be..0000000000000 --- a/src/legacy/ui/public/timefilter/index.ts +++ /dev/null @@ -1,39 +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 uiRoutes from 'ui/routes'; -import { npStart } from 'ui/new_platform'; - -import { TimefilterContract, TimeHistoryContract } from '../../../../plugins/data/public'; -import { registerTimefilterWithGlobalState } from './setup_router'; - -export { - getTime, - InputTimeRange, - TimeHistoryContract, - TimefilterContract, -} from '../../../../plugins/data/public'; -export type Timefilter = TimefilterContract; -export type TimeHistory = TimeHistoryContract; -export const timeHistory = npStart.plugins.data.query.timefilter.history; -export const timefilter = npStart.plugins.data.query.timefilter.timefilter; - -uiRoutes.addSetupWork((globalState, $rootScope) => { - return registerTimefilterWithGlobalState(timefilter, globalState, $rootScope); -}); diff --git a/src/legacy/ui/public/timefilter/setup_router.test.js b/src/legacy/ui/public/timefilter/setup_router.test.js deleted file mode 100644 index 2ae9a053ae2db..0000000000000 --- a/src/legacy/ui/public/timefilter/setup_router.test.js +++ /dev/null @@ -1,54 +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 { registerTimefilterWithGlobalState } from './setup_router'; - -jest.mock('../../../../plugins/kibana_legacy/public', () => ({ - subscribeWithScope: jest.fn(), -})); - -describe('registerTimefilterWithGlobalState()', () => { - it('should always use iso8601 strings', async () => { - const setTime = jest.fn(); - const timefilter = { - setTime, - setRefreshInterval: jest.fn(), - getRefreshIntervalUpdate$: jest.fn(), - getTimeUpdate$: jest.fn(), - }; - - const globalState = { - time: { - from: '2017-09-07T20:12:04.011Z', - to: '2017-09-07T20:18:55.733Z', - }, - on: (eventName, callback) => { - callback(); - }, - }; - - const rootScope = { - $on: jest.fn(), - }; - - registerTimefilterWithGlobalState(timefilter, globalState, rootScope); - - expect(setTime.mock.calls.length).toBe(2); - expect(setTime.mock.calls[1][0]).toEqual(globalState.time); - }); -}); diff --git a/src/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts deleted file mode 100644 index 7c25c6aa3166e..0000000000000 --- a/src/legacy/ui/public/timefilter/setup_router.ts +++ /dev/null @@ -1,118 +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 _ from 'lodash'; -import { IScope } from 'angular'; -import moment from 'moment'; -import chrome from 'ui/chrome'; -import { Subscription } from 'rxjs'; -import { fatalError } from 'ui/notify/fatal_error'; -import { subscribeWithScope } from '../../../../plugins/kibana_legacy/public'; -import { - RefreshInterval, - TimeRange, - TimefilterContract, - UI_SETTINGS, -} from '../../../../plugins/data/public'; - -// TODO -// remove everything underneath once globalState is no longer an angular service -// and listener can be registered without angular. -function convertISO8601(stringTime: string): string { - const obj = moment(stringTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); - return obj.isValid() ? obj.toISOString() : stringTime; -} - -export function getTimefilterConfig() { - const settings = chrome.getUiSettingsClient(); - return { - timeDefaults: settings.get('timepicker:timeDefaults'), - refreshIntervalDefaults: settings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS), - }; -} - -export const registerTimefilterWithGlobalStateFactory = ( - timefilter: TimefilterContract, - globalState: any, - $rootScope: IScope -) => { - // settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account. - const config = getTimefilterConfig(); - timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults)); - timefilter.setRefreshInterval( - _.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults) - ); - - globalState.on('fetch_with_changes', () => { - // clone and default to {} in one - const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults); - const newRefreshInterval: RefreshInterval = _.defaults( - {}, - globalState.refreshInterval, - config.refreshIntervalDefaults - ); - - if (newTime) { - if (newTime.to) newTime.to = convertISO8601(newTime.to); - if (newTime.from) newTime.from = convertISO8601(newTime.from); - } - - timefilter.setTime(newTime); - timefilter.setRefreshInterval(newRefreshInterval); - }); - - const updateGlobalStateWithTime = () => { - globalState.time = timefilter.getTime(); - globalState.refreshInterval = timefilter.getRefreshInterval(); - globalState.save(); - }; - - const subscriptions = new Subscription(); - subscriptions.add( - subscribeWithScope( - $rootScope, - timefilter.getRefreshIntervalUpdate$(), - { - next: updateGlobalStateWithTime, - }, - fatalError - ) - ); - - subscriptions.add( - subscribeWithScope( - $rootScope, - timefilter.getTimeUpdate$(), - { - next: updateGlobalStateWithTime, - }, - fatalError - ) - ); - - $rootScope.$on('$destroy', () => { - subscriptions.unsubscribe(); - }); -}; - -// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter -// and require it to be executed to properly function. -// This function is exposed for applications that do not use uiRoutes like APM -// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter -export const registerTimefilterWithGlobalState = _.once(registerTimefilterWithGlobalStateFactory); diff --git a/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js b/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js deleted file mode 100644 index 965e8f4bc9f38..0000000000000 --- a/src/legacy/ui/public/url/__tests__/extract_app_path_and_id.js +++ /dev/null @@ -1,122 +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 expect from '@kbn/expect'; - -import { extractAppPathAndId } from '../extract_app_path_and_id'; - -describe('extractAppPathAndId', function () { - describe('from an absolute url with a base path', function () { - describe('with a base path', function () { - const basePath = '/gza'; - const absoluteUrl = 'http://www.test.com:5601/gza/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(absoluteUrl, basePath).appPath).to.be( - 'appPathIsHere?query=here' - ); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl, basePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('http://www.test.com:5601/gza/noapppath'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('without a base path', function () { - const absoluteUrl = 'http://www.test.com:5601/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(absoluteUrl).appPath).to.be('appPathIsHere?query=here'); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('http://www.test.com:5601/noapppath'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('when appPath is empty', function () { - const absoluteUrl = 'http://www.test.com:5601/app/appId'; - it('extracts app id', function () { - expect(extractAppPathAndId(absoluteUrl).appId).to.be('appId'); - }); - it('extracts empty appPath', function () { - expect(extractAppPathAndId(absoluteUrl).appPath).to.be(''); - }); - }); - }); - - describe('from a root relative url', function () { - describe('with a base path', function () { - const basePath = '/gza'; - const rootRelativePath = '/gza/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(rootRelativePath, basePath).appPath).to.be( - 'appPathIsHere?query=here' - ); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath, basePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('/gza/notformattedright'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('without a base path', function () { - const rootRelativePath = '/app/appId#appPathIsHere?query=here'; - it('extracts app path', function () { - expect(extractAppPathAndId(rootRelativePath).appPath).to.be('appPathIsHere?query=here'); - }); - - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath).appId).to.be('appId'); - }); - - it('returns an empty object when there is no app path', function () { - const appPathAndId = extractAppPathAndId('/notformattedright'); - expect(appPathAndId.appId).to.be(undefined); - expect(appPathAndId.appPath).to.be(undefined); - }); - }); - - describe('when appPath is empty', function () { - const rootRelativePath = '/app/appId'; - it('extracts app id', function () { - expect(extractAppPathAndId(rootRelativePath).appId).to.be('appId'); - }); - it('extracts empty appPath', function () { - expect(extractAppPathAndId(rootRelativePath).appPath).to.be(''); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js b/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js deleted file mode 100644 index 6ea199c3d22cc..0000000000000 --- a/src/legacy/ui/public/url/__tests__/kibana_parsed_url.js +++ /dev/null @@ -1,217 +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 expect from '@kbn/expect'; - -import { KibanaParsedUrl } from '../kibana_parsed_url'; - -describe('KibanaParsedUrl', function () { - it('getHashedAppPath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/hi', - appId: 'bye', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getHashedAppPath()).to.be('#visualize?hi=there&bye'); - }); - - it('getAppRootPath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/hi', - appId: 'appId', - appPath: 'dashboard?edit=123', - }); - expect(kibanaParsedUrl.getAppRootPath()).to.be('/app/appId#dashboard?edit=123'); - }); - - describe('when basePath is specified', function () { - it('getRootRelativePath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getRootRelativePath()).to.be('/base/app/appId#visualize?hi=there&bye'); - }); - - describe('getAbsolutePath', function () { - const protocol = 'http'; - const hostname = 'www.test.com'; - const port = '5601'; - - it('returns the absolute url when there is a port', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=there&bye', - hostname, - protocol, - port, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com:5601/base/app/appId#visualize?hi=there&bye' - ); - }); - - it('returns the absolute url when there are no query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/base/app/appId#visualize' - ); - }); - - it('returns the absolute url when the are query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - basePath: '/base', - appId: 'appId', - appPath: 'visualize?hi=bye&tata', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/base/app/appId#visualize?hi=bye&tata' - ); - }); - }); - }); - - describe('when basePath is not specified', function () { - it('getRootRelativePath', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=there&bye', - }); - expect(kibanaParsedUrl.getRootRelativePath()).to.be('/app/appId#visualize?hi=there&bye'); - }); - - describe('getAbsolutePath', function () { - const protocol = 'http'; - const hostname = 'www.test.com'; - const port = '5601'; - - it('returns the absolute url when there is a port', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=there&bye', - hostname, - protocol, - port, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com:5601/app/appId#visualize?hi=there&bye' - ); - }); - - it('returns the absolute url when there are no query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be('http://www.test.com/app/appId#visualize'); - }); - - it('returns the absolute url when there are query parameters', function () { - const kibanaParsedUrl = new KibanaParsedUrl({ - appId: 'appId', - appPath: 'visualize?hi=bye&tata', - hostname, - protocol, - }); - expect(kibanaParsedUrl.getAbsoluteUrl()).to.be( - 'http://www.test.com/app/appId#visualize?hi=bye&tata' - ); - }); - }); - }); - - describe('getGlobalState', function () { - const basePath = '/xyz'; - const appId = 'myApp'; - - it('returns an empty string when the KibanaParsedUrl is in an invalid state', function () { - const url = new KibanaParsedUrl({ basePath }); - expect(url.getGlobalState()).to.be(''); - }); - - it('returns an empty string when there is no global state', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi?notg=something' }); - expect(url.getGlobalState()).to.be(''); - }); - - it('returns the global state when it is the last parameter', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?notg=something&_g=(thisismyglobalstate)', - }); - expect(url.getGlobalState()).to.be('(thisismyglobalstate)'); - }); - - it('returns the global state when it is the first parameter', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?_g=(thisismyglobalstate)&hi=bye', - }); - expect(url.getGlobalState()).to.be('(thisismyglobalstate)'); - }); - }); - - describe('setGlobalState', function () { - const basePath = '/xyz'; - const appId = 'myApp'; - - it('does nothing when KibanaParsedUrl is in an invalid state', function () { - const url = new KibanaParsedUrl({ basePath }); - url.setGlobalState('newglobalstate'); - expect(url.getGlobalState()).to.be(''); - }); - - it('clears the global state when setting it to an empty string', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi?_g=globalstate' }); - url.setGlobalState(''); - expect(url.getGlobalState()).to.be(''); - }); - - it('updates the global state when a string is passed in', function () { - const url = new KibanaParsedUrl({ - basePath, - appId, - appPath: '/hi?notg=something&_g=oldstate', - }); - url.setGlobalState('newstate'); - expect(url.getGlobalState()).to.be('newstate'); - }); - - it('adds the global state parameters if it did not exist before', function () { - const url = new KibanaParsedUrl({ basePath, appId, appPath: '/hi' }); - url.setGlobalState('newstate'); - expect(url.getGlobalState()).to.be('newstate'); - expect(url.appPath).to.be('/hi?_g=newstate'); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/prepend_path.js b/src/legacy/ui/public/url/__tests__/prepend_path.js deleted file mode 100644 index 36991b77553e4..0000000000000 --- a/src/legacy/ui/public/url/__tests__/prepend_path.js +++ /dev/null @@ -1,80 +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 expect from '@kbn/expect'; - -import { prependPath } from '../prepend_path'; - -describe('url prependPath', function () { - describe('returns the relative path unchanged', function () { - it('if it is null', function () { - expect(prependPath(null, 'kittens')).to.be(null); - }); - - it('if it is undefined', function () { - expect(prependPath(undefined, 'kittens')).to.be(undefined); - }); - - it('if it is an absolute url', function () { - expect(prependPath('http://www.hithere.com/howareyou', 'welcome')).to.be( - 'http://www.hithere.com/howareyou' - ); - }); - - it('if it does not start with a /', function () { - expect(prependPath('are/so/cool', 'cats')).to.be('are/so/cool'); - }); - - it('when new path is empty', function () { - expect(prependPath('/are/so/cool', '')).to.be('/are/so/cool'); - }); - - it('when it is only a slash and new path is empty', function () { - expect(prependPath('/', '')).to.be('/'); - }); - }); - - describe('returns an updated relative path', function () { - it('when it starts with a slash', function () { - expect(prependPath('/are/so/cool', 'dinosaurs')).to.be('dinosaurs/are/so/cool'); - }); - - it('when new path starts with a slash', function () { - expect(prependPath('/are/so/cool', '/fish')).to.be('/fish/are/so/cool'); - }); - - it('with two slashes if new path is a slash', function () { - expect(prependPath('/are/so/cool', '/')).to.be('//are/so/cool'); - }); - - it('when there is a slash on the end', function () { - expect(prependPath('/are/delicious/', 'lollipops')).to.be('lollipops/are/delicious/'); - }); - - it('when pathname that ends with a file', function () { - expect(prependPath('/are/delicious/index.html', 'donuts')).to.be( - 'donuts/are/delicious/index.html' - ); - }); - - it('when it is only a slash', function () { - expect(prependPath('/', 'kittens')).to.be('kittens/'); - }); - }); -}); diff --git a/src/legacy/ui/public/url/__tests__/url.js b/src/legacy/ui/public/url/__tests__/url.js deleted file mode 100644 index 8b173482e1bb4..0000000000000 --- a/src/legacy/ui/public/url/__tests__/url.js +++ /dev/null @@ -1,562 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import faker from 'faker'; -import _ from 'lodash'; -import { AppStateProvider } from '../../state_management/app_state'; -import '..'; - -// global vars, injected and mocked in init() -let kbnUrl; -let $route; -let $location; -let $rootScope; -let appState; - -class StubAppState { - constructor() { - this.getQueryParamName = () => '_a'; - this.toQueryParam = () => 'stateQueryParam'; - this.destroy = sinon.stub(); - } -} - -function init() { - ngMock.module('kibana/url', 'kibana', function ($provide, PrivateProvider) { - $provide.service('$route', function () { - return { - reload: _.noop, - }; - }); - - appState = new StubAppState(); - PrivateProvider.swap(AppStateProvider, ($decorate) => { - const AppState = $decorate(); - AppState.getAppState = () => appState; - return AppState; - }); - }); - - ngMock.inject(function ($injector) { - $route = $injector.get('$route'); - $location = $injector.get('$location'); - $rootScope = $injector.get('$rootScope'); - kbnUrl = $injector.get('kbnUrl'); - }); -} - -describe('kbnUrl', function () { - beforeEach(function () { - init(); - }); - - describe('forcing reload', function () { - it('schedules a listener for $locationChangeSuccess on the $rootScope', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - - sinon.stub($rootScope, '$on'); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - sinon.assert.calledOnce(appState.destroy); - expect($rootScope.$on.callCount).to.be(1); - expect($rootScope.$on.firstCall.args[0]).to.be('$locationChangeSuccess'); - }); - - it('handler unbinds the listener and calls reload', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - - const unbind = sinon.stub(); - sinon.stub($rootScope, '$on').returns(unbind); - $route.reload = sinon.stub(); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - const handler = $rootScope.$on.firstCall.args[1]; - handler(); - expect(unbind.callCount).to.be(1); - expect($route.reload.callCount).to.be(1); - }); - - it('reloads requested before the first are ignored', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - $route.reload = sinon.stub(); - - sinon.stub($rootScope, '$on').returns(sinon.stub()); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - // don't call the first handler - - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - }); - - it('one reload can happen once the first has completed', function () { - $location.url('/url'); - $route.current = { - $$route: { - regex: /.*/, - }, - }; - $route.reload = sinon.stub(); - - sinon.stub($rootScope, '$on').returns(sinon.stub()); - - expect($rootScope.$on.callCount).to.be(0); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(1); - - // call the first handler - $rootScope.$on.firstCall.args[1](); - expect($route.reload.callCount).to.be(1); - - expect($rootScope.$on.callCount).to.be(1); - kbnUrl.change('/url'); - expect($rootScope.$on.callCount).to.be(2); - }); - }); - - describe('remove', function () { - it('removes a parameter with a value from the url', function () { - $location.url('/myurl?exist&WithAParamToRemove=2&anothershouldexist=5'); - kbnUrl.removeParam('WithAParamToRemove'); - expect($location.url()).to.be('/myurl?exist&anothershouldexist=5'); - }); - - it('removes a parameter with no value from the url', function () { - $location.url('/myurl?removeme&hi=5'); - kbnUrl.removeParam('removeme'); - expect($location.url()).to.be('/myurl?hi=5'); - }); - - it('is noop if the given parameter doesn\t exist in the url', function () { - $location.url('/myurl?hi&bye'); - kbnUrl.removeParam('noexist'); - expect($location.url()).to.be('/myurl?hi&bye'); - }); - - it('is noop if given empty string param', function () { - $location.url('/myurl?hi&bye'); - kbnUrl.removeParam(''); - expect($location.url()).to.be('/myurl?hi&bye'); - }); - }); - - describe('change', function () { - it('should set $location.url', function () { - sinon.stub($location, 'url'); - - expect($location.url.callCount).to.be(0); - kbnUrl.change('/some-url'); - expect($location.url.callCount).to.be(1); - }); - - it('should uri encode replaced params', function () { - const url = '/some/path/'; - const params = { replace: faker.Lorem.words(3).join(' ') }; - const check = encodeURIComponent(params.replace); - sinon.stub($location, 'url'); - - kbnUrl.change(url + '{{replace}}', params); - - expect($location.url.firstCall.args[0]).to.be(url + check); - }); - - it('should parse angular expression in substitutions and uri encode the results', function () { - // build url by piecing together these parts - const urlParts = ['/', '/', '?', '&', '#']; - // make sure it can parse templates with weird spacing - const wrappers = [ - ['{{', '}}'], - ['{{ ', ' }}'], - ['{{', ' }}'], - ['{{ ', '}}'], - ['{{ ', ' }}'], - ]; - // make sure filters are evaluated via angular expressions - const objIndex = 4; // used to case one replace as an object - const filters = ['', 'uppercase', '', 'uppercase', '']; - - // the words (template keys) used must all be unique - const words = _.uniq(faker.Lorem.words(10)) - .slice(0, urlParts.length) - .map(function (word, i) { - if (filters[i].length) { - return word + '|' + filters[i]; - } - return word; - }); - - const replacements = faker.Lorem.words(urlParts.length).map(function (word, i) { - // make selected replacement into an object - if (i === objIndex) { - return { replace: word }; - } - - return word; - }); - - // build the url and test url - let url = ''; - let testUrl = ''; - urlParts.forEach(function (part, i) { - url += part + wrappers[i][0] + words[i] + wrappers[i][1]; - const locals = {}; - locals[words[i].split('|')[0]] = replacements[i]; - testUrl += part + encodeURIComponent($rootScope.$eval(words[i], locals)); - }); - - // create the locals replacement object - const params = {}; - replacements.forEach(function (replacement, i) { - const word = words[i].split('|')[0]; - params[word] = replacement; - }); - - sinon.stub($location, 'url'); - - kbnUrl.change(url, params); - - expect($location.url.firstCall.args[0]).to.not.be(url); - expect($location.url.firstCall.args[0]).to.be(testUrl); - }); - - it('should handle dot notation', function () { - const url = '/some/thing/{{that.is.substituted}}'; - - kbnUrl.change(url, { - that: { - is: { - substituted: 'test', - }, - }, - }); - - expect($location.url()).to.be('/some/thing/test'); - }); - - it('should throw when params are missing', function () { - const url = '/{{replace_me}}'; - const params = {}; - - try { - kbnUrl.change(url, params); - throw new Error('this should not run'); - } catch (err) { - expect(err).to.be.an(Error); - expect(err.message).to.match(/replace_me/); - } - }); - - it('should throw when filtered params are missing', function () { - const url = '/{{replace_me|number}}'; - const params = {}; - - try { - kbnUrl.change(url, params); - throw new Error('this should not run'); - } catch (err) { - expect(err).to.be.an(Error); - expect(err.message).to.match(/replace_me\|number/); - } - }); - - it('should change the entire url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.change(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({}); - expect($location.hash()).to.be(''); - }); - - it('should allow setting app state on the target url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.change(newPath, null, new StubAppState()); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({ _a: 'stateQueryParam' }); - expect($location.hash()).to.be(''); - }); - }); - - describe('changePath', function () { - it('should change just the path', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.changePath(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - }); - }); - - describe('redirect', function () { - it('should change the entire url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirect(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({}); - expect($location.hash()).to.be(''); - }); - - it('should allow setting app state on the target url', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirect(newPath, null, new StubAppState()); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql({ _a: 'stateQueryParam' }); - expect($location.hash()).to.be(''); - }); - - it('should replace the current history entry', function () { - sinon.stub($location, 'replace'); - $location.url('/some/path'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirect('/new/path/'); - expect($location.replace.callCount).to.be(1); - }); - - it('should call replace on $location', function () { - sinon.stub(kbnUrl, '_shouldForceReload').returns(false); - sinon.stub($location, 'replace'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirect('/poop'); - expect($location.replace.callCount).to.be(1); - }); - }); - - describe('redirectPath', function () { - it('should only change the path', function () { - const path = '/test/path'; - const search = { search: 'test' }; - const hash = 'hash'; - const newPath = '/new/location'; - - $location.path(path).search(search).hash(hash); - - // verify the starting state - expect($location.path()).to.be(path); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - - kbnUrl.redirectPath(newPath); - - // verify the ending state - expect($location.path()).to.be(newPath); - expect($location.search()).to.eql(search); - expect($location.hash()).to.be(hash); - }); - - it('should call replace on $location', function () { - sinon.stub(kbnUrl, '_shouldForceReload').returns(false); - sinon.stub($location, 'replace'); - - expect($location.replace.callCount).to.be(0); - kbnUrl.redirectPath('/poop'); - expect($location.replace.callCount).to.be(1); - }); - }); - - describe('_shouldForceReload', function () { - let next; - let prev; - - beforeEach(function () { - $route.current = { - $$route: { - regexp: /^\/is-current-route\/(\d+)/, - reloadOnSearch: true, - }, - }; - - prev = { path: '/is-current-route/1', search: {} }; - next = { path: '/is-current-route/1', search: {} }; - }); - - it("returns false if the passed url doesn't match the current route", function () { - next.path = '/not current'; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - - describe('if the passed url does match the route', function () { - describe('and the route reloads on search', function () { - describe('and the path is the same', function () { - describe('and the search params are the same', function () { - it('returns true', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - - describe('and the path is different', function () { - beforeEach(function () { - next.path = '/not-same'; - }); - - describe('and the search params are the same', function () { - it('returns false', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - }); - - describe('but the route does not reload on search', function () { - beforeEach(function () { - $route.current.$$route.reloadOnSearch = false; - }); - - describe('and the path is the same', function () { - describe('and the search params are the same', function () { - it('returns true', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - describe('but the search params are different', function () { - it('returns true', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(true); - }); - }); - }); - - describe('and the path is different', function () { - beforeEach(function () { - next.path = '/not-same'; - }); - - describe('and the search params are the same', function () { - it('returns false', function () { - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - describe('but the search params are different', function () { - it('returns false', function () { - next.search = {}; - prev.search = { q: 'search term' }; - expect(kbnUrl._shouldForceReload(next, prev, $route)).to.be(false); - }); - }); - }); - }); - }); - }); -}); diff --git a/src/legacy/ui/public/url/absolute_to_parsed_url.ts b/src/legacy/ui/public/url/absolute_to_parsed_url.ts deleted file mode 100644 index 30f493c25776c..0000000000000 --- a/src/legacy/ui/public/url/absolute_to_parsed_url.ts +++ /dev/null @@ -1,44 +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 { parse } from 'url'; - -import { extractAppPathAndId } from './extract_app_path_and_id'; -import { KibanaParsedUrl } from './kibana_parsed_url'; - -/** - * - * @param absoluteUrl - an absolute url, e.g. https://localhost:5601/gra/app/visualize#/edit/viz_id?hi=bye - * @param basePath - An optional base path for kibana. If supplied, should start with a "/". - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the basePath is - * "/gra". - * @return {KibanaParsedUrl} - */ -export function absoluteToParsedUrl(absoluteUrl: string, basePath = '') { - const { appPath, appId } = extractAppPathAndId(absoluteUrl, basePath); - const { hostname, port, protocol } = parse(absoluteUrl); - return new KibanaParsedUrl({ - basePath, - appId: appId!, - appPath, - hostname, - port, - protocol, - }); -} diff --git a/src/legacy/ui/public/url/extract_app_path_and_id.ts b/src/legacy/ui/public/url/extract_app_path_and_id.ts deleted file mode 100644 index 44bba272e0873..0000000000000 --- a/src/legacy/ui/public/url/extract_app_path_and_id.ts +++ /dev/null @@ -1,41 +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 { parse } from 'url'; - -/** - * If the url is determined to contain an appId and appPath, it returns those portions. If it is not in the right - * format and an appId and appPath can't be extracted, it returns an empty object. - * @param {string} url - a relative or absolute url which contains an appPath, an appId, and optionally, a basePath. - * @param {string} basePath - optional base path, if given should start with "/". - */ -export function extractAppPathAndId(url: string, basePath = '') { - const parsedUrl = parse(url); - if (!parsedUrl.path) { - return {}; - } - const pathWithoutBase = parsedUrl.path.slice(basePath.length); - - if (!pathWithoutBase.startsWith('/app/')) { - return {}; - } - - const appPath = parsedUrl.hash && parsedUrl.hash.length > 0 ? parsedUrl.hash.slice(1) : ''; - return { appId: pathWithoutBase.slice('/app/'.length), appPath }; -} diff --git a/src/legacy/ui/public/url/index.js b/src/legacy/ui/public/url/index.js deleted file mode 100644 index 8ef267de2890c..0000000000000 --- a/src/legacy/ui/public/url/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { KbnUrlProvider } from './url'; -export { RedirectWhenMissingProvider } from './redirect_when_missing'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { modifyUrl } from '../../../../core/utils'; diff --git a/src/legacy/ui/public/url/kbn_url.ts b/src/legacy/ui/public/url/kbn_url.ts deleted file mode 100644 index 42b6a8f19f9a9..0000000000000 --- a/src/legacy/ui/public/url/kbn_url.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export interface KbnUrl { - change: (url: string) => void; - removeParam: (param: string) => void; -} diff --git a/src/legacy/ui/public/url/kibana_parsed_url.ts b/src/legacy/ui/public/url/kibana_parsed_url.ts deleted file mode 100644 index 1c60e8729e0ff..0000000000000 --- a/src/legacy/ui/public/url/kibana_parsed_url.ts +++ /dev/null @@ -1,148 +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 { parse } from 'url'; - -import { modifyUrl } from '../../../../core/public'; -import { prependPath } from './prepend_path'; - -interface Options { - /** - * An optional base path for kibana. If supplied, should start with a "/". - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the - * basePath is "/gra" - */ - basePath?: string; - - /** - * The app id. - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id the app id is "kibana". - */ - appId: string; - - /** - * The path for a page in the the app. Should start with a "/". Don't include the hash sign. Can - * include all query parameters. - * e.g. in https://localhost:5601/gra/app/visualize#/edit/viz_id?g=state the appPath is - * "/edit/viz_id?g=state" - */ - appPath?: string; - - /** - * Optional hostname. Uses current window location's hostname if hostname, port, - * and protocol are undefined. - */ - hostname?: string; - - /** - * Optional port. Uses current window location's port if hostname, port, - * and protocol are undefined. - */ - port?: string; - - /** - * Optional protocol. Uses current window location's protocol if hostname, port, - * and protocol are undefined. - */ - protocol?: string; -} - -/** - * Represents the pieces that make up a url in Kibana, offering some helpful functionality - * for translating those pieces into absolute or relative urls. A Kibana url with a basePath - * looks like this: http://localhost:5601/basePath/app/appId#/an/appPath?with=query¶ms - * - * - basePath is "/basePath" - * - appId is "appId" - * - appPath is "/an/appPath?with=query¶ms" - * - * Almost all urls in Kibana should have this structure, including the "/app" portion in front of the appId - * (one exception is the login link). - */ -export class KibanaParsedUrl { - public appId: string; - public appPath: string; - public basePath: string; - public hostname?: string; - public protocol?: string; - public port?: string; - - constructor(options: Options) { - const { appId, basePath = '', appPath = '', hostname, protocol, port } = options; - - // We'll use window defaults - const hostOrProtocolSpecified = hostname || protocol || port; - - this.basePath = basePath; - this.appId = appId; - this.appPath = appPath; - this.hostname = hostOrProtocolSpecified ? hostname : window.location.hostname; - this.port = hostOrProtocolSpecified ? port : window.location.port; - this.protocol = hostOrProtocolSpecified ? protocol : window.location.protocol; - } - - public getGlobalState() { - if (!this.appPath) { - return ''; - } - const parsedUrl = parse(this.appPath, true); - const query = parsedUrl.query || {}; - return query._g || ''; - } - - public setGlobalState(newGlobalState: string | string[]) { - if (!this.appPath) { - return; - } - - this.appPath = modifyUrl(this.appPath, (parsed) => { - parsed.query._g = newGlobalState; - }); - } - - public addQueryParameter(name: string, val: string) { - this.appPath = modifyUrl(this.appPath, (parsed) => { - parsed.query[name] = val; - }); - } - - public getHashedAppPath() { - return `#${this.appPath}`; - } - - public getAppBasePath() { - return `/${this.appId}`; - } - - public getAppRootPath() { - return `/app${this.getAppBasePath()}${this.getHashedAppPath()}`; - } - - public getRootRelativePath() { - return prependPath(this.getAppRootPath(), this.basePath); - } - - public getAbsoluteUrl() { - return modifyUrl(this.getRootRelativePath(), (parsed) => { - parsed.protocol = this.protocol; - parsed.port = this.port; - parsed.hostname = this.hostname; - }); - } -} diff --git a/src/legacy/ui/public/url/prepend_path.ts b/src/legacy/ui/public/url/prepend_path.ts deleted file mode 100644 index b8a77d5c23bee..0000000000000 --- a/src/legacy/ui/public/url/prepend_path.ts +++ /dev/null @@ -1,50 +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 { isString } from 'lodash'; -import { format, parse } from 'url'; - -/** - * - * @param {string} relativePath - a relative path that must start with a "/". - * @param {string} newPath - the new path to prefix. ex: 'xyz' - * @return {string} the url with the basePath prepended. ex. '/xyz/app/kibana#/management'. If - * the relative path isn't in the right format (e.g. doesn't start with a "/") the relativePath is returned - * unchanged. - */ -export function prependPath(relativePath: string, newPath = '') { - if (!relativePath || !isString(relativePath)) { - return relativePath; - } - - const parsed = parse(relativePath, true, true); - if (!parsed.host && parsed.pathname) { - if (parsed.pathname[0] === '/') { - parsed.pathname = newPath + parsed.pathname; - } - } - - return format({ - protocol: parsed.protocol, - host: parsed.host, - pathname: parsed.pathname, - query: parsed.query, - hash: parsed.hash, - }); -} diff --git a/src/legacy/ui/public/url/redirect_when_missing.js b/src/legacy/ui/public/url/redirect_when_missing.js deleted file mode 100644 index 85c90a14d9fd7..0000000000000 --- a/src/legacy/ui/public/url/redirect_when_missing.js +++ /dev/null @@ -1,71 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { MarkdownSimple } from '../../../../plugins/kibana_react/public'; -import { toastNotifications } from 'ui/notify'; -import { SavedObjectNotFound } from '../../../../plugins/kibana_utils/public'; -import { uiModules } from '../modules'; - -uiModules.get('kibana/url').service('redirectWhenMissing', function (Private) { - return Private(RedirectWhenMissingProvider); -}); - -export function RedirectWhenMissingProvider(kbnUrl, Promise) { - /** - * Creates an error handler that will redirect to a url when a SavedObjectNotFound - * error is thrown - * - * @param {string|object} mapping - a mapping of url's to redirect to based on the saved object that - * couldn't be found, or just a string that will be used for all types - * @return {function} - the handler to pass to .catch() - */ - return function (mapping) { - if (typeof mapping === 'string') { - mapping = { '*': mapping }; - } - - return function (error) { - // if this error is not "404", rethrow - const savedObjectNotFound = error instanceof SavedObjectNotFound; - const unknownVisType = error.message.indexOf('Invalid type') === 0; - if (unknownVisType) { - error.savedObjectType = 'visualization'; - } else if (!savedObjectNotFound) { - throw error; - } - - let url = mapping[error.savedObjectType] || mapping['*']; - if (!url) url = '/'; - - url += (url.indexOf('?') >= 0 ? '&' : '?') + `notFound=${error.savedObjectType}`; - - toastNotifications.addWarning({ - title: i18n.translate('common.ui.url.savedObjectIsMissingNotificationMessage', { - defaultMessage: 'Saved object is missing', - }), - text: {error.message}, - }); - - kbnUrl.redirect(url); - return Promise.halt(); - }; - }; -} diff --git a/src/legacy/ui/public/url/relative_to_absolute.ts b/src/legacy/ui/public/url/relative_to_absolute.ts deleted file mode 100644 index 7d0737d145a1b..0000000000000 --- a/src/legacy/ui/public/url/relative_to_absolute.ts +++ /dev/null @@ -1,34 +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. - */ - -/** - * - * @param {string} url - a relative or root relative url. If a relative path is given then the - * absolute url returned will depend on the current page where this function is called from. For example - * if you are on page "http://www.mysite.com/shopping/kids" and you pass this function "adults", you would get - * back "http://www.mysite.com/shopping/adults". If you passed this function a root relative path, or one that - * starts with a "/", for example "/account/cart", you would get back "http://www.mysite.com/account/cart". - * @return {string} the relative url transformed into an absolute url - */ -export function relativeToAbsolute(url: string) { - // convert all link urls to absolute urls - const a = document.createElement('a'); - a.setAttribute('href', url); - return a.href; -} diff --git a/src/legacy/ui/public/url/url.js b/src/legacy/ui/public/url/url.js deleted file mode 100644 index fb243b02e05c7..0000000000000 --- a/src/legacy/ui/public/url/url.js +++ /dev/null @@ -1,247 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiModules } from '../modules'; -import { AppStateProvider } from '../state_management/app_state'; - -uiModules.get('kibana/url').service('kbnUrl', function (Private, $injector) { - //config is not directly used but registers global event listeners to kbnUrl to function - $injector.get('config'); - return Private(KbnUrlProvider); -}); - -export function KbnUrlProvider($injector, $location, $rootScope, $parse, Private) { - /** - * the `kbnUrl` service was created to smooth over some of the - * inconsistent behavior that occurs when modifying the url via - * the `$location` api. In general it is recommended that you use - * the `kbnUrl` service any time you want to modify the url. - * - * "features" that `kbnUrl` does it's best to guarantee, which - * are not guaranteed with the `$location` service: - * - calling `kbnUrl.change()` with a url that resolves to the current - * route will force a full transition (rather than just updating the - * properties of the $route object) - * - * Additional features of `kbnUrl` - * - parameterized urls - * - easily include an app state with the url - * - * @type {KbnUrl} - */ - const self = this; - - /** - * Navigate to a url - * - * @param {String} url - the new url, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the url template - * @return {undefined} - */ - self.change = function (url, paramObj, appState) { - self._changeLocation('url', url, paramObj, false, appState); - }; - - /** - * Same as #change except only changes the url's path, - * leaving the search string and such intact - * - * @param {String} path - the new path, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the path template - * @return {undefined} - */ - self.changePath = function (path, paramObj) { - self._changeLocation('path', path, paramObj); - }; - - /** - * Same as #change except that it removes the current url from history - * - * @param {String} url - the new url, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the url template - * @return {undefined} - */ - self.redirect = function (url, paramObj, appState) { - self._changeLocation('url', url, paramObj, true, appState); - }; - - /** - * Same as #redirect except only changes the url's path, - * leaving the search string and such intact - * - * @param {String} path - the new path, can be a template. See #eval - * @param {Object} [paramObj] - optional set of parameters for the path template - * @return {undefined} - */ - self.redirectPath = function (path, paramObj) { - self._changeLocation('path', path, paramObj, true); - }; - - /** - * Evaluate a url template. templates can contain double-curly wrapped - * expressions that are evaluated in the context of the paramObj - * - * @param {String} template - the url template to evaluate - * @param {Object} [paramObj] - the variables to expose to the template - * @return {String} - the evaluated result - * @throws {Error} If any of the expressions can't be parsed. - */ - self.eval = function (template, paramObj) { - paramObj = paramObj || {}; - - return template.replace(/\{\{([^\}]+)\}\}/g, function (match, expr) { - // remove filters - const key = expr.split('|')[0].trim(); - - // verify that the expression can be evaluated - const p = $parse(key)(paramObj); - - // if evaluation can't be made, throw - if (_.isUndefined(p)) { - throw new Error( - i18n.translate('common.ui.url.replacementFailedErrorMessage', { - defaultMessage: 'Replacement failed, unresolved expression: {expr}', - values: { expr }, - }) - ); - } - - return encodeURIComponent($parse(expr)(paramObj)); - }); - }; - - /** - * convert an object's route to an href, compatible with - * window.location.href= and - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {string} - the computed href - */ - self.getRouteHref = function (obj, route) { - return '#' + self.getRouteUrl(obj, route); - }; - - /** - * convert an object's route to a url, compatible with url.change() or $location.url() - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {string} - the computed url - */ - self.getRouteUrl = function (obj, route) { - const template = obj && obj.routes && obj.routes[route]; - if (template) return self.eval(template, obj); - }; - - /** - * Similar to getRouteUrl, supports objects which list their routes, - * and redirects to the named route. See #redirect - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {undefined} - */ - self.redirectToRoute = function (obj, route) { - self.redirect(self.getRouteUrl(obj, route)); - }; - - /** - * Similar to getRouteUrl, supports objects which list their routes, - * and changes the url to the named route. See #change - * - * @param {Object} obj - any object that list's it's routes at obj.routes{} - * @param {string} route - the route name - * @return {undefined} - */ - self.changeToRoute = function (obj, route) { - self.change(self.getRouteUrl(obj, route)); - }; - - /** - * Removes the given parameter from the url. Does so without modifying the browser - * history. - * @param param - */ - self.removeParam = function (param) { - $location.search(param, null).replace(); - }; - - ///// - // private api - ///// - let reloading; - - self._changeLocation = function (type, url, paramObj, replace, appState) { - const prev = { - path: $location.path(), - search: $location.search(), - }; - - url = self.eval(url, paramObj); - $location[type](url); - if (replace) $location.replace(); - - if (appState) { - $location.search(appState.getQueryParamName(), appState.toQueryParam()); - } - - const next = { - path: $location.path(), - search: $location.search(), - }; - - if ($injector.has('$route')) { - const $route = $injector.get('$route'); - - if (self._shouldForceReload(next, prev, $route)) { - const appState = Private(AppStateProvider).getAppState(); - if (appState) appState.destroy(); - - reloading = $rootScope.$on('$locationChangeSuccess', function () { - // call the "unlisten" function returned by $on - reloading(); - reloading = false; - - $route.reload(); - }); - } - } - }; - - // determine if the router will automatically reload the route - self._shouldForceReload = function (next, prev, $route) { - if (reloading) return false; - - const route = $route.current && $route.current.$$route; - if (!route) return false; - - // for the purposes of determining whether the router will - // automatically be reloading, '' and '/' are equal - const nextPath = next.path || '/'; - const prevPath = prev.path || '/'; - if (nextPath !== prevPath) return false; - - const reloadOnSearch = route.reloadOnSearch; - const searchSame = _.isEqual(next.search, prev.search); - return (reloadOnSearch && searchSame) || !reloadOnSearch; - }; -} diff --git a/src/legacy/ui/public/utils/collection.test.ts b/src/legacy/ui/public/utils/collection.test.ts deleted file mode 100644 index 0841e3554c0d0..0000000000000 --- a/src/legacy/ui/public/utils/collection.test.ts +++ /dev/null @@ -1,141 +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 { move } from './collection'; - -describe('collection', () => { - describe('move', () => { - test('accepts previous from->to syntax', () => { - const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1]; - - expect(list[3]).toBe(1); - expect(list[8]).toBe(8); - - move(list, 8, 3); - - expect(list[8]).toBe(1); - expect(list[3]).toBe(8); - }); - - test('moves an object up based on a function callback', () => { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).toBe(0); - expect(list[5]).toBe(1); - expect(list[6]).toBe(0); - - move(list, 5, false, (v: any) => v === 0); - - expect(list[4]).toBe(1); - expect(list[5]).toBe(0); - expect(list[6]).toBe(0); - }); - - test('moves an object down based on a function callback', () => { - const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1]; - - expect(list[4]).toBe(0); - expect(list[5]).toBe(1); - expect(list[6]).toBe(0); - - move(list, 5, true, (v: any) => v === 0); - - expect(list[4]).toBe(0); - expect(list[5]).toBe(0); - expect(list[6]).toBe(1); - }); - - test('moves an object up based on a where callback', () => { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 1); - expect(list[6]).toHaveProperty('v', 0); - - move(list, 5, false, { v: 0 }); - - expect(list[4]).toHaveProperty('v', 1); - expect(list[5]).toHaveProperty('v', 0); - expect(list[6]).toHaveProperty('v', 0); - }); - - test('moves an object down based on a where callback', () => { - const list = [ - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 0 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - { v: 1 }, - ]; - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 1); - expect(list[6]).toHaveProperty('v', 0); - - move(list, 5, true, { v: 0 }); - - expect(list[4]).toHaveProperty('v', 0); - expect(list[5]).toHaveProperty('v', 0); - expect(list[6]).toHaveProperty('v', 1); - }); - - test('moves an object down based on a pluck callback', () => { - const list = [ - { id: 0, normal: true }, - { id: 1, normal: true }, - { id: 2, normal: true }, - { id: 3, normal: true }, - { id: 4, normal: true }, - { id: 5, normal: false }, - { id: 6, normal: true }, - { id: 7, normal: true }, - { id: 8, normal: true }, - { id: 9, normal: true }, - ]; - - expect(list[4]).toHaveProperty('id', 4); - expect(list[5]).toHaveProperty('id', 5); - expect(list[6]).toHaveProperty('id', 6); - - move(list, 5, true, 'normal'); - - expect(list[4]).toHaveProperty('id', 4); - expect(list[5]).toHaveProperty('id', 6); - expect(list[6]).toHaveProperty('id', 5); - }); - }); -}); diff --git a/src/legacy/ui/public/utils/collection.ts b/src/legacy/ui/public/utils/collection.ts deleted file mode 100644 index b882a2bbe6e5b..0000000000000 --- a/src/legacy/ui/public/utils/collection.ts +++ /dev/null @@ -1,76 +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 _ from 'lodash'; - -/** - * move an obj either up or down in the collection by - * injecting it either before/after the prev/next obj that - * satisfied the qualifier - * - * or, just from one index to another... - * - * @param {array} objs - the list to move the object within - * @param {number|any} obj - the object that should be moved, or the index that the object is currently at - * @param {number|boolean} below - the index to move the object to, or whether it should be moved up or down - * @param {function} qualifier - a lodash-y callback, object = _.where, string = _.pluck - * @return {array} - the objs argument - */ -export function move( - objs: any[], - obj: object | number, - below: number | boolean, - qualifier?: ((object: object, index: number) => any) | Record | string -): object[] { - const origI = _.isNumber(obj) ? obj : objs.indexOf(obj); - if (origI === -1) { - return objs; - } - - if (_.isNumber(below)) { - // move to a specific index - objs.splice(below, 0, objs.splice(origI, 1)[0]); - return objs; - } - - below = !!below; - qualifier = qualifier && _.iteratee(qualifier); - - const above = !below; - const finder = below ? _.findIndex : _.findLastIndex; - - // find the index of the next/previous obj that meets the qualifications - const targetI = finder(objs, (otherAgg, otherI) => { - if (below && otherI <= origI) { - return; - } - if (above && otherI >= origI) { - return; - } - return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI)); - }); - - if (targetI === -1) { - return objs; - } - - // place the obj at it's new index - objs.splice(targetI, 0, objs.splice(origI, 1)[0]); - return objs; -} diff --git a/src/legacy/ui/public/utils/legacy_class.js b/src/legacy/ui/public/utils/legacy_class.js deleted file mode 100644 index f47650a77bb6d..0000000000000 --- a/src/legacy/ui/public/utils/legacy_class.js +++ /dev/null @@ -1,56 +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. - */ - -// create a property descriptor for properties -// that won't change -function describeConst(val) { - return { - writable: false, - enumerable: false, - configurable: false, - value: val, - }; -} - -const props = { - inherits: describeConst(function (SuperClass) { - const prototype = Object.create(SuperClass.prototype, { - constructor: describeConst(this), - superConstructor: describeConst(SuperClass), - }); - - Object.defineProperties(this, { - prototype: describeConst(prototype), - Super: describeConst(SuperClass), - }); - - return this; - }), -}; - -/** - * Add class-related behavior to a function, currently this - * only attaches an .inherits() method. - * - * @param {Constructor} ClassConstructor - The function that should be extended - * @return {Constructor} - the constructor passed in; - */ -export function createLegacyClass(ClassConstructor) { - return Object.defineProperties(ClassConstructor, props); -} diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index 6103041cf0a4c..68a21c6a1587c 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -32,10 +32,6 @@ import { AdvancedSettingsComponent } from './advanced_settings'; import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { ComponentRegistry } from '../component_registry'; -jest.mock('ui/new_platform', () => ({ - npStart: mockConfig(), -})); - jest.mock('./components/field', () => ({ Field: () => { return 'field'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts index 51f4fc7ce94b9..5e0eeaee3c0d0 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts @@ -22,5 +22,3 @@ import { setup } from 'test_utils/http_test_setup'; export const { http } = setup((injectedMetadata) => { injectedMetadata.getBasePath.mockReturnValue('/hola/daro/'); }); - -jest.doMock('ui/new_platform', () => ({ npSetup: { core: { http } } })); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ba40dece25df9..0c4465ae7f4b9 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1508,7 +1508,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 86ee98b7af9d8..2d311fd88eb39 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -17,8 +17,7 @@ * under the License. */ -import { Component } from 'react'; -import React from 'react'; +import React, { Component, RefObject, createRef } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -30,6 +29,7 @@ import { EuiButton, EuiLink, htmlIdGenerator, + EuiPortal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -42,6 +42,7 @@ import { withKibana, KibanaReactContextValue, toMountPoint } from '../../../../k import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; +import { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '..'; interface Props { @@ -60,6 +61,7 @@ interface Props { onChangeQueryInputFocus?: (isFocused: boolean) => void; onSubmit?: (query: Query) => void; dataTestSubj?: string; + size?: SuggestionsListSize; } interface State { @@ -70,6 +72,7 @@ interface State { selectionStart: number | null; selectionEnd: number | null; indexPatterns: IIndexPattern[]; + queryBarRect: DOMRect | undefined; } const KEY_CODES = { @@ -93,6 +96,7 @@ export class QueryStringInputUI extends Component { selectionStart: null, selectionEnd: null, indexPatterns: [], + queryBarRect: undefined, }; public inputRef: HTMLTextAreaElement | null = null; @@ -101,6 +105,7 @@ export class QueryStringInputUI extends Component { private abortController?: AbortController; private services = this.props.kibana.services; private componentIsUnmounting = false; + private queryBarInputDivRefInstance: RefObject = createRef(); private getQueryString = () => { return toUser(this.props.query.query); @@ -494,8 +499,13 @@ export class QueryStringInputUI extends Component { this.initPersistedLog(); this.fetchIndexPatterns().then(this.updateSuggestions); + this.handleListUpdate(); window.addEventListener('resize', this.handleAutoHeight); + window.addEventListener('scroll', this.handleListUpdate, { + passive: true, // for better performance as we won't call preventDefault + capture: true, // scroll events don't bubble, they must be captured instead + }); } public componentDidUpdate(prevProps: Props) { @@ -533,12 +543,19 @@ export class QueryStringInputUI extends Component { this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); + window.removeEventListener('scroll', this.handleListUpdate); } + handleListUpdate = () => + this.setState({ + queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(), + }); + handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important'); } + this.handleListUpdate(); }; handleRemoveHeight = () => { @@ -587,6 +604,7 @@ export class QueryStringInputUI extends Component {

- - + + + diff --git a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap index 3b51c1db50d00..2fa7834872f6b 100644 --- a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap +++ b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap @@ -1,17 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SuggestionsComponent Passing the index should control which suggestion is selected 1`] = ` -
-
+ `; exports[`SuggestionsComponent Should display given suggestions if the show prop is true 1`] = ` -
-
+ `; diff --git a/src/plugins/data/public/ui/typeahead/_suggestion.scss b/src/plugins/data/public/ui/typeahead/_suggestion.scss index 81c05f1a8a78c..67ff17d017053 100644 --- a/src/plugins/data/public/ui/typeahead/_suggestion.scss +++ b/src/plugins/data/public/ui/typeahead/_suggestion.scss @@ -6,28 +6,36 @@ $kbnTypeaheadTypes: ( conjunction: $euiColorVis3, ); +.kbnTypeahead.kbnTypeahead--small { + max-height: 20vh; +} + +.kbnTypeahead__popover--top { + @include euiBottomShadowFlat; + border-top-left-radius: $euiBorderRadius; + border-top-right-radius: $euiBorderRadius; +} + +.kbnTypeahead__popover--bottom { + @include euiBottomShadow($adjustBorders: true); + border-bottom-left-radius: $euiBorderRadius; + border-bottom-right-radius: $euiBorderRadius; +} + .kbnTypeahead { - position: relative; + max-height: 60vh; .kbnTypeahead__popover { - @include euiBottomShadow($adjustBorders: true); + max-height: inherit; + @include euiScrollBar; border: 1px solid; border-color: $euiBorderColor; color: $euiTextColor; background-color: $euiColorEmptyShade; - position: absolute; - top: -2px; + position: relative; z-index: $euiZContentMenu; width: 100%; - border-bottom-left-radius: $euiBorderRadius; - border-bottom-right-radius: $euiBorderRadius; - - .kbnTypeahead__items { - @include euiScrollBar; - - max-height: 60vh; - overflow-y: auto; - } + overflow-y: auto; .kbnTypeahead__item { height: $euiSizeXL; diff --git a/src/legacy/ui/public/notify/toasts/toast_notifications.ts b/src/plugins/data/public/ui/typeahead/constants.ts similarity index 64% rename from src/legacy/ui/public/notify/toasts/toast_notifications.ts rename to src/plugins/data/public/ui/typeahead/constants.ts index d3ec8edb5d73a..08f9bd23e16f3 100644 --- a/src/legacy/ui/public/notify/toasts/toast_notifications.ts +++ b/src/plugins/data/public/ui/typeahead/constants.ts @@ -16,7 +16,21 @@ * specific language governing permissions and limitations * under the License. */ + /** - * ToastNotifications is deprecated! Please use npSetup.core.notifications.toasts instead + * Minimum width in px to display suggestion description correctly + * @public */ -export { ToastNotifications } from '../../../../../plugins/kibana_legacy/public'; +export const SUGGESTIONS_LIST_REQUIRED_WIDTH = 600; + +/** + * Minimum bottom distance in px to display list of suggestions + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE = 250; + +/** + * A distance in px to display suggestions list right under the query input without a gap + * @public + */ +export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 2; diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx index 9fe33b003527e..ba78bdd802601 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.test.tsx @@ -44,6 +44,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -59,6 +60,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -79,6 +81,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={innerRefCallback} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); }); @@ -94,6 +97,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); @@ -113,6 +117,7 @@ describe('SuggestionComponent', () => { suggestion={mockSuggestion} innerRef={noop} ariaId={'suggestion-1'} + shouldDisplayDescription={true} /> ); diff --git a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx index b859428e6ed7e..724287b874bf7 100644 --- a/src/plugins/data/public/ui/typeahead/suggestion_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestion_component.tsx @@ -46,6 +46,7 @@ interface Props { suggestion: QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; + shouldDisplayDescription: boolean; } export function SuggestionComponent(props: Props) { @@ -72,7 +73,9 @@ export function SuggestionComponent(props: Props) {
{props.suggestion.text}
-
{props.suggestion.description}
+ {props.shouldDisplayDescription && ( +
{props.suggestion.description}
+ )}
); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index 011a729c6a616..583940015c152 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -54,6 +54,7 @@ describe('SuggestionsComponent', () => { show={false} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -69,6 +70,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={[]} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -84,6 +86,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -100,6 +103,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -116,6 +120,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); @@ -134,6 +139,7 @@ describe('SuggestionsComponent', () => { show={true} suggestions={mockSuggestions} loadMore={noop} + queryBarRect={{ top: 0 } as DOMRect} /> ); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index 77dd7dcec01ee..dc7c55374f1d5 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -19,8 +19,15 @@ import { isEmpty } from 'lodash'; import React, { Component } from 'react'; +import classNames from 'classnames'; +import styled from 'styled-components'; import { QuerySuggestion } from '../../autocomplete'; import { SuggestionComponent } from './suggestion_component'; +import { + SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE, + SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET, + SUGGESTIONS_LIST_REQUIRED_WIDTH, +} from './constants'; interface Props { index: number | null; @@ -29,18 +36,24 @@ interface Props { show: boolean; suggestions: QuerySuggestion[]; loadMore: () => void; + queryBarRect?: DOMRect; + size?: SuggestionsListSize; } +export type SuggestionsListSize = 's' | 'l'; + export class SuggestionsComponent extends Component { private childNodes: HTMLDivElement[] = []; private parentNode: HTMLDivElement | null = null; public render() { - if (!this.props.show || isEmpty(this.props.suggestions)) { + if (!this.props.queryBarRect || !this.props.show || isEmpty(this.props.suggestions)) { return null; } const suggestions = this.props.suggestions.map((suggestion, index) => { + const isDescriptionFittable = + this.props.queryBarRect!.width >= SUGGESTIONS_LIST_REQUIRED_WIDTH; return ( (this.childNodes[index] = node)} @@ -50,17 +63,38 @@ export class SuggestionsComponent extends Component { onMouseEnter={() => this.props.onMouseEnter(index)} ariaId={'suggestion-' + index} key={`${suggestion.type} - ${suggestion.text}`} + shouldDisplayDescription={isDescriptionFittable} /> ); }); + const documentHeight = document.documentElement.clientHeight || window.innerHeight; + const { queryBarRect } = this.props; + + // reflects if the suggestions list has enough space below to be opened down + const isSuggestionsListFittable = + documentHeight - (queryBarRect.top + queryBarRect.height) > + SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE; + const verticalListPosition = isSuggestionsListFittable + ? `top: ${window.scrollY + queryBarRect.bottom - SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET}px;` + : `bottom: ${documentHeight - (window.scrollY + queryBarRect.top)}px;`; + return ( -
-
-
+ +
+
(this.parentNode = node)} onScroll={this.handleScroll} @@ -69,7 +103,7 @@ export class SuggestionsComponent extends Component {
-
+ ); } @@ -116,3 +150,11 @@ export class SuggestionsComponent extends Component { } }; } + +const StyledSuggestionsListDiv = styled.div` + ${(props: { queryBarRect: DOMRect; verticalListPosition: string }) => ` + position: absolute; + left: ${props.queryBarRect.left}px; + width: ${props.queryBarRect.width}px; + ${props.verticalListPosition}`} +`; diff --git a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx index 46be1044c0d4d..e6cf8a57686f1 100644 --- a/src/plugins/discover/public/application/components/field_name/field_name.test.tsx +++ b/src/plugins/discover/public/application/components/field_name/field_name.test.tsx @@ -20,8 +20,6 @@ import React from 'react'; import { render } from 'enzyme'; import { FieldName } from './field_name'; -jest.mock('ui/new_platform'); - // Note that it currently provides just 2 basic tests, there should be more, but // the components involved will soon change test('FieldName renders a string field by providing fieldType and fieldName', () => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index a0d9e3c541e47..6d1238e02c7fb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -80,11 +80,10 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals const props = { indexPattern, field, - getDetails: jest.fn(), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), - onShowDetails: jest.fn(), showDetails, selected, useShortDots, @@ -104,4 +103,9 @@ describe('discover sidebar field', function () { findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); + it('should trigger getDetails', function () { + const { comp, props } = getComponent(true); + findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + expect(props.getDetails).toHaveBeenCalledWith(props.field); + }); }); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx index aa575cd64944c..548e477c7c411 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/global_flyout/global_flyout.tsx @@ -160,6 +160,8 @@ export const useGlobalFlyout = () => { Array.from(getContents()).forEach(removeContent); } }; + // https://github.com/elastic/kibana/issues/73970 + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [removeContent]); return { ...ctx, addContent }; diff --git a/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx index 8c63cc8494a8b..7d21722781d60 100644 --- a/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx +++ b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx @@ -52,6 +52,8 @@ export const JsonEditor = React.memo( isControlled, }); + // https://github.com/elastic/kibana/issues/73971 + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const debouncedSetContent = useCallback(debounce(setContent, 300), [setContent]); // We let the consumer control the validation and the error message. @@ -76,6 +78,7 @@ export const JsonEditor = React.memo( debouncedSetContent(updated); } }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [isControlled] ); diff --git a/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts index 1b5ca5d7f4384..0ba39f5f05fe6 100644 --- a/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts +++ b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts @@ -84,6 +84,8 @@ export const useJson = ({ } else { didMount.current = true; } + // https://github.com/elastic/kibana/issues/73971 + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [content]); return { diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index bdea5ccf5fe26..995ae0ba42837 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -39,7 +39,7 @@ export { UseRequestResponse, sendRequest, useRequest, -} from './request/np_ready_request'; +} from './request'; export { indices } from './indices'; diff --git a/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts index a6792543cd726..b28cac1cb76f5 100644 --- a/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts +++ b/src/plugins/es_ui_shared/public/indices/validate/validate_index.test.ts @@ -17,8 +17,6 @@ * under the License. */ -jest.mock('ui/new_platform'); - import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from '../constants'; import { diff --git a/src/plugins/es_ui_shared/public/request/index.ts b/src/plugins/es_ui_shared/public/request/index.ts index f942a9cc3932b..9e38bfe5ee16b 100644 --- a/src/plugins/es_ui_shared/public/request/index.ts +++ b/src/plugins/es_ui_shared/public/request/index.ts @@ -17,11 +17,5 @@ * under the License. */ -export { - SendRequestConfig, - SendRequestResponse, - UseRequestConfig, - UseRequestResponse, - sendRequest, - useRequest, -} from './request'; +export { SendRequestConfig, SendRequestResponse, sendRequest } from './send_request'; +export { UseRequestConfig, UseRequestResponse, useRequest } from './use_request'; diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts deleted file mode 100644 index 06af698f2ce02..0000000000000 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ /dev/null @@ -1,184 +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 { useEffect, useState, useRef, useMemo } from 'react'; - -import { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public'; - -export interface SendRequestConfig { - path: string; - method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; - query?: HttpFetchQuery; - body?: any; -} - -export interface SendRequestResponse { - data: D | null; - error: E | null; -} - -export interface UseRequestConfig extends SendRequestConfig { - pollIntervalMs?: number; - initialData?: any; - deserializer?: (data: any) => any; -} - -export interface UseRequestResponse { - isInitialRequest: boolean; - isLoading: boolean; - error: E | null; - data?: D | null; - sendRequest: (...args: any[]) => Promise>; -} - -export const sendRequest = async ( - httpClient: HttpSetup, - { path, method, body, query }: SendRequestConfig -): Promise> => { - try { - const stringifiedBody = typeof body === 'string' ? body : JSON.stringify(body); - const response = await httpClient[method](path, { body: stringifiedBody, query }); - - return { - data: response.data ? response.data : response, - error: null, - }; - } catch (e) { - return { - data: null, - error: e.response && e.response.data ? e.response.data : e.body, - }; - } -}; - -export const useRequest = ( - httpClient: HttpSetup, - { - path, - method, - query, - body, - pollIntervalMs, - initialData, - deserializer = (data: any): any => data, - }: UseRequestConfig -): UseRequestResponse => { - const sendRequestRef = useRef<() => Promise>>(); - // Main states for tracking request status and data - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [data, setData] = useState(initialData); - - // Consumers can use isInitialRequest to implement a polling UX. - const [isInitialRequest, setIsInitialRequest] = useState(true); - const pollInterval = useRef(null); - const pollIntervalId = useRef(null); - - // We always want to use the most recently-set interval in scheduleRequest. - pollInterval.current = pollIntervalMs; - - // Tied to every render and bound to each request. - let isOutdatedRequest = false; - - const scheduleRequest = () => { - // Clear current interval - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - - // Set new interval - if (pollInterval.current) { - pollIntervalId.current = setTimeout( - () => (sendRequestRef.current ?? _sendRequest)(), - pollInterval.current - ); - } - }; - - const _sendRequest = async () => { - // We don't clear error or data, so it's up to the consumer to decide whether to display the - // "old" error/data or loading state when a new request is in-flight. - setIsLoading(true); - - const requestBody = { - path, - method, - query, - body, - }; - - const response = await sendRequest(httpClient, requestBody); - const { data: serializedResponseData, error: responseError } = response; - - // If an outdated request has resolved, DON'T update state, but DO allow the processData handler - // to execute side effects like update telemetry. - if (isOutdatedRequest) { - return { data: null, error: null }; - } - - setError(responseError); - - if (!responseError) { - const responseData = deserializer(serializedResponseData); - setData(responseData); - } - - setIsLoading(false); - setIsInitialRequest(false); - - // If we're on an interval, we need to schedule the next request. This also allows us to reset - // the interval if the user has manually requested the data, to avoid doubled-up requests. - scheduleRequest(); - - return { data: serializedResponseData, error: responseError }; - }; - - useEffect(() => { - sendRequestRef.current = _sendRequest; - }, [_sendRequest]); - - const stringifiedQuery = useMemo(() => JSON.stringify(query), [query]); - - useEffect(() => { - (sendRequestRef.current ?? _sendRequest)(); - // To be functionally correct we'd send a new request if the method, path, query or body changes. - // But it doesn't seem likely that the method will change and body is likely to be a new - // object even if its shape hasn't changed, so for now we're just watching the path and the query. - }, [path, stringifiedQuery]); - - useEffect(() => { - scheduleRequest(); - - // Clean up intervals and inflight requests and corresponding state changes - return () => { - isOutdatedRequest = true; - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - }; - }, [pollIntervalMs]); - - return { - isInitialRequest, - isLoading, - error, - data, - sendRequest: sendRequestRef.current ?? _sendRequest, // Gives the user the ability to manually request data - }; -}; diff --git a/src/plugins/es_ui_shared/public/request/request.test.js b/src/plugins/es_ui_shared/public/request/request.test.js deleted file mode 100644 index 190c32517eefe..0000000000000 --- a/src/plugins/es_ui_shared/public/request/request.test.js +++ /dev/null @@ -1,262 +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 sinon from 'sinon'; -// import { sendRequest as sendRequestUnbound, useRequest as useRequestUnbound } from './request'; - -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { mount } from 'enzyme'; - -const TestHook = ({ callback }) => { - callback(); - return null; -}; - -let element; - -const testHook = (callback) => { - element = mount(); -}; - -const wait = async (wait) => new Promise((resolve) => setTimeout(resolve, wait || 1)); - -// FLAKY: -// - https://github.com/elastic/kibana/issues/42561 -// - https://github.com/elastic/kibana/issues/42562 -// - https://github.com/elastic/kibana/issues/42563 -// - https://github.com/elastic/kibana/issues/42225 -describe.skip('request lib', () => { - const successRequest = { path: '/success', method: 'post', body: {} }; - const errorRequest = { path: '/error', method: 'post', body: {} }; - const successResponse = { statusCode: 200, data: { message: 'Success message' } }; - const errorResponse = { statusCode: 400, statusText: 'Error message' }; - - let sendPost; - let sendRequest; - let useRequest; - - /** - * - * commented out due to hooks being called regardless of skip - * https://github.com/facebook/jest/issues/8379 - - beforeEach(() => { - sendPost = sinon.stub(); - sendPost.withArgs(successRequest.path, successRequest.body).returns(successResponse); - sendPost.withArgs(errorRequest.path, errorRequest.body).throws(errorResponse); - - const httpClient = { - post: (...args) => { - return sendPost(...args); - }, - }; - - sendRequest = sendRequestUnbound.bind(null, httpClient); - useRequest = useRequestUnbound.bind(null, httpClient); - }); - - */ - - describe('sendRequest function', () => { - it('uses the provided path, method, and body to send the request', async () => { - const response = await sendRequest({ ...successRequest }); - sinon.assert.calledOnce(sendPost); - expect(response).toEqual({ data: successResponse.data, error: null }); - }); - - it('surfaces errors', async () => { - try { - await sendRequest({ ...errorRequest }); - } catch (e) { - sinon.assert.calledOnce(sendPost); - expect(e).toBe(errorResponse.error); - } - }); - }); - - describe('useRequest hook', () => { - let hook; - - function initUseRequest(config) { - act(() => { - testHook(() => { - hook = useRequest(config); - }); - }); - } - - describe('parameters', () => { - describe('path, method, body', () => { - it('is used to send the request', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - expect(hook.data).toBe(successResponse.data); - }); - }); - - describe('pollIntervalMs', () => { - it('sends another request after the specified time has elapsed', async () => { - initUseRequest({ ...successRequest, pollIntervalMs: 10 }); - await wait(50); - // We just care that multiple requests have been sent out. We don't check the specific - // timing because that risks introducing flakiness into the tests, and it's unlikely - // we could break the implementation by getting the exact timing wrong. - expect(sendPost.callCount).toBeGreaterThan(1); - - // We have to manually clean up or else the interval will continue to fire requests, - // interfering with other tests. - element.unmount(); - }); - }); - - describe('initialData', () => { - it('sets the initial data value', () => { - initUseRequest({ ...successRequest, initialData: 'initialData' }); - expect(hook.data).toBe('initialData'); - }); - }); - - describe('deserializer', () => { - it('is called once the request resolves', async () => { - const deserializer = sinon.stub(); - initUseRequest({ ...successRequest, deserializer }); - sinon.assert.notCalled(deserializer); - - await wait(50); - sinon.assert.calledOnce(deserializer); - sinon.assert.calledWith(deserializer, successResponse.data); - }); - - it('processes data', async () => { - initUseRequest({ ...successRequest, deserializer: () => 'intercepted' }); - await wait(50); - expect(hook.data).toBe('intercepted'); - }); - }); - }); - - describe('state', () => { - describe('isInitialRequest', () => { - it('is true for the first request and false for subsequent requests', async () => { - initUseRequest({ ...successRequest }); - expect(hook.isInitialRequest).toBe(true); - - hook.sendRequest(); - await wait(50); - expect(hook.isInitialRequest).toBe(false); - }); - }); - - describe('isLoading', () => { - it('represents in-flight request status', async () => { - initUseRequest({ ...successRequest }); - expect(hook.isLoading).toBe(true); - - await wait(50); - expect(hook.isLoading).toBe(false); - }); - }); - - describe('error', () => { - it('surfaces errors from requests', async () => { - initUseRequest({ ...errorRequest }); - await wait(50); - expect(hook.error).toBe(errorResponse); - }); - - it('persists while a request is in-flight', async () => { - initUseRequest({ ...errorRequest }); - await wait(50); - hook.sendRequest(); - expect(hook.isLoading).toBe(true); - expect(hook.error).toBe(errorResponse); - }); - - it('is null when the request is successful', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - expect(hook.isLoading).toBe(false); - expect(hook.error).toBeNull(); - }); - }); - - describe('data', () => { - it('surfaces payloads from requests', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - expect(hook.data).toBe(successResponse.data); - }); - - it('persists while a request is in-flight', async () => { - initUseRequest({ ...successRequest }); - await wait(50); - hook.sendRequest(); - expect(hook.isLoading).toBe(true); - expect(hook.data).toBe(successResponse.data); - }); - - it('is null when the request fails', async () => { - initUseRequest({ ...errorRequest }); - await wait(50); - expect(hook.isLoading).toBe(false); - expect(hook.data).toBeNull(); - }); - }); - }); - - describe('callbacks', () => { - describe('sendRequest', () => { - it('sends the request', () => { - initUseRequest({ ...successRequest }); - sinon.assert.calledOnce(sendPost); - hook.sendRequest(); - sinon.assert.calledTwice(sendPost); - }); - - it('resets the pollIntervalMs', async () => { - initUseRequest({ ...successRequest, pollIntervalMs: 800 }); - await wait(200); // 200ms - hook.sendRequest(); - expect(sendPost.callCount).toBe(2); - - await wait(200); // 400ms - hook.sendRequest(); - - await wait(200); // 600ms - hook.sendRequest(); - - await wait(200); // 800ms - hook.sendRequest(); - - await wait(200); // 1000ms - hook.sendRequest(); - - // If sendRequest didn't reset the interval, the interval would have triggered another - // request by now, and the callCount would be 7. - expect(sendPost.callCount).toBe(6); - - // We have to manually clean up or else the interval will continue to fire requests, - // interfering with other tests. - element.unmount(); - }); - }); - }); - }); -}); diff --git a/src/plugins/es_ui_shared/public/request/request.ts b/src/plugins/es_ui_shared/public/request/request.ts deleted file mode 100644 index fd6980367136e..0000000000000 --- a/src/plugins/es_ui_shared/public/request/request.ts +++ /dev/null @@ -1,165 +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 { useEffect, useState, useRef } from 'react'; - -export interface SendRequestConfig { - path: string; - method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; - body?: any; -} - -export interface SendRequestResponse { - data: any; - error: Error | null; -} - -export interface UseRequestConfig extends SendRequestConfig { - pollIntervalMs?: number; - initialData?: any; - deserializer?: (data: any) => any; -} - -export interface UseRequestResponse { - isInitialRequest: boolean; - isLoading: boolean; - error: null | unknown; - data: any; - sendRequest: (...args: any[]) => Promise; -} - -export const sendRequest = async ( - httpClient: ng.IHttpService, - { path, method, body }: SendRequestConfig -): Promise => { - try { - const response = await (httpClient as any)[method](path, body); - - if (typeof response.data === 'undefined') { - throw new Error(response.statusText); - } - - return { data: response.data, error: null }; - } catch (e) { - return { - data: null, - error: e.response ? e.response : e, - }; - } -}; - -export const useRequest = ( - httpClient: ng.IHttpService, - { - path, - method, - body, - pollIntervalMs, - initialData, - deserializer = (data: any): any => data, - }: UseRequestConfig -): UseRequestResponse => { - // Main states for tracking request status and data - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [data, setData] = useState(initialData); - - // Consumers can use isInitialRequest to implement a polling UX. - const [isInitialRequest, setIsInitialRequest] = useState(true); - const pollInterval = useRef(null); - const pollIntervalId = useRef(null); - - // We always want to use the most recently-set interval in scheduleRequest. - pollInterval.current = pollIntervalMs; - - // Tied to every render and bound to each request. - let isOutdatedRequest = false; - - const scheduleRequest = () => { - // Clear current interval - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - - // Set new interval - if (pollInterval.current) { - pollIntervalId.current = setTimeout(_sendRequest, pollInterval.current); - } - }; - - const _sendRequest = async () => { - // We don't clear error or data, so it's up to the consumer to decide whether to display the - // "old" error/data or loading state when a new request is in-flight. - setIsLoading(true); - - const requestBody = { - path, - method, - body, - }; - - const response = await sendRequest(httpClient, requestBody); - const { data: serializedResponseData, error: responseError } = response; - const responseData = deserializer(serializedResponseData); - - // If an outdated request has resolved, DON'T update state, but DO allow the processData handler - // to execute side effects like update telemetry. - if (isOutdatedRequest) { - return { data: null, error: null }; - } - - setError(responseError); - setData(responseData); - setIsLoading(false); - setIsInitialRequest(false); - - // If we're on an interval, we need to schedule the next request. This also allows us to reset - // the interval if the user has manually requested the data, to avoid doubled-up requests. - scheduleRequest(); - - return { data: serializedResponseData, error: responseError }; - }; - - useEffect(() => { - _sendRequest(); - // To be functionally correct we'd send a new request if the method, path, or body changes. - // But it doesn't seem likely that the method will change and body is likely to be a new - // object even if its shape hasn't changed, so for now we're just watching the path. - }, [path]); - - useEffect(() => { - scheduleRequest(); - - // Clean up intervals and inflight requests and corresponding state changes - return () => { - isOutdatedRequest = true; - if (pollIntervalId.current) { - clearTimeout(pollIntervalId.current); - } - }; - }, [pollIntervalMs]); - - return { - isInitialRequest, - isLoading, - error, - data, - sendRequest: _sendRequest, // Gives the user the ability to manually request data - }; -}; diff --git a/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts b/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts new file mode 100644 index 0000000000000..4312780e74c0f --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/send_request.test.helpers.ts @@ -0,0 +1,83 @@ +/* + * 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 sinon from 'sinon'; + +import { HttpSetup, HttpFetchOptions } from '../../../../../src/core/public'; +import { + SendRequestConfig, + SendRequestResponse, + sendRequest as originalSendRequest, +} from './send_request'; + +export interface SendRequestHelpers { + getSendRequestSpy: () => sinon.SinonStub; + sendSuccessRequest: () => Promise; + getSuccessResponse: () => SendRequestResponse; + sendErrorRequest: () => Promise; + getErrorResponse: () => SendRequestResponse; +} + +const successRequest: SendRequestConfig = { method: 'post', path: '/success', body: {} }; +const successResponse = { statusCode: 200, data: { message: 'Success message' } }; + +const errorValue = { statusCode: 400, statusText: 'Error message' }; +const errorRequest: SendRequestConfig = { method: 'post', path: '/error', body: {} }; +const errorResponse = { response: { data: errorValue } }; + +export const createSendRequestHelpers = (): SendRequestHelpers => { + const sendRequestSpy = sinon.stub(); + const httpClient = { + post: (path: string, options: HttpFetchOptions) => sendRequestSpy(path, options), + }; + const sendRequest = originalSendRequest.bind(null, httpClient as HttpSetup) as ( + config: SendRequestConfig + ) => Promise>; + + // Set up successful request helpers. + sendRequestSpy + .withArgs(successRequest.path, { + body: JSON.stringify(successRequest.body), + query: undefined, + }) + .resolves(successResponse); + const sendSuccessRequest = () => sendRequest({ ...successRequest }); + const getSuccessResponse = () => ({ data: successResponse.data, error: null }); + + // Set up failed request helpers. + sendRequestSpy + .withArgs(errorRequest.path, { + body: JSON.stringify(errorRequest.body), + query: undefined, + }) + .rejects(errorResponse); + const sendErrorRequest = () => sendRequest({ ...errorRequest }); + const getErrorResponse = () => ({ + data: null, + error: errorResponse.response.data, + }); + + return { + getSendRequestSpy: () => sendRequestSpy, + sendSuccessRequest, + getSuccessResponse, + sendErrorRequest, + getErrorResponse, + }; +}; diff --git a/src/plugins/es_ui_shared/public/request/send_request.test.ts b/src/plugins/es_ui_shared/public/request/send_request.test.ts new file mode 100644 index 0000000000000..e4deaeaba817e --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/send_request.test.ts @@ -0,0 +1,47 @@ +/* + * 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 sinon from 'sinon'; + +import { SendRequestHelpers, createSendRequestHelpers } from './send_request.test.helpers'; + +describe('sendRequest function', () => { + let helpers: SendRequestHelpers; + + beforeEach(() => { + helpers = createSendRequestHelpers(); + }); + + it('uses the provided path, method, and body to send the request', async () => { + const { sendSuccessRequest, getSendRequestSpy, getSuccessResponse } = helpers; + + const response = await sendSuccessRequest(); + sinon.assert.calledOnce(getSendRequestSpy()); + expect(response).toEqual(getSuccessResponse()); + }); + + it('surfaces errors', async () => { + const { sendErrorRequest, getSendRequestSpy, getErrorResponse } = helpers; + + // For some reason sinon isn't throwing an error on rejection, as an awaited Promise normally would. + const error = await sendErrorRequest(); + sinon.assert.calledOnce(getSendRequestSpy()); + expect(error).toEqual(getErrorResponse()); + }); +}); diff --git a/src/plugins/es_ui_shared/public/request/send_request.ts b/src/plugins/es_ui_shared/public/request/send_request.ts new file mode 100644 index 0000000000000..453e91570cd85 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/send_request.ts @@ -0,0 +1,52 @@ +/* + * 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 { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public'; + +export interface SendRequestConfig { + path: string; + method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; + query?: HttpFetchQuery; + body?: any; +} + +export interface SendRequestResponse { + data: D | null; + error: E | null; +} + +export const sendRequest = async ( + httpClient: HttpSetup, + { path, method, body, query }: SendRequestConfig +): Promise> => { + try { + const stringifiedBody = typeof body === 'string' ? body : JSON.stringify(body); + const response = await httpClient[method](path, { body: stringifiedBody, query }); + + return { + data: response.data ? response.data : response, + error: null, + }; + } catch (e) { + return { + data: null, + error: e.response?.data ?? e.body, + }; + } +}; diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx new file mode 100644 index 0000000000000..0d6fd122ad22c --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -0,0 +1,184 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { mount, ReactWrapper } from 'enzyme'; +import sinon from 'sinon'; + +import { HttpSetup, HttpFetchOptions } from '../../../../../src/core/public'; +import { SendRequestConfig, SendRequestResponse } from './send_request'; +import { useRequest, UseRequestResponse, UseRequestConfig } from './use_request'; + +export interface UseRequestHelpers { + advanceTime: (ms: number) => Promise; + completeRequest: () => Promise; + hookResult: UseRequestResponse; + getSendRequestSpy: () => sinon.SinonStub; + setupSuccessRequest: (overrides?: {}, requestTimings?: number[]) => void; + getSuccessResponse: () => SendRequestResponse; + setupErrorRequest: (overrides?: {}, requestTimings?: number[]) => void; + getErrorResponse: () => SendRequestResponse; + setErrorResponse: (overrides?: {}) => void; + setupErrorWithBodyRequest: (overrides?: {}) => void; + getErrorWithBodyResponse: () => SendRequestResponse; +} + +// Each request will take 1s to resolve. +export const REQUEST_TIME = 1000; + +const successRequest: SendRequestConfig = { method: 'post', path: '/success', body: {} }; +const successResponse = { statusCode: 200, data: { message: 'Success message' } }; + +const errorValue = { statusCode: 400, statusText: 'Error message' }; +const errorRequest: SendRequestConfig = { method: 'post', path: '/error', body: {} }; +const errorResponse = { response: { data: errorValue } }; + +const errorWithBodyRequest: SendRequestConfig = { + method: 'post', + path: '/errorWithBody', + body: {}, +}; +const errorWithBodyResponse = { body: errorValue }; + +export const createUseRequestHelpers = (): UseRequestHelpers => { + // The behavior we're testing involves state changes over time, so we need finer control over + // timing. + jest.useFakeTimers(); + + const flushPromiseJobQueue = async () => { + // See https://stackoverflow.com/questions/52177631/jest-timer-and-promise-dont-work-well-settimeout-and-async-function + await Promise.resolve(); + }; + + const completeRequest = async () => { + await act(async () => { + jest.runAllTimers(); + await flushPromiseJobQueue(); + }); + }; + + const advanceTime = async (ms: number) => { + await act(async () => { + jest.advanceTimersByTime(ms); + await flushPromiseJobQueue(); + }); + }; + + let element: ReactWrapper; + // We'll use this object to observe the state of the hook and access its callback(s). + const hookResult = {} as UseRequestResponse; + const sendRequestSpy = sinon.stub(); + + const setupUseRequest = (config: UseRequestConfig, requestTimings?: number[]) => { + let requestCount = 0; + + const httpClient = { + post: (path: string, options: HttpFetchOptions) => { + return new Promise((resolve, reject) => { + // Increase the time it takes to resolve a request so we have time to inspect the hook + // as it goes through various states. + setTimeout(() => { + try { + resolve(sendRequestSpy(path, options)); + } catch (e) { + reject(e); + } + }, (requestTimings && requestTimings[requestCount++]) || REQUEST_TIME); + }); + }, + }; + + const TestComponent = ({ requestConfig }: { requestConfig: UseRequestConfig }) => { + const { isInitialRequest, isLoading, error, data, sendRequest } = useRequest( + httpClient as HttpSetup, + requestConfig + ); + + hookResult.isInitialRequest = isInitialRequest; + hookResult.isLoading = isLoading; + hookResult.error = error; + hookResult.data = data; + hookResult.sendRequest = sendRequest; + + return null; + }; + + act(() => { + element = mount(); + }); + }; + + // Set up successful request helpers. + sendRequestSpy + .withArgs(successRequest.path, { + body: JSON.stringify(successRequest.body), + query: undefined, + }) + .resolves(successResponse); + const setupSuccessRequest = (overrides = {}, requestTimings?: number[]) => + setupUseRequest({ ...successRequest, ...overrides }, requestTimings); + const getSuccessResponse = () => ({ data: successResponse.data, error: null }); + + // Set up failed request helpers. + sendRequestSpy + .withArgs(errorRequest.path, { + body: JSON.stringify(errorRequest.body), + query: undefined, + }) + .rejects(errorResponse); + const setupErrorRequest = (overrides = {}, requestTimings?: number[]) => + setupUseRequest({ ...errorRequest, ...overrides }, requestTimings); + const getErrorResponse = () => ({ + data: null, + error: errorResponse.response.data, + }); + // We'll use this to change a success response to an error response, to test how the state changes. + const setErrorResponse = (overrides = {}) => { + element.setProps({ requestConfig: { ...errorRequest, ...overrides } }); + }; + + // Set up failed request helpers with the alternative error shape. + sendRequestSpy + .withArgs(errorWithBodyRequest.path, { + body: JSON.stringify(errorWithBodyRequest.body), + query: undefined, + }) + .rejects(errorWithBodyResponse); + const setupErrorWithBodyRequest = (overrides = {}) => + setupUseRequest({ ...errorWithBodyRequest, ...overrides }); + const getErrorWithBodyResponse = () => ({ + data: null, + error: errorWithBodyResponse.body, + }); + + return { + advanceTime, + completeRequest, + hookResult, + getSendRequestSpy: () => sendRequestSpy, + setupSuccessRequest, + getSuccessResponse, + setupErrorRequest, + getErrorResponse, + setErrorResponse, + setupErrorWithBodyRequest, + getErrorWithBodyResponse, + }; +}; diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts new file mode 100644 index 0000000000000..f7902218d9314 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts @@ -0,0 +1,353 @@ +/* + * 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 { act } from 'react-dom/test-utils'; +import sinon from 'sinon'; + +import { + UseRequestHelpers, + REQUEST_TIME, + createUseRequestHelpers, +} from './use_request.test.helpers'; + +describe('useRequest hook', () => { + let helpers: UseRequestHelpers; + + beforeEach(() => { + helpers = createUseRequestHelpers(); + }); + + describe('parameters', () => { + describe('path, method, body', () => { + it('is used to send the request', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest(); + await completeRequest(); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + }); + + describe('pollIntervalMs', () => { + it('sends another request after the specified time has elapsed', async () => { + const { setupSuccessRequest, advanceTime, getSendRequestSpy } = helpers; + setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); + + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(1); + + // We need to advance (1) the pollIntervalMs and (2) the request time. + await advanceTime(REQUEST_TIME * 2); + expect(getSendRequestSpy().callCount).toBe(2); + + // We need to advance (1) the pollIntervalMs and (2) the request time. + await advanceTime(REQUEST_TIME * 2); + expect(getSendRequestSpy().callCount).toBe(3); + }); + }); + + describe('initialData', () => { + it('sets the initial data value', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest({ initialData: 'initialData' }); + expect(hookResult.data).toBe('initialData'); + + // The initial data value will be overwritten once the request resolves. + await completeRequest(); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + }); + + describe('deserializer', () => { + it('is called with the response once the request resolves', async () => { + const { setupSuccessRequest, completeRequest, getSuccessResponse } = helpers; + + const deserializer = sinon.stub(); + setupSuccessRequest({ deserializer }); + sinon.assert.notCalled(deserializer); + await completeRequest(); + + sinon.assert.calledOnce(deserializer); + sinon.assert.calledWith(deserializer, getSuccessResponse().data); + }); + + it('provides the data return value', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest({ deserializer: () => 'intercepted' }); + await completeRequest(); + expect(hookResult.data).toBe('intercepted'); + }); + }); + }); + + describe('state', () => { + describe('isInitialRequest', () => { + it('is true for the first request and false for subsequent requests', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest(); + expect(hookResult.isInitialRequest).toBe(true); + + hookResult.sendRequest(); + await completeRequest(); + expect(hookResult.isInitialRequest).toBe(false); + }); + }); + + describe('isLoading', () => { + it('represents in-flight request status', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest(); + expect(hookResult.isLoading).toBe(true); + + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + }); + }); + + describe('error', () => { + it('surfaces errors from requests', async () => { + const { setupErrorRequest, completeRequest, hookResult, getErrorResponse } = helpers; + setupErrorRequest(); + await completeRequest(); + expect(hookResult.error).toBe(getErrorResponse().error); + }); + + it('surfaces body-shaped errors from requests', async () => { + const { + setupErrorWithBodyRequest, + completeRequest, + hookResult, + getErrorWithBodyResponse, + } = helpers; + + setupErrorWithBodyRequest(); + await completeRequest(); + expect(hookResult.error).toBe(getErrorWithBodyResponse().error); + }); + + it('persists while a request is in-flight', async () => { + const { setupErrorRequest, completeRequest, hookResult, getErrorResponse } = helpers; + setupErrorRequest(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBe(getErrorResponse().error); + + act(() => { + hookResult.sendRequest(); + }); + expect(hookResult.isLoading).toBe(true); + expect(hookResult.error).toBe(getErrorResponse().error); + }); + + it('is null when the request is successful', async () => { + const { setupSuccessRequest, completeRequest, hookResult } = helpers; + setupSuccessRequest(); + expect(hookResult.error).toBeNull(); + + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBeNull(); + }); + }); + + describe('data', () => { + it('surfaces payloads from requests', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest(); + expect(hookResult.data).toBeUndefined(); + + await completeRequest(); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + + it('persists while a request is in-flight', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSuccessResponse } = helpers; + setupSuccessRequest(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.data).toBe(getSuccessResponse().data); + + act(() => { + hookResult.sendRequest(); + }); + expect(hookResult.isLoading).toBe(true); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + + it('persists from last successful request when the next request fails', async () => { + const { + setupSuccessRequest, + completeRequest, + hookResult, + getErrorResponse, + setErrorResponse, + getSuccessResponse, + } = helpers; + + setupSuccessRequest(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBeNull(); + expect(hookResult.data).toBe(getSuccessResponse().data); + + setErrorResponse(); + await completeRequest(); + expect(hookResult.isLoading).toBe(false); + expect(hookResult.error).toBe(getErrorResponse().error); + expect(hookResult.data).toBe(getSuccessResponse().data); + }); + }); + }); + + describe('callbacks', () => { + describe('sendRequest', () => { + it('sends the request', async () => { + const { setupSuccessRequest, completeRequest, hookResult, getSendRequestSpy } = helpers; + setupSuccessRequest(); + + await completeRequest(); + expect(getSendRequestSpy().callCount).toBe(1); + + await act(async () => { + hookResult.sendRequest(); + await completeRequest(); + }); + expect(getSendRequestSpy().callCount).toBe(2); + }); + + it('resets the pollIntervalMs', async () => { + const { setupSuccessRequest, advanceTime, hookResult, getSendRequestSpy } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + setupSuccessRequest({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // The initial request resolves, and then we'll immediately send a new one manually... + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(1); + act(() => { + hookResult.sendRequest(); + }); + + // The manual request resolves, and we'll send yet another one... + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(2); + act(() => { + hookResult.sendRequest(); + }); + + // At this point, we've moved forward 3s. The poll is set at 2s. If sendRequest didn't + // reset the poll, the request call count would be 4, not 3. + await advanceTime(REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(3); + }); + }); + }); + + describe('request behavior', () => { + it('outdated responses are ignored by poll requests', async () => { + const { + setupSuccessRequest, + setErrorResponse, + completeRequest, + hookResult, + getErrorResponse, + getSendRequestSpy, + } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + // Send initial request, which will have a longer round-trip time. + setupSuccessRequest({}, [DOUBLE_REQUEST_TIME]); + + // Send a new request, which will have a shorter round-trip time. + setErrorResponse(); + + // Complete both requests. + await completeRequest(); + + // Two requests were sent... + expect(getSendRequestSpy().callCount).toBe(2); + // ...but the error response is the one that takes precedence because it was *sent* more + // recently, despite the success response *returning* more recently. + expect(hookResult.error).toBe(getErrorResponse().error); + expect(hookResult.data).toBeUndefined(); + }); + + it(`outdated responses are ignored if there's a more recently-sent manual request`, async () => { + const { setupSuccessRequest, advanceTime, hookResult, getSendRequestSpy } = helpers; + + const HALF_REQUEST_TIME = REQUEST_TIME * 0.5; + setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); + + // Before the original request resolves, we make a manual sendRequest call. + await advanceTime(HALF_REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(0); + act(() => { + hookResult.sendRequest(); + }); + + // The original quest resolves but it's been marked as outdated by the the manual sendRequest + // call "interrupts", so data is left undefined. + await advanceTime(HALF_REQUEST_TIME); + expect(getSendRequestSpy().callCount).toBe(1); + expect(hookResult.data).toBeUndefined(); + }); + + it(`changing pollIntervalMs doesn't trigger a new request`, async () => { + const { setupErrorRequest, setErrorResponse, completeRequest, getSendRequestSpy } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + // Send initial request. + setupErrorRequest({ pollIntervalMs: REQUEST_TIME }); + + // Setting a new poll will schedule a second request, but not send one immediately. + setErrorResponse({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // Complete initial request. + await completeRequest(); + + // Complete scheduled poll request. + await completeRequest(); + expect(getSendRequestSpy().callCount).toBe(2); + }); + + it('when the path changes after a request is scheduled, the scheduled request is sent with that path', async () => { + const { + setupSuccessRequest, + completeRequest, + hookResult, + getErrorResponse, + setErrorResponse, + getSendRequestSpy, + } = helpers; + const DOUBLE_REQUEST_TIME = REQUEST_TIME * 2; + + // Sned first request and schedule a request, both with the success path. + setupSuccessRequest({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // Change the path to the error path, sending a second request. pollIntervalMs is the same + // so the originally scheduled poll remains cheduled. + setErrorResponse({ pollIntervalMs: DOUBLE_REQUEST_TIME }); + + // Complete the initial request, the requests by the path change, and the scheduled poll request. + await completeRequest(); + await completeRequest(); + + // If the scheduled poll request was sent to the success path, we wouldn't have an error result. + // But we do, because it was sent to the error path. + expect(getSendRequestSpy().callCount).toBe(3); + expect(hookResult.error).toBe(getErrorResponse().error); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts new file mode 100644 index 0000000000000..481843bf40e88 --- /dev/null +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -0,0 +1,161 @@ +/* + * 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 { useEffect, useCallback, useState, useRef, useMemo } from 'react'; + +import { HttpSetup } from '../../../../../src/core/public'; +import { + sendRequest as sendStatelessRequest, + SendRequestConfig, + SendRequestResponse, +} from './send_request'; + +export interface UseRequestConfig extends SendRequestConfig { + pollIntervalMs?: number; + initialData?: any; + deserializer?: (data: any) => any; +} + +export interface UseRequestResponse { + isInitialRequest: boolean; + isLoading: boolean; + error: E | null; + data?: D | null; + sendRequest: () => Promise>; +} + +export const useRequest = ( + httpClient: HttpSetup, + { path, method, query, body, pollIntervalMs, initialData, deserializer }: UseRequestConfig +): UseRequestResponse => { + const isMounted = useRef(false); + + // Main states for tracking request status and data + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [data, setData] = useState(initialData); + + // Consumers can use isInitialRequest to implement a polling UX. + const requestCountRef = useRef(0); + const isInitialRequest = requestCountRef.current === 0; + const pollIntervalIdRef = useRef(null); + + const clearPollInterval = useCallback(() => { + if (pollIntervalIdRef.current) { + clearTimeout(pollIntervalIdRef.current); + pollIntervalIdRef.current = null; + } + }, []); + + // Convert our object to string to be able to compare them in our useMemo, + // allowing the consumer to freely passed new objects to the hook on each + // render without requiring them to be memoized. + const queryStringified = query ? JSON.stringify(query) : undefined; + const bodyStringified = body ? JSON.stringify(body) : undefined; + + const requestBody = useMemo(() => { + return { + path, + method, + query: queryStringified ? query : undefined, + body: bodyStringified ? body : undefined, + }; + // queryStringified and bodyStringified stand in for query and body as dependencies. + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [path, method, queryStringified, bodyStringified]); + + const sendRequest = useCallback(async () => { + // If we're on an interval, this allows us to reset it if the user has manually requested the + // data, to avoid doubled-up requests. + clearPollInterval(); + + const requestId = ++requestCountRef.current; + + // We don't clear error or data, so it's up to the consumer to decide whether to display the + // "old" error/data or loading state when a new request is in-flight. + setIsLoading(true); + + const response = await sendStatelessRequest(httpClient, requestBody); + const { data: serializedResponseData, error: responseError } = response; + + const isOutdatedRequest = requestId !== requestCountRef.current; + const isUnmounted = isMounted.current === false; + + // Ignore outdated or irrelevant data. + if (isOutdatedRequest || isUnmounted) { + return { data: null, error: null }; + } + + setError(responseError); + // If there's an error, keep the data from the last request in case it's still useful to the user. + if (!responseError) { + const responseData = deserializer + ? deserializer(serializedResponseData) + : serializedResponseData; + setData(responseData); + } + // Setting isLoading to false also acts as a signal for scheduling the next poll request. + setIsLoading(false); + + return { data: serializedResponseData, error: responseError }; + }, [requestBody, httpClient, deserializer, clearPollInterval]); + + const scheduleRequest = useCallback(() => { + // If there's a scheduled poll request, this new one will supersede it. + clearPollInterval(); + + if (pollIntervalMs) { + pollIntervalIdRef.current = setTimeout(sendRequest, pollIntervalMs); + } + }, [pollIntervalMs, sendRequest, clearPollInterval]); + + // Send the request on component mount and whenever the dependencies of sendRequest() change. + useEffect(() => { + sendRequest(); + }, [sendRequest]); + + // Schedule the next poll request when the previous one completes. + useEffect(() => { + // When a request completes, attempt to schedule the next one. Note that we aren't re-scheduling + // a request whenever sendRequest's dependencies change. isLoading isn't set to false until the + // initial request has completed, so we won't schedule a request on mount. + if (!isLoading) { + scheduleRequest(); + } + }, [isLoading, scheduleRequest]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + + // Clean up on unmount. + clearPollInterval(); + }; + }, [clearPollInterval]); + + return { + isInitialRequest, + isLoading, + error, + data, + sendRequest, // Gives the user the ability to manually request data + }; +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx index 9fb804eb7fafa..b2f1a70341315 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/combobox_field.tsx @@ -74,7 +74,7 @@ export const ComboBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => }; const onSearchComboChange = (value: string) => { - if (value) { + if (value !== undefined) { field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); } }; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx index e02c9b6c18615..b83b0af5f97c6 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/range_field.tsx @@ -39,6 +39,8 @@ export const RangeField = ({ field, euiFieldProps = {}, ...rest }: Props) => { }>; field.onChange(event); }, + // https://github.com/elastic/kibana/issues/73972 + /* eslint-disable-next-line react-hooks/exhaustive-deps */ [field.onChange] ); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx index b3a15fea8b187..287ac56243446 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form.tsx @@ -21,6 +21,7 @@ import React, { ReactNode } from 'react'; import { EuiForm } from '@elastic/eui'; import { FormProvider } from '../form_context'; +import { FormDataContextProvider } from '../form_data_context'; import { FormHook } from '../types'; interface Props { @@ -30,8 +31,14 @@ interface Props { [key: string]: any; } -export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => ( - - - -); +export const Form = ({ form, FormWrapper = EuiForm, ...rest }: Props) => { + const { getFormData, __getFormData$ } = form; + + return ( + + + + + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx index 25448dff18e8a..d9095944eaa33 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.test.tsx @@ -75,16 +75,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - /** - * The children will be rendered three times: - * - Twice for each input value that has changed - * - once because after updating both fields, the **form** isValid state changes (from "undefined" to "true") - * causing a new "form" object to be returned and thus a re-render. - * - * When the form object will be memoized (in a future PR), te bellow call count should only be 2 as listening - * to form data changes should not receive updates when the "isValid" state of the form changes. - */ - expect(onFormData.mock.calls.length).toBe(3); + expect(onFormData).toBeCalledTimes(2); const [formDataUpdated] = onFormData.mock.calls[onFormData.mock.calls.length - 1] as Parameters< OnUpdateHandler @@ -130,7 +121,7 @@ describe('', () => { find, } = setup() as TestBed; - expect(onFormData.mock.calls.length).toBe(0); // Not present in the DOM yet + expect(onFormData).toBeCalledTimes(0); // Not present in the DOM yet // Make some changes to the form fields await act(async () => { @@ -188,7 +179,7 @@ describe('', () => { setInputValue('lastNameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); }); test('props.pathsToWatch (Array): should not re-render the children when the field that changed is not in the watch list', async () => { @@ -228,14 +219,14 @@ describe('', () => { }); // No re-render - expect(onFormData.mock.calls.length).toBe(0); + expect(onFormData).toBeCalledTimes(0); // Make some changes to fields in the watch list await act(async () => { setInputValue('nameField', 'updated value'); }); - expect(onFormData.mock.calls.length).toBe(1); + expect(onFormData).toBeCalledTimes(1); onFormData.mockReset(); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 3630b902f0564..ac141baf8fc71 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -17,10 +17,10 @@ * under the License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React from 'react'; import { FormData } from '../types'; -import { useFormContext } from '../form_context'; +import { useFormData } from '../hooks'; interface Props { children: (formData: FormData) => JSX.Element | null; @@ -28,46 +28,9 @@ interface Props { } export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { - const form = useFormContext(); - const { subscribe } = form; - const previousRawData = useRef(form.__getFormData$().value); - const isMounted = useRef(false); - const [formData, setFormData] = useState(previousRawData.current); + const { 0: formData, 2: isReady } = useFormData({ watch: pathsToWatch }); - const onFormData = useCallback( - ({ data: { raw } }) => { - // To avoid re-rendering the children for updates on the form data - // that we are **not** interested in, we can specify one or multiple path(s) - // to watch. - if (pathsToWatch) { - const valuesToWatchArray = Array.isArray(pathsToWatch) - ? (pathsToWatch as string[]) - : ([pathsToWatch] as string[]); - - if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { - previousRawData.current = raw; - setFormData(raw); - } - } else { - setFormData(raw); - } - }, - [pathsToWatch] - ); - - useEffect(() => { - const subscription = subscribe(onFormData); - return subscription.unsubscribe; - }, [subscribe, onFormData]); - - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - if (!isMounted.current && Object.keys(formData).length === 0) { + if (!isReady) { // No field has mounted yet, don't render anything return null; } diff --git a/src/legacy/ui/public/new_platform/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx similarity index 51% rename from src/legacy/ui/public/new_platform/index.ts rename to src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx index ca5890854f3aa..0e6a75e9c5065 100644 --- a/src/legacy/ui/public/new_platform/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -16,4 +16,35 @@ * specific language governing permissions and limitations * under the License. */ -export { __setup__, __start__, npSetup, npStart } from './new_platform'; + +import React, { createContext, useContext, useMemo } from 'react'; + +import { FormData, FormHook } from './types'; +import { Subject } from './lib'; + +export interface Context { + getFormData$: () => Subject; + getFormData: FormHook['getFormData']; +} + +const FormDataContext = createContext | undefined>(undefined); + +interface Props extends Context { + children: React.ReactNode; +} + +export const FormDataContextProvider = ({ children, getFormData$, getFormData }: Props) => { + const value = useMemo( + () => ({ + getFormData, + getFormData$, + }), + [getFormData, getFormData$] + ); + + return {children}; +}; + +export function useFormDataContext() { + return useContext | undefined>(FormDataContext); +} diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 6a04a592227f9..45c11dd6272e4 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -19,3 +19,4 @@ export { useField } from './use_field'; export { useForm } from './use_form'; +export { useFormData } from './use_form_data'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index 9d22e4eb2ee5e..fa29f900af2ef 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -254,6 +254,8 @@ export const useField = ( validationErrors.push({ ...validationResult, + // See comment below that explains why we add "__isBlocking__". + __isBlocking__: validationResult.__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -306,6 +308,11 @@ export const useField = ( validationErrors.push({ ...(validationResult as ValidationError), + // We add an "__isBlocking__" property to know if this error is a blocker or no. + // Most validation errors are blockers but in some cases a validation is more a warning than an error + // like with the ComboBox items when they are added. + __isBlocking__: + (validationResult as ValidationError).__isBlocking__ ?? validation.isBlocking, validationType: validationType || VALIDATION_TYPES.FIELD, }); @@ -394,7 +401,13 @@ export const useField = ( ); const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { - setErrors(_errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, ...error }))); + setErrors( + _errors.map((error) => ({ + validationType: VALIDATION_TYPES.FIELD, + __isBlocking__: true, + ...error, + })) + ); }, []); /** @@ -463,7 +476,8 @@ export const useField = ( [setValue, deserializeValue, defaultValue] ); - const isValid = errors.length === 0; + // Don't take into account non blocker validation. Some are just warning (like trying to add a wrong ComboBox item) + const isValid = errors.filter((e) => e.__isBlocking__ !== false).length === 0; const field = useMemo>(() => { return { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index 007e492243bac..4a880415b6d22 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -39,7 +39,7 @@ const onFormHook = (_form: FormHook) => { formHook = _form; }; -describe('use_form() hook', () => { +describe('useForm() hook', () => { beforeEach(() => { formHook = null; }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 35bac5b9a58c6..7b72a9eeacf7b 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -240,6 +240,12 @@ export function useForm( if (!field.isValidated) { setIsValid(undefined); + + // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. + // If a field is added and it is not validated it means that we have swapped fields and added new ones: + // --> we have basically have a new form in front of us. + // For that reason we make sure that the "isSubmitted" state is false. + setIsSubmitted(false); } }, [updateFormDataAt] @@ -389,6 +395,7 @@ export function useForm( isValid, id, submit: submitForm, + validate: validateAllFields, subscribe, setFieldValue, setFieldErrors, @@ -428,6 +435,7 @@ export function useForm( addField, removeField, validateFields, + validateAllFields, ]); useEffect(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx new file mode 100644 index 0000000000000..0fb65daecf2f4 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.test.tsx @@ -0,0 +1,234 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect } from 'react'; +import { act } from 'react-dom/test-utils'; + +import { registerTestBed, TestBed } from '../shared_imports'; +import { Form, UseField } from '../components'; +import { useForm } from './use_form'; +import { useFormData, HookReturn } from './use_form_data'; + +interface Props { + onChange(data: HookReturn): void; + watch?: string | string[]; +} + +describe('useFormData() hook', () => { + const HookListenerComp = React.memo(({ onChange, watch }: Props) => { + const hookValue = useFormData({ watch }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return null; + }); + + describe('form data updates', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should return the form data', () => { + // Called twice: + // once when the hook is called and once when the fields have mounted and updated the form data + expect(onChangeSpy).toBeCalledTimes(2); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleInitialValue' }); + }); + + test('should listen to field changes', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + expect(onChangeSpy).toBeCalledTimes(3); + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged' }); + }); + }); + + describe('format form data', () => { + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + setup({ onChange: onChangeSpy }); + }); + + test('should expose a handler to build the form data', () => { + const { 1: format } = getLastMockValue(); + expect(format()).toEqual({ + user: { + firstName: 'John', + lastName: 'Snow', + }, + }); + }); + }); + + describe('options', () => { + describe('watch', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = (props: Props) => { + const { form } = useForm(); + + return ( +
+ + + + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ watch: 'title', onChange: onChangeSpy }) as TestBed; + }); + + test('should not listen to changes on fields we are not interested in', async () => { + const { + form: { setInputValue }, + } = testBed; + + await act(async () => { + // Changing a field we are **not** interested in + setInputValue('subTitleField', 'subTitleChanged'); + // Changing a field we **are** interested in + setInputValue('titleField', 'titleChanged'); + }); + + const [data] = getLastMockValue(); + expect(data).toEqual({ title: 'titleChanged', subTitle: 'subTitleInitialValue' }); + }); + }); + + describe('form', () => { + let testBed: TestBed; + let onChangeSpy: jest.Mock; + + const getLastMockValue = () => { + return onChangeSpy.mock.calls[onChangeSpy.mock.calls.length - 1][0] as HookReturn; + }; + + const TestComp = ({ onChange }: Props) => { + const { form } = useForm(); + const hookValue = useFormData({ form }); + + useEffect(() => { + onChange(hookValue); + }, [hookValue, onChange]); + + return ( +
+ + + ); + }; + + const setup = registerTestBed(TestComp, { + memoryRouter: { wrapComponent: false }, + }); + + beforeEach(() => { + onChangeSpy = jest.fn(); + testBed = setup({ onChange: onChangeSpy }) as TestBed; + }); + + test('should allow a form to be provided when the hook is called outside of the FormDataContext', async () => { + const { + form: { setInputValue }, + } = testBed; + + const [initialData] = getLastMockValue(); + expect(initialData).toEqual({ title: 'titleInitialValue' }); + + await act(async () => { + setInputValue('titleField', 'titleChanged'); + }); + + const [updatedData] = getLastMockValue(); + expect(updatedData).toEqual({ title: 'titleChanged' }); + }); + }); + }); +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts new file mode 100644 index 0000000000000..fb4a0984438ad --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useState, useEffect, useRef, useCallback } from 'react'; + +import { FormData, FormHook } from '../types'; +import { useFormDataContext, Context } from '../form_data_context'; + +interface Options { + watch?: string | string[]; + form?: FormHook; +} + +export type HookReturn = [FormData, () => T, boolean]; + +export const useFormData = (options: Options = {}): HookReturn => { + const { watch, form } = options; + const ctx = useFormDataContext(); + + let getFormData: Context['getFormData']; + let getFormData$: Context['getFormData$']; + + if (form !== undefined) { + getFormData = form.getFormData; + getFormData$ = form.__getFormData$; + } else if (ctx !== undefined) { + ({ getFormData, getFormData$ } = ctx); + } else { + throw new Error( + 'useFormData() must be used within a or you need to pass FormHook object in the options.' + ); + } + + const initialValue = getFormData$().value; + + const previousRawData = useRef(initialValue); + const isMounted = useRef(false); + const [formData, setFormData] = useState(previousRawData.current); + + const formatFormData = useCallback(() => { + return getFormData({ unflatten: true }); + }, [getFormData]); + + useEffect(() => { + const subscription = getFormData$().subscribe((raw) => { + if (watch) { + const valuesToWatchArray = Array.isArray(watch) + ? (watch as string[]) + : ([watch] as string[]); + + if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { + previousRawData.current = raw; + // Only update the state if one of the field we watch has changed. + setFormData(raw); + } + } else { + setFormData(raw); + } + }); + return subscription.unsubscribe; + }, [getFormData$, watch]); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (!isMounted.current && Object.keys(formData).length === 0) { + // No field has mounted yet + return [formData, formatFormData, false]; + } + + return [formData, formatFormData, true]; +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts index 3079814c9ad14..8d6b57fbeb315 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/index.ts @@ -19,7 +19,7 @@ // Only export the useForm hook. The "useField" hook is for internal use // as the consumer of the library must use the component -export { useForm } from './hooks'; +export { useForm, useFormData } from './hooks'; export { getFieldValidityAndErrorMessage } from './helpers'; export * from './form_context'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index dc495f6eb56b4..4b343ec5e9f2e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -30,6 +30,7 @@ export interface FormHook { readonly isValid: boolean | undefined; readonly id: string; submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + validate: () => Promise; subscribe: (handler: OnUpdateHandler) => Subscription; setFieldValue: (fieldName: string, value: FieldValue) => void; setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; @@ -147,6 +148,7 @@ export interface ValidationError { message: string; code?: T; validationType?: string; + __isBlocking__?: boolean; [key: string]: any; } @@ -185,5 +187,11 @@ type FieldValue = unknown; export interface ValidationConfig { validator: ValidationFunc; type?: string; + /** + * By default all validation are blockers, which means that if they fail, the field is invalid. + * In some cases, like when trying to add an item to the ComboBox, if the item is not valid we want + * to show a validation error. But this validation is **not** blocking. Simply, the item has not been added. + */ + isBlocking?: boolean; exitOnFail?: boolean; } diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 8139bc6d38ab1..f55462bdc9e28 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -201,26 +201,18 @@ class TutorialUi extends React.Component { * @return {Promise} */ fetchEsHitsStatus = async (esHitsCheckConfig) => { - const searchHeader = JSON.stringify({ index: esHitsCheckConfig.index }); - const searchBody = JSON.stringify({ query: esHitsCheckConfig.query, size: 1 }); - const response = await fetch(this.props.addBasePath('/elasticsearch/_msearch'), { - method: 'post', - body: `${searchHeader}\n${searchBody}\n`, - headers: { - accept: 'application/json', - 'content-type': 'application/x-ndjson', - 'kbn-xsrf': 'kibana', - }, - credentials: 'same-origin', - }); - - if (response.status > 300) { + const { http } = getServices(); + try { + const response = await http.post('/api/home/hits_status', { + body: JSON.stringify({ + index: esHitsCheckConfig.index, + query: esHitsCheckConfig.query, + }), + }); + return response.count > 0 ? StatusCheckStates.HAS_DATA : StatusCheckStates.NO_DATA; + } catch (e) { return StatusCheckStates.ERROR; } - - const results = await response.json(); - const numHits = _.get(results, 'responses.[0].hits.hits.length', 0); - return numHits === 0 ? StatusCheckStates.NO_DATA : StatusCheckStates.HAS_DATA; }; renderInstructionSetsToggle = () => { diff --git a/src/plugins/home/server/plugin.test.ts b/src/plugins/home/server/plugin.test.ts index 33d907315e512..58103430b4d7c 100644 --- a/src/plugins/home/server/plugin.test.ts +++ b/src/plugins/home/server/plugin.test.ts @@ -19,10 +19,7 @@ import { registryForTutorialsMock, registryForSampleDataMock } from './plugin.test.mocks'; import { HomeServerPlugin } from './plugin'; -import { coreMock } from '../../../core/server/mocks'; -import { CoreSetup } from '../../../core/server'; - -type MockedKeys = { [P in keyof T]: jest.Mocked }; +import { coreMock, httpServiceMock } from '../../../core/server/mocks'; describe('HomeServerPlugin', () => { beforeEach(() => { @@ -33,8 +30,16 @@ describe('HomeServerPlugin', () => { }); describe('setup', () => { - const mockCoreSetup: MockedKeys = coreMock.createSetup(); - const initContext = coreMock.createPluginInitializerContext(); + let mockCoreSetup: ReturnType; + let initContext: ReturnType; + let routerMock: ReturnType; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + routerMock = httpServiceMock.createRouter(); + mockCoreSetup.http.createRouter.mockReturnValue(routerMock); + initContext = coreMock.createPluginInitializerContext(); + }); test('wires up tutorials provider service and returns registerTutorial and addScopedTutorialContextFactory', () => { const setup = new HomeServerPlugin(initContext).setup(mockCoreSetup, {}); @@ -52,6 +57,18 @@ describe('HomeServerPlugin', () => { expect(setup.sampleData).toHaveProperty('addAppLinksToSampleDataset'); expect(setup.sampleData).toHaveProperty('replacePanelInSampleDatasetDashboard'); }); + + test('registers the `/api/home/hits_status` route', () => { + new HomeServerPlugin(initContext).setup(mockCoreSetup, {}); + + expect(routerMock.post).toHaveBeenCalledTimes(1); + expect(routerMock.post).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/home/hits_status', + }), + expect.any(Function) + ); + }); }); describe('start', () => { diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 1050c19362ae1..a2f8eec686b21 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -28,6 +28,7 @@ import { import { UsageCollectionSetup } from '../../usage_collection/server'; import { capabilitiesProvider } from './capabilities_provider'; import { sampleDataTelemetry } from './saved_objects'; +import { registerRoutes } from './routes'; interface HomeServerPluginSetupDependencies { usageCollection?: UsageCollectionSetup; @@ -41,6 +42,10 @@ export class HomeServerPlugin implements Plugin { + router.post( + { + path: '/api/home/hits_status', + validate: { + body: schema.object({ + index: schema.string(), + query: schema.recordOf(schema.string(), schema.any()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { index, query } = req.body; + const client = context.core.elasticsearch.client; + + try { + const { body } = await client.asCurrentUser.search({ + index, + size: 1, + body: { + query, + }, + }); + const count = body.hits.hits.length; + + return res.ok({ + body: { + count, + }, + }); + } catch (e) { + return res.badRequest({ + body: e, + }); + } + }) + ); +}; diff --git a/src/legacy/ui/public/__mocks__/metadata.ts b/src/plugins/home/server/routes/index.ts similarity index 80% rename from src/legacy/ui/public/__mocks__/metadata.ts rename to src/plugins/home/server/routes/index.ts index b7f944de27463..bf492051cdedf 100644 --- a/src/legacy/ui/public/__mocks__/metadata.ts +++ b/src/plugins/home/server/routes/index.ts @@ -17,9 +17,9 @@ * under the License. */ -import { metadata as metadataImpl } from '../metadata'; +import { IRouter } from 'src/core/server'; +import { registerHitsStatusRoute } from './fetch_es_hits_status'; -export const metadata: typeof metadataImpl = { - branch: 'jest-metadata-mock-branch', - version: '42.23.26', +export const registerRoutes = (router: IRouter) => { + registerHitsStatusRoute(router); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index 80132167b7f58..ed50317aed6a0 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -46,14 +46,6 @@ jest.mock('./components/table', () => ({ }, })); -jest.mock('ui/documentation_links', () => ({ - documentationLinks: { - scriptedFields: { - painless: 'painlessDocs', - }, - }, -})); - const helpers = { redirectToRoute: () => {}, getRouteHref: () => '#', diff --git a/src/plugins/timelion/public/directives/_saved_object_finder.scss b/src/plugins/timelion/public/directives/_saved_object_finder.scss index b97dace5e9e00..e1a055a5f49e9 100644 --- a/src/plugins/timelion/public/directives/_saved_object_finder.scss +++ b/src/plugins/timelion/public/directives/_saved_object_finder.scss @@ -23,3 +23,74 @@ } } } + + +saved-object-finder { + + .list-sort-button { + border-top-left-radius: 0; + border-top-right-radius: 0; + border: none; + padding: $euiSizeS $euiSize; + font-weight: $euiFontWeightRegular; + background-color: $euiColorLightestShade; + } + + .li-striped { + li { + border: none; + } + + li:nth-child(even) { + background-color: $euiColorLightestShade; + } + + li:nth-child(odd) { + background-color: $euiColorEmptyShade; + } + + .paginate-heading { + font-weight: $euiFontWeightRegular; + color: $euiColorDarkestShade; + } + + .list-group-item { + padding: $euiSizeS $euiSize; + + ul { + padding: 0; + display: flex; + flex-direction: row; + + .finder-type { + margin-right: $euiSizeS; + } + } + + a { + display: block; + color: $euiColorPrimary; + + i { + color: shade($euiColorPrimary, 10%); + margin-right: $euiSizeS; + } + } + + &:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + &.list-group-no-results p { + margin-bottom: 0; + } + } + } + + paginate { + paginate-controls { + margin: $euiSize; + } + } +} diff --git a/src/plugins/vis_default_editor/public/components/controls/filter.tsx b/src/plugins/vis_default_editor/public/components/controls/filter.tsx index 101ca3e8ad457..c373f9331c9a7 100644 --- a/src/plugins/vis_default_editor/public/components/controls/filter.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/filter.tsx @@ -115,6 +115,7 @@ function FilterRow({ dataTestSubj={dataTestSubj} bubbleSubmitEvent={true} languageSwitcherPopoverAnchorPosition="leftDown" + size="s" /> {showCustomLabel ? ( diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js index cfbd5ecb7bf65..068e3fea8b768 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_axis_label_string.test.js @@ -19,8 +19,6 @@ import { getAxisLabelString } from './get_axis_label_string'; -jest.mock('ui/new_platform'); - describe('getAxisLabelString(interval)', () => { test('should return a valid label for 10 seconds', () => { expect(getAxisLabelString(10000)).toEqual('per 10 seconds'); diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js index c950c39f06a66..3b081d8eb7db9 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js @@ -67,7 +67,9 @@ export function PanelConfig(props) { return ( - +
+ +
); diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js index 18380680283ef..7f26701e38277 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/gauge.js @@ -330,7 +330,7 @@ class GaugePanelConfigUi extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index 03d84d67ea787..36d0e3a80e227 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -294,7 +294,7 @@ class MarkdownPanelConfigUi extends Component { ); } return ( -
+ <> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js index feb809f20995a..568194c2e0918 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/metric.js @@ -167,7 +167,7 @@ export class MetricPanelConfig extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js index 2b729e403898e..bb3f0041abca7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.js @@ -269,7 +269,7 @@ export class TablePanelConfig extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js index 3e5e335a9ea39..03da52b10f08b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.js @@ -44,6 +44,7 @@ import { import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { getDefaultQueryLanguage } from '../lib/get_default_query_language'; import { QueryBarWrapper } from '../query_bar_wrapper'; + class TimeseriesPanelConfigUi extends Component { constructor(props) { super(props); @@ -401,7 +402,7 @@ class TimeseriesPanelConfigUi extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js index 4752e0ee7de65..14c4400180280 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/top_n.js @@ -227,7 +227,7 @@ export class TopNPanelConfig extends Component { ); } return ( -
+ <> this.switchTab('data')}> {view} -
+ ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 3bf8b77621cf5..daff68e40dbae 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -44,6 +44,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js labelType="label" > { - if (filterQuery && filterQuery.language === 'kuery') { - try { - const queryOptions = this.coreContext.uiSettings.get( - UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS - ); - esKuery.fromKueryExpression(filterQuery.query, { allowLeadingWildcards: queryOptions }); - } catch (error) { - return false; - } - } - return true; - }; - handleChange = (partialModel) => { if (isEmpty(partialModel)) { return; @@ -134,6 +119,14 @@ export class VisEditor extends Component { }); }; + updateModel = () => { + const { params } = this.props.vis.clone(); + + this.setState({ + model: params, + }); + }; + handleCommit = () => { this.updateVisState(); this.setState({ dirty: false }); @@ -219,6 +212,10 @@ export class VisEditor extends Component { componentDidMount() { this.props.renderComplete(); + + if (this.props.isEditorMode && this.props.eventEmitter) { + this.props.eventEmitter.on('updateEditor', this.updateModel); + } } componentDidUpdate() { @@ -227,6 +224,10 @@ export class VisEditor extends Component { componentWillUnmount() { this.updateVisState.cancel(); + + if (this.props.isEditorMode && this.props.eventEmitter) { + this.props.eventEmitter.off('updateEditor', this.updateModel); + } } } diff --git a/src/test_utils/public/no_digest_promises.js b/src/test_utils/public/no_digest_promises.js deleted file mode 100644 index 396d6fb4892f3..0000000000000 --- a/src/test_utils/public/no_digest_promises.js +++ /dev/null @@ -1,60 +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 Bluebird from 'bluebird'; -import 'ui/promises'; -import { uiModules } from 'ui/modules'; - -Bluebird.longStackTraces(); - -/** - * replace the Promise service with Bluebird so that tests - * can use promises without having to call $rootScope.apply() - * - * import noDigestPromises from 'test_utils/no_digest_promises'; - * - * describe('some module that does complex shit with promises', function () { - * beforeEach(noDigestPromises.activate); - * - * }); - */ - -let active = false; - -uiModules.get('kibana').config(function ($provide) { - $provide.decorator('Promise', function ($delegate) { - return active ? Bluebird : $delegate; - }); -}); - -function activate() { - active = true; -} -function deactivate() { - active = false; -} - -export default { - activate: activate, - deactivate: deactivate, - activateForSuite: function () { - before(activate); - after(deactivate); - }, -}; diff --git a/src/test_utils/public/stub_get_active_injector.js b/src/test_utils/public/stub_get_active_injector.js deleted file mode 100644 index fb72cc5dabb7e..0000000000000 --- a/src/test_utils/public/stub_get_active_injector.js +++ /dev/null @@ -1,65 +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. - */ - -/** - * This test file contains stubs for chrome.dangerouslyGetActiveInjector, that you will - * need to load if any part of the code you are testing relies on that method. - * You will need to call setupInjectorStub and teardownInjectorStub in specific - * places inside your test file to setup and teardown the stub. - * If you can call both of them at the same place you can also use the shortcut - * setupAndTeardownInjectorStub instead. - */ - -import ngMock from 'ng_mock'; - -import chrome from 'ui/chrome'; -import sinon from 'sinon'; - -/** - * This method setups the stub for chrome.dangerouslyGetActiveInjector. You must call it in - * a place where beforeEach is allowed to be called (read: inside your describe) - * method. You must call this AFTER you've called `ngMock.module` to setup the modules, - * but BEFORE you first execute code, that uses chrome.dangerouslyGetActiveInjector. - */ -export function setupInjectorStub() { - beforeEach( - ngMock.inject(($injector) => { - sinon.stub(chrome, 'dangerouslyGetActiveInjector').returns(Promise.resolve($injector)); - }) - ); -} - -/** - * This methods tears down the stub for chrome.dangerouslyGetActiveInjector. You must call it - * in a place where afterEach is allowed to be called. - */ -export function teardownInjectorStub() { - afterEach(() => { - chrome.dangerouslyGetActiveInjector.restore(); - }); -} - -/** - * This method combines setupInjectorStub and teardownInjectorStub in one method. - * It can be used if you can call the other two methods directly behind each other. - */ -export function setupAndTeardownInjectorStub() { - setupInjectorStub(); - teardownInjectorStub(); -} diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 74d5798d127c3..06828e8e98cce 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -21,6 +21,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); const esArchiver = getService('esArchiver'); const log = getService('log'); const inspector = getService('inspector'); @@ -146,5 +147,79 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(newValue).to.eql('10'); }); }); + + describe('browser history changes', () => { + it('should activate previous/next chart tab and panel config', async () => { + await PageObjects.visualBuilder.resetPage(); + + log.debug('Click metric chart'); + await PageObjects.visualBuilder.clickMetric(); + await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('metric'); + + log.debug('Click Top N chart'); + await PageObjects.visualBuilder.clickTopN(); + await PageObjects.visualBuilder.checkTopNTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('top_n'); + + log.debug('Go back in browser history'); + await browser.goBack(); + + log.debug('Check metric chart and panel config is rendered'); + await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('metric'); + await PageObjects.visualBuilder.checkPanelConfigIsPresent('metric'); + + log.debug('Go back in browser history'); + await browser.goBack(); + + log.debug('Check timeseries chart and panel config is rendered'); + await PageObjects.visualBuilder.checkTimeSeriesChartIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('timeseries'); + await PageObjects.visualBuilder.checkPanelConfigIsPresent('timeseries'); + + log.debug('Go forward in browser history'); + await browser.goForward(); + + log.debug('Check metric chart and panel config is rendered'); + await PageObjects.visualBuilder.checkMetricTabIsPresent(); + await PageObjects.visualBuilder.checkTabIsSelected('metric'); + await PageObjects.visualBuilder.checkPanelConfigIsPresent('metric'); + }); + + it('should update panel config', async () => { + await PageObjects.visualBuilder.resetPage(); + + const initialLegendItems = ['Count: 156']; + const finalLegendItems = ['jpg: 106', 'css: 22', 'png: 14', 'gif: 8', 'php: 6']; + + log.debug('Group metrics by terms: extension.raw'); + await PageObjects.visualBuilder.setMetricsGroupByTerms('extension.raw'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legendItems1 = await PageObjects.visualBuilder.getLegendItemsContent(); + expect(legendItems1).to.eql(finalLegendItems); + + log.debug('Go back in browser history'); + await browser.goBack(); + const isTermsSelected = await PageObjects.visualBuilder.checkSelectedMetricsGroupByValue( + 'Terms' + ); + expect(isTermsSelected).to.be(true); + + log.debug('Go back in browser history'); + await browser.goBack(); + await PageObjects.visualBuilder.checkSelectedMetricsGroupByValue('Everything'); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legendItems2 = await PageObjects.visualBuilder.getLegendItemsContent(); + expect(legendItems2).to.eql(initialLegendItems); + + log.debug('Go forward twice in browser history'); + await browser.goForward(); + await browser.goForward(); + await PageObjects.visChart.waitForVisualizationRenderingStabilized(); + const legendItems3 = await PageObjects.visualBuilder.getLegendItemsContent(); + expect(legendItems3).to.eql(finalLegendItems); + }); + }); }); } diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index f376c39ff67bb..a95d535281fa3 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -64,6 +64,19 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } } + public async checkTabIsSelected(chartType: string) { + const chartTypeBtn = await testSubjects.find(`${chartType}TsvbTypeBtn`); + const isSelected = await chartTypeBtn.getAttribute('aria-selected'); + + if (isSelected !== 'true') { + throw new Error(`TSVB ${chartType} tab is not selected`); + } + } + + public async checkPanelConfigIsPresent(chartType: string) { + await testSubjects.existOrFail(`tvbPanelConfig__${chartType}`); + } + public async checkVisualBuilderIsPresent() { await this.checkTabIsLoaded('tvbVisEditor', 'Time Series'); } @@ -558,9 +571,40 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro return await find.allByCssSelector('.echLegendItem'); } + public async getLegendItemsContent(): Promise { + const legendList = await find.byCssSelector('.echLegendList'); + const $ = await legendList.parseDomContent(); + + return $('li') + .toArray() + .map((li) => { + const label = $(li).find('.echLegendItem__label').text(); + const value = $(li).find('.echLegendItem__extra').text(); + + return `${label}: ${value}`; + }); + } + public async getSeries(): Promise { return await find.allByCssSelector('.tvbSeriesEditor'); } + + public async setMetricsGroupByTerms(field: string) { + const groupBy = await find.byCssSelector( + '.tvbAggRow--split [data-test-subj="comboBoxInput"]' + ); + await comboBox.setElement(groupBy, 'Terms', { clickWithMouse: true }); + await PageObjects.common.sleep(1000); + const byField = await testSubjects.find('groupByField'); + await comboBox.setElement(byField, field, { clickWithMouse: true }); + } + + public async checkSelectedMetricsGroupByValue(value: string) { + const groupBy = await find.byCssSelector( + '.tvbAggRow--split [data-test-subj="comboBoxInput"]' + ); + return await comboBox.isOptionSelected(groupBy, value); + } } return new VisualBuilderPage(); 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 deleted file mode 100644 index 7aa12ea7a1130..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_vis.js +++ /dev/null @@ -1,47 +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 { SelfChangingEditor } from './self_changing_editor'; -import { SelfChangingComponent } from './self_changing_components'; - -import { npSetup } from '../../../../../../src/legacy/ui/public/new_platform'; - -npSetup.plugins.visualizations.createReactVisualization({ - name: 'self_changing_vis', - title: 'Self Changing Vis', - icon: 'controlsHorizontal', - description: - 'This visualization is able to change its own settings, that you could also set in the editor.', - visConfig: { - component: SelfChangingComponent, - defaults: { - counter: 0, - }, - }, - editorConfig: { - optionTabs: [ - { - name: 'options', - title: 'Options', - editor: SelfChangingEditor, - }, - ], - }, - requestHandler: 'none', -}); diff --git a/tsconfig.json b/tsconfig.json index 66906fb18bb80..819cb5dc4c1e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,9 +7,6 @@ "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], "plugins/*": ["src/legacy/core_plugins/*/public/"], - "ui/*": [ - "src/legacy/ui/public/*" - ], "test_utils/*": [ "src/test_utils/public/*" ], diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a0574dbdf36da..a693e008db6ea 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -13,7 +13,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, - '^ui/(.*)': `${kibanaDirectory}/src/legacy/ui/public/$1`, '^fixtures/(.*)': `${kibanaDirectory}/src/fixtures/$1`, 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index b83c5a5ff606e..854fba6624719 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -6,12 +6,9 @@ import { resolve } from 'path'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; -export { callClusterFactory } from './server/lib/call_cluster_factory'; - export const xpackMain = (kibana) => { return new kibana.Plugin({ id: 'xpack_main', @@ -25,32 +22,7 @@ export const xpackMain = (kibana) => { }).default(); }, - uiCapabilities(server) { - const featuresPlugin = server.newPlatform.setup.plugins.features; - if (!featuresPlugin) { - throw new Error('New Platform XPack Features plugin is not available.'); - } - return featuresPlugin.getFeaturesUICapabilities(); - }, - - uiExports: { - replaceInjectedVars, - injectDefaultVars(server) { - const config = server.config(); - - return { - activeSpace: null, - spacesEnabled: config.get('xpack.spaces.enabled'), - }; - }, - }, - init(server) { - const featuresPlugin = server.newPlatform.setup.plugins.features; - if (!featuresPlugin) { - throw new Error('New Platform XPack Features plugin is not available.'); - } - mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); setupXPackMain(server); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js deleted file mode 100644 index abe0d327d7b5c..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/call_cluster_factory.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { callClusterFactory } from '../call_cluster_factory'; - -describe('callClusterFactory', () => { - let mockServer; - let mockCluster; - - beforeEach(() => { - mockCluster = { - callWithRequest: sinon.stub().returns(Promise.resolve({ hits: { total: 0 } })), - callWithInternalUser: sinon.stub().returns(Promise.resolve({ hits: { total: 0 } })), - }; - mockServer = { - plugins: { - elasticsearch: { - getCluster: sinon.stub().withArgs('admin').returns(mockCluster), - }, - }, - log() {}, - }; - }); - - it('returns an object with getter methods', () => { - const _callClusterFactory = callClusterFactory(mockServer); - expect(_callClusterFactory).to.be.an('object'); - expect(_callClusterFactory.getCallClusterWithReq).to.be.an('function'); - expect(_callClusterFactory.getCallClusterInternal).to.be.an('function'); - expect(mockCluster.callWithRequest.called).to.be(false); - expect(mockCluster.callWithInternalUser.called).to.be(false); - }); - - describe('getCallClusterWithReq', () => { - it('throws an error if req is not passed', async () => { - const runCallCluster = () => callClusterFactory(mockServer).getCallClusterWithReq(); - expect(runCallCluster).to.throwException(); - }); - - it('returns a method that wraps callWithRequest', async () => { - const mockReq = { - headers: { - authorization: 'Basic dSQzcm5AbTM6cEAkJHcwcmQ=', // u$3rn@m3:p@$$w0rd - }, - }; - const callCluster = callClusterFactory(mockServer).getCallClusterWithReq(mockReq); - - const result = await callCluster('search', { body: { match: { match_all: {} } } }); - expect(result).to.eql({ hits: { total: 0 } }); - - expect(mockCluster.callWithInternalUser.called).to.be(false); - expect(mockCluster.callWithRequest.calledOnce).to.be(true); - const [req, method] = mockCluster.callWithRequest.getCall(0).args; - expect(req).to.be(mockReq); - expect(method).to.be('search'); - }); - }); - - describe('getCallClusterInternal', () => { - it('returns a method that wraps callWithInternalUser', async () => { - const callCluster = callClusterFactory(mockServer).getCallClusterInternal(); - - const result = await callCluster('search', { body: { match: { match_all: {} } } }); - expect(result).to.eql({ hits: { total: 0 } }); - - expect(mockCluster.callWithRequest.called).to.be(false); - expect(mockCluster.callWithInternalUser.calledOnce).to.be(true); - const [method] = mockCluster.callWithInternalUser.getCall(0).args; - expect(method).to.eql('search'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js deleted file mode 100644 index 420f3b2d6631c..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/inject_xpack_info_signature.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import sinon from 'sinon'; -import expect from '@kbn/expect'; -import { injectXPackInfoSignature } from '../inject_xpack_info_signature'; - -describe('injectXPackInfoSignature()', () => { - class MockErrorResponse extends Error { - constructor() { - super(); - this.output = { - headers: {}, - }; - - this.headers = {}; - } - } - - const fakeH = { continue: 'blah' }; - - let mockXPackInfo; - beforeEach(() => { - mockXPackInfo = sinon.stub({ - isAvailable() {}, - getSignature() {}, - refreshNow() {}, - }); - }); - - describe('error response', () => { - it('refreshes `xpackInfo` and do not inject signature if it is not available.', async () => { - mockXPackInfo.isAvailable.returns(true); - mockXPackInfo.getSignature.returns('this-should-never-be-set'); - - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - mockXPackInfo.refreshNow = () => { - return new Promise((resolve) => { - mockXPackInfo.isAvailable.returns(false); - resolve(); - }); - }; - - const mockResponse = new MockErrorResponse(); - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({}); - expect(mockResponse.output.headers).to.eql({}); - expect(response).to.be(fakeH.continue); - }); - - it('refreshes `xpackInfo` and injects its updated signature.', async () => { - mockXPackInfo.isAvailable.returns(true); - mockXPackInfo.getSignature.returns('old-signature'); - - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - mockXPackInfo.refreshNow = () => { - return new Promise((resolve) => { - mockXPackInfo.getSignature.returns('new-signature'); - resolve(); - }); - }; - - const mockResponse = new MockErrorResponse(); - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({}); - expect(mockResponse.output.headers).to.eql({ - 'kbn-xpack-sig': 'new-signature', - }); - expect(response).to.be(fakeH.continue); - }); - }); - - describe('non-error response', () => { - it('do not inject signature if `xpackInfo` is not available.', async () => { - mockXPackInfo.isAvailable.returns(false); - mockXPackInfo.getSignature.returns('this-should-never-be-set'); - - const mockResponse = { headers: {}, output: { headers: {} } }; - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({}); - expect(mockResponse.output.headers).to.eql({}); - sinon.assert.notCalled(mockXPackInfo.refreshNow); - expect(response).to.be(fakeH.continue); - }); - - it('injects signature if `xpackInfo` is available.', async () => { - mockXPackInfo.isAvailable.returns(true); - mockXPackInfo.getSignature.returns('available-signature'); - - const mockResponse = { headers: {}, output: { headers: {} } }; - const response = await injectXPackInfoSignature( - mockXPackInfo, - { response: mockResponse }, - fakeH - ); - - expect(mockResponse.headers).to.eql({ - 'kbn-xpack-sig': 'available-signature', - }); - expect(mockResponse.output.headers).to.eql({}); - sinon.assert.notCalled(mockXPackInfo.refreshNow); - expect(response).to.be(fakeH.continue); - }); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js deleted file mode 100644 index ce6e20bd874b2..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ /dev/null @@ -1,225 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { replaceInjectedVars } from '../replace_injected_vars'; -import { KibanaRequest } from '../../../../../../../src/core/server'; - -const buildRequest = (path = '/app/kibana') => { - const get = sinon.stub(); - - return { - app: {}, - path, - route: { settings: {} }, - headers: {}, - raw: { - req: { - socket: {}, - }, - }, - getSavedObjectsClient: () => { - return { - get, - create: sinon.stub(), - - errors: { - isNotFoundError: (error) => { - return error.message === 'not found exception'; - }, - }, - }; - }, - }; -}; - -describe('replaceInjectedVars uiExport', () => { - it('sends xpack info if request is authenticated and license is not basic', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - - sinon.assert.calledOnce(server.newPlatform.setup.plugins.security.authc.isAuthenticated); - sinon.assert.calledWithExactly( - server.newPlatform.setup.plugins.security.authc.isAuthenticated, - sinon.match.instanceOf(KibanaRequest) - ); - }); - - it('sends the xpack info if security plugin is disabled', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - delete server.plugins.security; - delete server.newPlatform.setup.plugins.security; - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('sends the xpack info if xpack license is basic', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('respects the telemetry opt-in document when opted-out', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('respects the telemetry opt-in document when opted-in', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('indicates that telemetry is opted-out when not loading an application', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(true, '/'); - const server = mockServer(); - server.plugins.xpack_main.info.license.isOneOf.returns(true); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: { - b: 1, - }, - }); - }); - - it('sends the originalInjectedVars if not authenticated', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.newPlatform.setup.plugins.security.authc.isAuthenticated.returns(false); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql(originalInjectedVars); - }); - - it('sends the originalInjectedVars if xpack info is unavailable', async () => { - const originalInjectedVars = { a: 1 }; - const request = buildRequest(); - const server = mockServer(); - server.plugins.xpack_main.info.isAvailable.returns(false); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql(originalInjectedVars); - }); - - it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => { - const originalInjectedVars = { - a: 1, - uiCapabilities: { navLinks: { foo: true }, bar: { baz: true }, catalogue: { cfoo: true } }, - }; - const request = buildRequest(); - const server = mockServer(); - delete server.plugins.security; - server.plugins.xpack_main.info.isAvailable.returns(false); - - const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.eql({ - a: 1, - xpackInitialInfo: undefined, - uiCapabilities: { - navLinks: { foo: true }, - bar: { baz: true }, - catalogue: { - cfoo: true, - }, - }, - }); - }); -}); - -// creates a mock server object that defaults to being authenticated with a -// non-basic license -function mockServer() { - const getLicenseCheckResults = sinon.stub().returns({}); - return { - newPlatform: { - setup: { - plugins: { security: { authc: { isAuthenticated: sinon.stub().returns(true) } } }, - }, - }, - plugins: { - security: {}, - xpack_main: { - getFeatures: () => [ - { - id: 'mockFeature', - name: 'Mock Feature', - privileges: { - all: { - app: [], - savedObject: { - all: [], - read: [], - }, - ui: ['mockFeatureCapability'], - }, - }, - }, - ], - info: { - isAvailable: sinon.stub().returns(true), - feature: () => ({ - getLicenseCheckResults, - }), - license: { - isOneOf: sinon.stub().returns(false), - }, - toJSON: () => ({ b: 1 }), - }, - }, - }, - }; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js index b4a2c090d6309..c34e27642d2ce 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js @@ -8,7 +8,6 @@ import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; import { XPackInfo } from '../xpack_info'; import { setupXPackMain } from '../setup_xpack_main'; -import * as InjectXPackInfoSignatureNS from '../inject_xpack_info_signature'; describe('setupXPackMain()', () => { const sandbox = sinon.createSandbox(); @@ -19,7 +18,6 @@ describe('setupXPackMain()', () => { beforeEach(() => { sandbox.useFakeTimers(); - sandbox.stub(InjectXPackInfoSignatureNS, 'injectXPackInfoSignature'); mockElasticsearchPlugin = { getCluster: sinon.stub(), @@ -63,29 +61,9 @@ describe('setupXPackMain()', () => { setupXPackMain(mockServer); sinon.assert.calledWithExactly(mockServer.expose, 'info', sinon.match.instanceOf(XPackInfo)); - sinon.assert.calledWithExactly(mockServer.ext, 'onPreResponse', sinon.match.func); sinon.assert.calledWithExactly(mockElasticsearchPlugin.status.on, 'change', sinon.match.func); }); - it('onPreResponse hook calls `injectXPackInfoSignature` for every request.', () => { - setupXPackMain(mockServer); - - const xPackInfo = mockServer.expose.firstCall.args[1]; - const onPreResponse = mockServer.ext.firstCall.args[1]; - - const mockRequest = {}; - const mockReply = sinon.stub(); - - onPreResponse(mockRequest, mockReply); - - sinon.assert.calledWithExactly( - InjectXPackInfoSignatureNS.injectXPackInfoSignature, - xPackInfo, - sinon.match.same(mockRequest), - sinon.match.same(mockReply) - ); - }); - describe('Elasticsearch plugin state changes cause XPackMain plugin state change.', () => { let xPackInfo; let onElasticsearchPluginStatusChange; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js b/x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js deleted file mode 100644 index f946725e017ff..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/call_cluster_factory.js +++ /dev/null @@ -1,45 +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. - */ - -/* - * Factory for acquiring a method that can send an authenticated query to Elasticsearch. - * - * The method can either: - * - take authentication from an HTTP request object, which should be used when - * the caller is an HTTP API - * - fabricate authentication using the system username and password from the - * Kibana config, which should be used when the caller is an internal process - * - * @param {Object} server: the Kibana server object - * @return {Function}: callCluster function - */ -export function callClusterFactory(server) { - const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( - 'admin' - ); - - return { - /* - * caller is coming from a request, so use callWithRequest with actual - * Authorization header credentials - * @param {Object} req: HTTP request object - */ - getCallClusterWithReq(req) { - if (req === undefined) { - throw new Error('request object is required'); - } - return (...args) => callWithRequest(req, ...args); - }, - - getCallClusterInternal() { - /* - * caller is an internal function of the stats collection system, so use - * internal system user - */ - return callWithInternalUser; - }, - }; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js b/x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js deleted file mode 100644 index 166bd2b4755f0..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/inject_xpack_info_signature.js +++ /dev/null @@ -1,25 +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. - */ - -export async function injectXPackInfoSignature(info, request, h) { - // If we're returning an error response, refresh xpack info from - // Elasticsearch in case the error is due to a change in license information - // in Elasticsearch. - const isErrorResponse = request.response instanceof Error; - if (isErrorResponse) { - await info.refreshNow(); - } - - if (info.isAvailable()) { - // Note: request.response.output is used instead of request.response because - // evidently HAPI does not allow headers to be set on the latter in case of - // error responses. - const response = isErrorResponse ? request.response.output : request.response; - response.headers['kbn-xpack-sig'] = info.getSignature(); - } - - return h.continue; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js deleted file mode 100644 index f09f97d44bfe8..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js +++ /dev/null @@ -1,38 +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 { KibanaRequest } from '../../../../../../src/core/server'; - -export async function replaceInjectedVars(originalInjectedVars, request, server) { - const xpackInfo = server.plugins.xpack_main.info; - - const withXpackInfo = async () => ({ - ...originalInjectedVars, - xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined, - }); - - // security feature is disabled - if (!server.plugins.security || !server.newPlatform.setup.plugins.security) { - return await withXpackInfo(); - } - - // not enough license info to make decision one way or another - if (!xpackInfo.isAvailable()) { - return originalInjectedVars; - } - - // request is not authenticated - if ( - !(await server.newPlatform.setup.plugins.security.authc.isAuthenticated( - KibanaRequest.from(request) - )) - ) { - return originalInjectedVars; - } - - // plugin enabled, license is appropriate, request is authenticated - return await withXpackInfo(); -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 9196b210bba20..33b551bbe864f 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectXPackInfoSignature } from './inject_xpack_info_signature'; import { XPackInfo } from './xpack_info'; /** @@ -20,11 +19,6 @@ export function setupXPackMain(server) { server.expose('info', info); - server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); - - const { getFeatures } = server.newPlatform.setup.plugins.features; - server.expose('getFeatures', getFeatures); - const setPluginStatus = () => { if (info.isAvailable()) { server.plugins.xpack_main.status.green('Ready'); diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index 2a59c2f1366d4..f4363a8e57b37 100644 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts @@ -11,5 +11,4 @@ export { XPackFeature } from './lib/xpack_info'; export interface XPackMainPlugin { info: XPackInfo; - getFeatures(): Feature[]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 844aa6d2de7ed..7e938e766657c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -65,7 +65,7 @@ describe('request', () => { logger, proxySettings: { proxyUrl: 'http://localhost:1212', - rejectUnauthorizedCertificates: false, + proxyRejectUnauthorizedCertificates: false, }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts index 2468fab8c6ac5..8623a67e8a68e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts @@ -14,7 +14,7 @@ const logger = loggingSystemMock.create().get() as jest.Mocked; describe('getProxyAgent', () => { test('return HttpsProxyAgent for https proxy url', () => { const agent = getProxyAgent( - { proxyUrl: 'https://someproxyhost', rejectUnauthorizedCertificates: false }, + { proxyUrl: 'https://someproxyhost', proxyRejectUnauthorizedCertificates: false }, logger ); expect(agent instanceof HttpsProxyAgent).toBeTruthy(); @@ -22,7 +22,7 @@ describe('getProxyAgent', () => { test('return HttpProxyAgent for http proxy url', () => { const agent = getProxyAgent( - { proxyUrl: 'http://someproxyhost', rejectUnauthorizedCertificates: false }, + { proxyUrl: 'http://someproxyhost', proxyRejectUnauthorizedCertificates: false }, logger ); expect(agent instanceof HttpProxyAgent).toBeTruthy(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts index bb4dadd3a4698..957d31546b019 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts @@ -23,7 +23,7 @@ export function getProxyAgent( protocol: proxyUrl.protocol, headers: proxySettings.proxyHeaders, // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings.rejectUnauthorizedCertificates, + rejectUnauthorized: proxySettings.proxyRejectUnauthorizedCertificates, }); } else { return new HttpProxyAgent(proxySettings.proxyUrl); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index f69a2fc1d209c..b6c4a4ea882e5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -73,7 +73,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://example.com', - rejectUnauthorizedCertificates: false, + proxyRejectUnauthorizedCertificates: false, } ); // @ts-expect-error @@ -140,6 +140,9 @@ describe('send_email module', () => { "host": "example.com", "port": 1025, "secure": false, + "tls": Object { + "rejectUnauthorized": undefined, + }, }, ] `); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index a4f32f1880cb5..dead8fee63d4f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -19,6 +19,7 @@ export interface SendEmailOptions { routing: Routing; content: Content; proxySettings?: ProxySettings; + rejectUnauthorized?: boolean; } // config validation ensures either service is set or host/port are set @@ -45,7 +46,7 @@ export interface Content { // send an email export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { - const { transport, routing, content, proxySettings } = options; + const { transport, routing, content, proxySettings, rejectUnauthorized } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; @@ -68,15 +69,18 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.host = host; transportConfig.port = port; transportConfig.secure = !!secure; - if (proxySettings && !transportConfig.secure) { + + if (proxySettings) { transportConfig.tls = { // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings?.rejectUnauthorizedCertificates, + rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates, }; - } - if (proxySettings) { transportConfig.proxy = proxySettings.proxyUrl; transportConfig.headers = proxySettings.proxyHeaders; + } else if (!transportConfig.secure) { + transportConfig.tls = { + rejectUnauthorized, + }; } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index b15d92cecba62..d98a41ed1f355 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -167,7 +167,7 @@ describe('execute()', () => { params: { message: 'this invocation should succeed' }, proxySettings: { proxyUrl: 'https://someproxyhost', - rejectUnauthorizedCertificates: false, + proxyRejectUnauthorizedCertificates: false, }, }); expect(response).toMatchInlineSnapshot(` @@ -206,7 +206,7 @@ describe('execute()', () => { params: { message: 'this invocation should succeed' }, proxySettings: { proxyUrl: 'https://someproxyhost', - rejectUnauthorizedCertificates: false, + proxyRejectUnauthorizedCertificates: false, }, }); expect(mockedLogger.debug).toHaveBeenCalledWith( diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index ac815a425a2b7..1d7edd5f6b38f 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -18,7 +18,8 @@ describe('config validation', () => { "*", ], "preconfigured": Object {}, - "rejectUnauthorizedCertificates": true, + "proxyRejectUnauthorizedCertificates": true, + "rejectUnauthorized": true, } `); }); @@ -34,7 +35,8 @@ describe('config validation', () => { }, }, }, - rejectUnauthorizedCertificates: false, + proxyRejectUnauthorizedCertificates: false, + rejectUnauthorized: false, }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { @@ -55,7 +57,8 @@ describe('config validation', () => { "secrets": Object {}, }, }, - "rejectUnauthorizedCertificates": false, + "proxyRejectUnauthorizedCertificates": false, + "rejectUnauthorized": false, } `); }); diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 087a08f572c65..8823cea9f4452 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -34,7 +34,8 @@ export const configSchema = schema.object({ }), proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), - rejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), + proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), + rejectUnauthorized: schema.boolean({ defaultValue: true }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index fef70c3a48455..31c4d26d1793e 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; import { configSchema } from './config'; import { ActionsClient as ActionsClientClass } from './actions_client'; import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization'; +import { ActionsConfigType } from './types'; export type ActionsClient = PublicMethodsOf; export type ActionsAuthorization = PublicMethodsOf; @@ -24,6 +25,9 @@ export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); -export const config = { +export const config: PluginConfigDescriptor = { schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), + ], }; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 4fdf9f2523568..9d545600e61ee 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -34,7 +34,8 @@ describe('Actions Plugin', () => { enabledActionTypes: ['*'], allowedHosts: ['*'], preconfigured: {}, - rejectUnauthorizedCertificates: true, + proxyRejectUnauthorizedCertificates: true, + rejectUnauthorized: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -195,7 +196,8 @@ describe('Actions Plugin', () => { secrets: {}, }, }, - rejectUnauthorizedCertificates: true, + proxyRejectUnauthorizedCertificates: true, + rejectUnauthorized: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 413e6663105b8..a6c5899281658 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -323,7 +323,8 @@ export class ActionsPlugin implements Plugin, Plugi ? { proxyUrl: this.actionsConfig.proxyUrl, proxyHeaders: this.actionsConfig.proxyHeaders, - rejectUnauthorizedCertificates: this.actionsConfig.rejectUnauthorizedCertificates, + proxyRejectUnauthorizedCertificates: this.actionsConfig + .proxyRejectUnauthorizedCertificates, } : undefined, }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 0a7d6bf01b7ec..3e92ca331bb93 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -145,5 +145,5 @@ export interface ActionTaskExecutorParams { export interface ProxySettings { proxyUrl: string; proxyHeaders?: Record; - rejectUnauthorizedCertificates: boolean; + proxyRejectUnauthorizedCertificates: boolean; } diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/common/utils/formatters.test.ts similarity index 96% rename from x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts rename to x-pack/plugins/apm/common/utils/formatters.test.ts index 66101baf3a746..ce317c5a6a827 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts +++ b/x-pack/plugins/apm/common/utils/formatters.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { asPercent } from '../formatters'; +import { asPercent } from './formatters'; describe('formatters', () => { describe('asPercent', () => { diff --git a/x-pack/plugins/apm/common/utils/formatters.ts b/x-pack/plugins/apm/common/utils/formatters.ts new file mode 100644 index 0000000000000..f7c609d949adf --- /dev/null +++ b/x-pack/plugins/apm/common/utils/formatters.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 numeral from '@elastic/numeral'; + +export function asPercent( + numerator: number, + denominator: number | undefined, + fallbackResult = '' +) { + if (!denominator || isNaN(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + + // 33.2 => 33% + // 3.32 => 3.3% + // 0 => 0% + if (Math.abs(decimal) >= 0.1 || decimal === 0) { + return numeral(decimal).format('0%'); + } + + return numeral(decimal).format('0.0%'); +} diff --git a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled index be1597c8340eb..727898773904e 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled +++ b/x-pack/plugins/apm/e2e/cypress/integration/rum_dashboard.feature.disabled @@ -12,20 +12,14 @@ Feature: RUM Dashboard | os | | location | - Scenario: Page load distribution percentiles + Scenario: Display RUM Data components When a user browses the APM UI application for RUM Data Then should display percentile for page load chart - - Scenario: Page load distribution chart tooltip - When a user browses the APM UI application for RUM Data - Then should display tooltip on hover - - Scenario: Page load distribution chart legends - When a user browses the APM UI application for RUM Data - Then should display chart legend + And should display tooltip on hover + And should display chart legend Scenario: Breakdown filter - Given a user click page load breakdown filter + Given a user clicks the page load breakdown filter When the user selected the breakdown Then breakdown series should appear in chart diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts new file mode 100644 index 0000000000000..1cc36059b8ff8 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/client_metrics_helper.ts @@ -0,0 +1,37 @@ +/* + * 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 { DEFAULT_TIMEOUT } from './rum_dashboard'; + +/** + * Verifies the behavior of the client metrics component + * @param metrics array of three elements + * @param checkTitleStatus if it's needed to check title elements + */ +export function verifyClientMetrics( + metrics: string[], + checkTitleStatus: boolean +) { + const clientMetricsSelector = '[data-cy=client-metrics] .euiStat__title'; + + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + if (checkTitleStatus) { + cy.get('.euiStat__title', { timeout: DEFAULT_TIMEOUT }).should( + 'be.visible' + ); + cy.get('.euiSelect-isLoading').should('not.be.visible'); + } + + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + + cy.get(clientMetricsSelector).eq(0).should('have.text', metrics[0]); + + cy.get(clientMetricsSelector).eq(1).should('have.text', metrics[1]); + + cy.get(clientMetricsSelector).eq(2).should('have.text', metrics[2]); +} diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts index f319f7ef98667..d671bdc0078eb 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/page_load_dist.ts @@ -9,7 +9,7 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; /** The default time in ms to wait for a Cypress command to complete */ export const DEFAULT_TIMEOUT = 60 * 1000; -Given(`a user click page load breakdown filter`, () => { +Given(`a user clicks the page load breakdown filter`, () => { // wait for all loading to finish cy.get('kbnLoadingIndicator').should('not.be.visible'); cy.get('.euiStat__title-isLoading').should('not.be.visible'); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts index 8e010d5180f88..804974d8d437d 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/rum_dashboard.ts @@ -6,6 +6,7 @@ import { Given, Then } from 'cypress-cucumber-preprocessor/steps'; import { loginAndWaitForPage } from '../../../integration/helpers'; +import { verifyClientMetrics } from './client_metrics_helper'; /** The default time in ms to wait for a Cypress command to complete */ export const DEFAULT_TIMEOUT = 60 * 1000; @@ -21,19 +22,9 @@ Given(`a user browses the APM UI application for RUM Data`, () => { }); Then(`should have correct client metrics`, () => { - const clientMetrics = '[data-cy=client-metrics] .euiStat__title'; + const metrics = ['0.01 sec', '0.08 sec', '55 ']; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title', { timeout: DEFAULT_TIMEOUT }).should('be.visible'); - cy.get('.euiSelect-isLoading').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); - - cy.get(clientMetrics).eq(2).should('have.text', '55 '); - - cy.get(clientMetrics).eq(1).should('have.text', '0.08 sec'); - - cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); + verifyClientMetrics(metrics, true); }); Then(`should display percentile for page load chart`, () => { diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts index b0694c902085a..68fc4d528543a 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/rum/service_name_filter.ts @@ -6,6 +6,7 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; import { DEFAULT_TIMEOUT } from '../apm'; +import { verifyClientMetrics } from './client_metrics_helper'; When('a user changes the selected service name', (filterName) => { // wait for all loading to finish @@ -16,15 +17,7 @@ When('a user changes the selected service name', (filterName) => { }); Then(`it displays relevant client metrics`, () => { - const clientMetrics = '[data-cy=client-metrics] .euiStat__title'; + const metrics = ['0.01 sec', '0.07 sec', '7 ']; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); - - cy.get(clientMetrics).eq(2).should('have.text', '7 '); - - cy.get(clientMetrics).eq(1).should('have.text', '0.07 sec'); - - cy.get(clientMetrics).eq(0).should('have.text', '0.01 sec'); + verifyClientMetrics(metrics, false); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index be52018735099..ba4451c37b304 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,8 +8,9 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; +import { asPercent } from '../../../../../common/utils/formatters'; import { ServiceNodeStats } from '../../../../../common/service_map'; -import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; +import { asDuration, tpmUnit } from '../../../../utils/formatters'; export const ItemRow = styled('tr')` line-height: 2; diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 3cde48aa483cb..9940a7aabb219 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -13,6 +13,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { asPercent } from '../../../../common/utils/formatters'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { Projection } from '../../../../common/projections'; @@ -20,11 +21,7 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; import { useFetcher } from '../../../hooks/useFetcher'; -import { - asDynamicBytes, - asInteger, - asPercent, -} from '../../../utils/formatters'; +import { asDynamicBytes, asInteger } from '../../../utils/formatters'; import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { truncate, px, unit } from '../../../style/variables'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx index 1725a13f4da66..41c958838803e 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { asPercent } from '../../../../utils/formatters'; +import { asPercent } from '../../../../../common/utils/formatters'; interface PercentOfParentProps { duration: number; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 0dc2f607b1ef2..515fcbc88c901 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -5,32 +5,29 @@ */ import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, EuiPanel, EuiSpacer, EuiTitle, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { EuiFlexGrid } from '@elastic/eui'; +import { useTrackPageview } from '../../../../../observability/public'; +import { Projection } from '../../../../common/projections'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { useLocation } from '../../../hooks/useLocation'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; +import { useUrlParams } from '../../../hooks/useUrlParams'; import { useWaterfall } from '../../../hooks/useWaterfall'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { ApmHeader } from '../../shared/ApmHeader'; +import { TransactionCharts } from '../../shared/charts/TransactionCharts'; +import { HeightRetainer } from '../../shared/HeightRetainer'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionDistribution } from './Distribution'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; -import { useLocation } from '../../../hooks/useLocation'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { HeightRetainer } from '../../shared/HeightRetainer'; -import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; export function TransactionDetails() { const location = useLocation(); @@ -86,21 +83,9 @@ export function TransactionDetails() { - - - - - - - - - - - diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 0757ae572152c..81d8a6f807375 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -7,7 +7,6 @@ import { EuiCallOut, EuiCode, - EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, @@ -29,13 +28,11 @@ import { useServiceTransactionTypes } from '../../../hooks/useServiceTransaction import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { ErroneousTransactionsRateChart } from '../../shared/charts/ErroneousTransactionsRateChart'; import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; -import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { TransactionList } from './List'; import { useRedirect } from './useRedirect'; @@ -125,20 +122,8 @@ export function TransactionOverview() { - - - - - - - - - - - diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 209657971620b..b908eb8da4d03 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; import { throttle } from 'lodash'; +import React, { useMemo } from 'react'; +import { asPercent } from '../../../../../common/utils/formatters'; +import { useUiTracker } from '../../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { Maybe } from '../../../../../typings/common'; -import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; -import { asPercent } from '../../../../utils/formatters'; -import { unit } from '../../../../style/variables'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { useUiTracker } from '../../../../../../observability/public'; +import { getEmptySeries } from '../../charts/CustomPlot/getEmptySeries'; +import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; interface Props { timeseries: TimeSeries[]; + noHits: boolean; } const tickFormatY = (y: Maybe) => { @@ -29,8 +31,9 @@ const formatTooltipValue = (coordinate: Coordinate) => { : NOT_AVAILABLE_LABEL; }; -function TransactionBreakdownGraph(props: Props) { - const { timeseries } = props; +function TransactionBreakdownGraph({ timeseries, noHits }: Props) { + const { urlParams } = useUrlParams(); + const { rangeFrom, rangeTo } = urlParams; const trackApmEvent = useUiTracker({ app: 'apm' }); const handleHover = useMemo( () => @@ -38,15 +41,23 @@ function TransactionBreakdownGraph(props: Props) { [trackApmEvent] ); + const emptySeries = + rangeFrom && rangeTo + ? getEmptySeries( + new Date(rangeFrom).getTime(), + new Date(rangeTo).getTime() + ) + : []; + return ( ); } diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx deleted file mode 100644 index d3761cf0fe38e..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ /dev/null @@ -1,75 +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 React from 'react'; -import { - EuiFlexGrid, - EuiFlexItem, - EuiFlexGroup, - EuiText, - EuiTitle, - EuiIcon, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { asPercent } from '../../../utils/formatters'; - -interface TransactionBreakdownKpi { - name: string; - percentage: number; - color: string; -} - -interface Props { - kpis: TransactionBreakdownKpi[]; -} - -const Description = styled.span` - { - white-space: nowrap; - } -`; - -function KpiDescription({ name, color }: { name: string; color: string }) { - return ( - - - - - - - {name} - - - - ); -} - -function TransactionBreakdownKpiList({ kpis }: Props) { - return ( - - {kpis.map((kpi) => ( - - - - - - - - {asPercent(kpi.percentage, 1)} - - - - - ))} - - ); -} - -export { TransactionBreakdownKpiList }; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx index 80ed9163ec08d..55826497ca385 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -3,28 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiText, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; import React from 'react'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown'; import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; -import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList'; - -const emptyMessage = i18n.translate('xpack.apm.transactionBreakdown.noData', { - defaultMessage: 'No data within this time range.', -}); function TransactionBreakdown() { const { data, status } = useTransactionBreakdown(); - const { kpis, timeseries } = data; - const noHits = data.kpis.length === 0 && status === FETCH_STATUS.SUCCESS; + const { timeseries } = data; + const noHits = isEmpty(timeseries) && status === FETCH_STATUS.SUCCESS; return ( @@ -39,14 +29,7 @@ function TransactionBreakdown() { - {noHits ? ( - {emptyMessage} - ) : ( - - )} - - - + diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js index 41925d651e361..501d30b5e2ba1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -67,7 +67,7 @@ export class InnerCustomPlot extends PureComponent { (series) => { return series.slice( 0, - VISIBLE_LEGEND_COUNT + getHiddenLegendCount(series) + this.props.visibleLegendCount + getHiddenLegendCount(series) ); } ); @@ -128,14 +128,20 @@ export class InnerCustomPlot extends PureComponent { } render() { - const { series, truncateLegends, width, annotations } = this.props; + const { + series, + truncateLegends, + width, + annotations, + visibleLegendCount, + } = this.props; if (!width) { return null; } const hiddenSeriesCount = Math.max( - series.length - VISIBLE_LEGEND_COUNT - getHiddenLegendCount(series), + series.length - visibleLegendCount - getHiddenLegendCount(series), 0 ); const visibleSeries = this.getVisibleSeries({ series }); @@ -239,6 +245,7 @@ InnerCustomPlot.propTypes = { }) ), noHits: PropTypes.bool, + visibleLegendCount: PropTypes.number, onToggleLegend: PropTypes.func, }; @@ -249,6 +256,7 @@ InnerCustomPlot.defaultProps = { truncateLegends: false, xAxisTickSizeOuter: 0, noHits: false, + visibleLegendCount: VISIBLE_LEGEND_COUNT, }; export default makeWidthFlexible(InnerCustomPlot); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index 7721375a404c4..67b7fd31b05bc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -19,7 +19,7 @@ const XY_HEIGHT = unit * 16; const XY_MARGIN = { top: unit, left: unit * 5, - right: 0, + right: unit, bottom: unit * 2, }; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index f413610ebd984..20636fa144479 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -82,7 +82,7 @@ Array [ className="rv-xy-plot__axis__line" style={Object {}} x1={0} - x2={720} + x2={704} y1={0} y2={0} /> @@ -93,7 +93,7 @@ Array [ @@ -434,7 +434,7 @@ Array [ > `; @@ -3018,7 +3018,7 @@ Array [ className="rv-xy-plot__axis__line" style={Object {}} x1={0} - x2={720} + x2={704} y1={0} y2={0} /> @@ -3029,7 +3029,7 @@ Array [ @@ -3370,7 +3370,7 @@ Array [ > @@ -5132,7 +5132,7 @@ Array [ rv-hint--verticalAlign-bottom" style={ Object { - "left": 440, + "left": 432, "position": "absolute", "top": 120, } @@ -5339,7 +5339,7 @@ Array [ > @@ -6097,7 +6097,7 @@ Array [ diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 3b6d1684e08e1..1676d3f68b57c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -8,11 +8,12 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { asPercent } from '../../../../../common/utils/formatters'; import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -import { asPercent } from '../../../../utils/formatters'; // @ts-ignore import CustomPlot from '../CustomPlot'; @@ -71,6 +72,7 @@ export function ErroneousTransactionsRateChart() { })} + void; + visibleLegendCount?: number; onToggleLegend?: (disabledSeriesState: boolean[]) => void; } @@ -32,6 +33,7 @@ function TransactionLineChart(props: Props) { truncateLegends, stacked = false, onHover, + visibleLegendCount, onToggleLegend, } = props; @@ -59,6 +61,7 @@ function TransactionLineChart(props: Props) { height={height} truncateLegends={truncateLegends} {...(stacked ? { stackBy: 'y' } : {})} + visibleLegendCount={visibleLegendCount} onToggleLegend={onToggleLegend} /> ); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index d11925dc0303d..6ba080a07b9d3 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -13,7 +13,6 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Location } from 'history'; import React from 'react'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { @@ -29,6 +28,8 @@ import { asDecimal, tpmUnit } from '../../../../utils/formatters'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { BrowserLineChart } from './BrowserLineChart'; import { DurationByCountryMap } from './DurationByCountryMap'; +import { ErroneousTransactionsRateChart } from '../ErroneousTransactionsRateChart'; +import { TransactionBreakdown } from '../../TransactionBreakdown'; import { getResponseTimeTickFormatter, getResponseTimeTooltipFormatter, @@ -39,13 +40,11 @@ import { useFormatter } from './use_formatter'; interface TransactionChartProps { charts: ITransactionChartData; - location: Location; urlParams: IUrlParams; } export function TransactionCharts({ charts, - location, urlParams, }: TransactionChartProps) { const getTPMFormatter = (t: number) => { @@ -72,48 +71,56 @@ export function TransactionCharts({ - - - - - {responseTimeLabel(transactionType)} - - - - {(license) => ( - - )} - - - - + + + + {responseTimeLabel(transactionType)} + + + + {(license) => ( + + )} + + + - - - {tpmLabel(transactionType)} - - - + + {tpmLabel(transactionType)} + + + + + + + + + + + + + + {transactionType === TRANSACTION_PAGE_LOAD && ( <> diff --git a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts index 2449c13f29435..9db78fde2d8c8 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts @@ -13,7 +13,7 @@ export function useTransactionBreakdown() { uiFilters, } = useUrlParams(); - const { data = { kpis: [], timeseries: [] }, error, status } = useFetcher( + const { data = { timeseries: [] }, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ diff --git a/x-pack/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts index 649f11063b149..6249ce53b6779 100644 --- a/x-pack/plugins/apm/public/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters/formatters.ts @@ -23,24 +23,3 @@ export function tpmUnit(type?: string) { defaultMessage: 'tpm', }); } - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - - // 33.2 => 33% - // 3.32 => 3.3% - // 0 => 0% - if (Math.abs(decimal) >= 0.1 || decimal === 0) { - return numeral(decimal).format('0%'); - } - - return numeral(decimal).format('0.0%'); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 731f75226cbe4..e943214b0b517 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -50,40 +50,9 @@ describe('getTransactionBreakdown', () => { setup: getMockSetup(noDataResponse), }); - expect(response.kpis.length).toBe(0); - expect(Object.keys(response.timeseries).length).toBe(0); }); - it('returns transaction breakdowns grouped by type and subtype', async () => { - const response = await getTransactionBreakdown({ - serviceName: 'myServiceName', - transactionType: 'request', - setup: getMockSetup(dataResponse), - }); - - expect(response.kpis.length).toBe(4); - - expect(response.kpis.map((kpi) => kpi.name)).toEqual([ - 'app', - 'dispatcher-servlet', - 'http', - 'postgresql', - ]); - - expect(response.kpis[0]).toEqual({ - name: 'app', - color: '#54b399', - percentage: 0.5408550899466306, - }); - - expect(response.kpis[3]).toEqual({ - name: 'postgresql', - color: '#9170b8', - percentage: 0.047366859295002, - }); - }); - it('returns a timeseries grouped by type and subtype', async () => { const response = await getTransactionBreakdown({ serviceName: 'myServiceName', @@ -98,7 +67,7 @@ describe('getTransactionBreakdown', () => { const appTimeseries = timeseries[0]; expect(appTimeseries.title).toBe('app'); expect(appTimeseries.type).toBe('areaStacked'); - expect(appTimeseries.hideLegend).toBe(true); + expect(appTimeseries.hideLegend).toBe(false); // empty buckets should result in null values for visible types expect(appTimeseries.data.length).toBe(276); diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index fbdddea32deb4..9730ddbbf38d7 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -5,6 +5,7 @@ */ import { flatten, orderBy, last } from 'lodash'; +import { asPercent } from '../../../../common/utils/formatters'; import { ProcessorEvent } from '../../../../common/processor_event'; import { SERVICE_NAME, @@ -149,9 +150,16 @@ export async function getTransactionBreakdown({ ) : []; - const kpis = orderBy(visibleKpis, 'name').map((kpi, index) => { - return { + const kpis = orderBy( + visibleKpis.map((kpi) => ({ ...kpi, + lowerCaseName: kpi.name.toLowerCase(), + })), + 'lowerCaseName' + ).map((kpi, index) => { + const { lowerCaseName, ...rest } = kpi; + return { + ...rest, color: getVizColorForIndex(index), }; }); @@ -213,11 +221,9 @@ export async function getTransactionBreakdown({ color: kpi.color, type: 'areaStacked', data: timeseriesPerSubtype[kpi.name], - hideLegend: true, + hideLegend: false, + legendValue: asPercent(kpi.percentage, 1), })); - return { - kpis, - timeseries, - }; + return { timeseries }; } diff --git a/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts index 4b1f31cb14687..0d2939ed0e8a5 100644 --- a/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/plugins/canvas/__tests__/fixtures/workpads.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CanvasWorkpad, CanvasElement, CanvasPage } from '../../types'; +import { CanvasWorkpad, CanvasElement, CanvasPage, CanvasVariable } from '../../types'; const BaseWorkpad: CanvasWorkpad = { '@created': '2019-02-08T18:35:23.029Z', @@ -50,6 +50,12 @@ const BaseElement: CanvasElement = { filter: '', }; +const BaseVariable: CanvasVariable = { + name: 'my-var', + value: 'Hello World', + type: 'string', +}; + export const workpads: CanvasWorkpad[] = [ { ...BaseWorkpad, @@ -71,6 +77,11 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -82,6 +93,11 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -103,6 +119,11 @@ export const workpads: CanvasWorkpad[] = [ { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, { ...BasePage, elements: [{ ...BaseElement, expression: 'image | render' }] }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -136,6 +157,11 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, @@ -166,6 +192,17 @@ export const workpads: CanvasWorkpad[] = [ ], }, ], + variables: [ + { + ...BaseVariable, + }, + { + ...BaseVariable, + }, + { + ...BaseVariable, + }, + ], }, { ...BaseWorkpad, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts index 16eee349475ef..11551c50d9f25 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts @@ -39,9 +39,9 @@ interface PieOptions { colors: string[]; legend: { show: boolean; - backgroundOpacity: number; - labelBoxBorderColor: string; - position: Legend; + backgroundOpacity?: number; + labelBoxBorderColor?: string; + position?: Legend; }; grid: { show: boolean; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts index 882d1e2ea58b9..33260b5c9303f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedLens } from './saved_lens'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts index 74e41a030de35..b2de0b6526d1e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedMap } from './saved_map'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts index 9bd32202b563a..7d5952754aa90 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedSearch } from './saved_search'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts index 8327c1433b9af..a64fb167dd19f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { savedVisualization } from './saved_visualization'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot new file mode 100644 index 0000000000000..b9bc21dd6e3ea --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/image default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot new file mode 100644 index 0000000000000..9b97ae1fdacb3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/repeat_image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/repeatImage default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot new file mode 100644 index 0000000000000..cf9cc6dd82f7f --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/__snapshots__/table.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/table default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx new file mode 100644 index 0000000000000..bcd8365034448 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/image.stories.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { image } from '../image'; +import { Render } from './render'; +import { elasticLogo } from '../../lib/elastic_logo'; + +storiesOf('renderers/image', module).add('default', () => { + const config = { + type: 'image' as 'image', + mode: 'cover', + dataurl: elasticLogo, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx new file mode 100644 index 0000000000000..647c63c2c1042 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { action } from '@storybook/addon-actions'; +import React, { useRef, useEffect } from 'react'; +import { RendererFactory, RendererHandlers } from '../../../types'; + +export const defaultHandlers: RendererHandlers = { + destroy: () => action('destroy'), + getElementId: () => 'element-id', + getFilter: () => 'filter', + onComplete: (fn) => undefined, + onEmbeddableDestroyed: action('onEmbeddableDestroyed'), + onEmbeddableInputChange: action('onEmbeddableInputChange'), + onResize: action('onResize'), + resize: action('resize'), + setFilter: action('setFilter'), + done: action('done'), + onDestroy: action('onDestroy'), + reload: action('reload'), + update: action('update'), + event: action('event'), +}; + +/* + Uses a RenderDefinitionFactory and Config to render into an element. + + Intended to be used for stories for RenderDefinitionFactory +*/ +interface RenderAdditionalProps { + height?: string; + width?: string; + handlers?: RendererHandlers; +} + +export const Render = ({ + renderer, + config, + ...rest +}: Renderer extends RendererFactory + ? { renderer: Renderer; config: Config } & RenderAdditionalProps + : { renderer: undefined; config: undefined } & RenderAdditionalProps) => { + const { height, width, handlers } = { + height: '200px', + width: '200px', + handlers: defaultHandlers, + ...rest, + }; + + const containerRef = useRef(null); + + useEffect(() => { + if (renderer && containerRef.current !== null) { + renderer().render(containerRef.current, config, handlers); + } + }, [renderer, config, handlers]); + + return ( +
+ {' '} +
+ ); +}; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx new file mode 100644 index 0000000000000..41ccc054a77fb --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/repeat_image.stories.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { repeatImage } from '../repeat_image'; +import { Render } from './render'; +import { elasticLogo } from '../../lib/elastic_logo'; +import { elasticOutline } from '../../lib/elastic_outline'; + +storiesOf('renderers/repeatImage', module).add('default', () => { + const config = { + count: 42, + image: elasticLogo, + size: 20, + max: 60, + emptyImage: elasticOutline, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.tsx new file mode 100644 index 0000000000000..f3c70cb30de45 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/table.stories.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 React from 'react'; +import { storiesOf } from '@storybook/react'; +import { table } from '../table'; +import { Render } from './render'; + +storiesOf('renderers/table', module).add('default', () => { + const config = { + paginate: true, + perPage: 5, + showHeader: true, + datatable: { + type: 'datatable' as 'datatable', + columns: [ + { + name: 'Foo', + type: 'string' as 'string', + id: 'id-foo', + meta: { type: 'string' as 'string' }, + }, + { + name: 'Bar', + type: 'number' as 'number', + id: 'id-bar', + meta: { type: 'string' as 'string' }, + }, + ], + rows: [ + { Foo: 'a', Bar: 700 }, + { Foo: 'b', Bar: 600 }, + { Foo: 'c', Bar: 500 }, + { Foo: 'd', Bar: 400 }, + { Foo: 'e', Bar: 300 }, + { Foo: 'f', Bar: 200 }, + { Foo: 'g', Bar: 100 }, + ], + }, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot new file mode 100644 index 0000000000000..b7039ee1847c7 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/__snapshots__/error.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/error default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx new file mode 100644 index 0000000000000..c71999bc04bb1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/error/__stories__/error.stories.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { error } from '../'; +import { Render } from '../../__stories__/render'; + +storiesOf('renderers/error', module).add('default', () => { + const thrownError = new Error('There was an error'); + const config = { + error: thrownError, + }; + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot new file mode 100644 index 0000000000000..79f447c953d6d --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/__snapshots__/markdown.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/markdown default 1`] = ` +
+ +
+`; + +exports[`Storyshots renderers/markdown links in new tab 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx new file mode 100644 index 0000000000000..d5b190c74a92f --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/markdown/__stories__/markdown.stories.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { markdown } from '../'; +import { Render } from '../../__stories__/render'; + +storiesOf('renderers/markdown', module) + .add('default', () => { + const config = { + content: '# This is Markdown', + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + openLinksInNewTab: false, + }; + return ; + }) + .add('links in new tab', () => { + const config = { + content: '[Elastic.co](https://elastic.co)', + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + openLinksInNewTab: true, + }; + return ; + }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot new file mode 100644 index 0000000000000..3260dbe8c83c2 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/__snapshots__/pie.stories.storyshot @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/pie default 1`] = ` +
+ +
+`; + +exports[`Storyshots renderers/pie with legend 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx new file mode 100644 index 0000000000000..dea2876de0ec8 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/__stories__/pie.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { pie } from '../'; +import { Render } from '../../__stories__/render'; + +const pieOptions = { + canvas: false, + colors: ['#882E72', '#B178A6', '#D6C1DE'], + grid: { show: false }, + legend: { show: false }, + series: { + pie: { + show: true, + innerRadius: 0, + label: { show: true, radius: 1 }, + radius: 'auto' as 'auto', + stroke: { width: 0 }, + tilt: 1, + }, + }, +}; + +const data = [ + { + data: [10], + label: 'A', + }, + { + data: [10], + label: 'B', + }, + { + data: [10], + label: 'C', + }, +]; + +storiesOf('renderers/pie', module) + .add('default', () => { + const config = { + data, + options: pieOptions, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + return ; + }) + .add('with legend', () => { + const options = { + ...pieOptions, + legend: { show: true }, + }; + + const config = { + data, + options, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + + return ; + }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot new file mode 100644 index 0000000000000..7419d13fc7195 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/__snapshots__/plot.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/plot default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx new file mode 100644 index 0000000000000..0e9566d2a5c20 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/__stories__/plot.stories.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { plot } from '../'; +import { Render } from '../../__stories__/render'; + +const plotOptions = { + canvas: false, + colors: ['#882E72', '#B178A6', '#D6C1DE'], + grid: { + margin: { + bottom: 0, + left: 0, + right: 30, + top: 20, + }, + }, + legend: { show: true }, + series: { + bubbles: { + show: true, + fill: false, + }, + }, + xaxis: { + show: true, + mode: 'time', + }, + yaxis: { + show: true, + }, +}; + +const data = [ + { + bubbles: { show: true }, + data: [ + [1546351551031, 33, { size: 5 }], + [1546351551131, 38, { size: 2 }], + ], + label: 'done', + }, + { + bubbles: { show: true }, + data: [ + [1546351551032, 37, { size: 4 }], + [1546351551139, 45, { size: 3 }], + ], + label: 'running', + }, +]; + +storiesOf('renderers/plot', module).add('default', () => { + const config = { + data, + options: plotOptions, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + }; + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot new file mode 100644 index 0000000000000..1fe884656ef3b --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/__snapshots__/progress.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/progress default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx new file mode 100644 index 0000000000000..3e20cfd4772a8 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/progress/__stories__/progress.stories.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { progress } from '../'; +import { Render } from '../../__stories__/render'; +import { Shape } from '../../../functions/common/progress'; + +storiesOf('renderers/progress', module).add('default', () => { + const config = { + barColor: '#bc1234', + barWeight: 20, + font: { + css: '', + spec: {}, + type: 'style' as 'style', + }, + label: '66%', + max: 1, + shape: Shape.UNICORN, + value: 0.66, + valueColor: '#000', + valueWeight: 15, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot new file mode 100644 index 0000000000000..b9963565a09f5 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/__snapshots__/reveal_image.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/revealImage default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx new file mode 100644 index 0000000000000..637f9a2bb6986 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/reveal_image/__stories__/reveal_image.stories.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { revealImage } from '../'; +import { Render } from '../../__stories__/render'; +import { elasticOutline } from '../../../lib/elastic_outline'; +import { elasticLogo } from '../../../lib/elastic_logo'; +import { Origin } from '../../../functions/common/revealImage'; + +storiesOf('renderers/revealImage', module).add('default', () => { + const config = { + image: elasticLogo, + emptyImage: elasticOutline, + origin: Origin.LEFT, + percent: 0.45, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot new file mode 100644 index 0000000000000..317c20021015a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/__snapshots__/shape.stories.storyshot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots renderers/shape default 1`] = ` +
+ +
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx new file mode 100644 index 0000000000000..19df14c08688f --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/shape/__stories__/shape.stories.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { shape } from '../'; +import { Render } from '../../__stories__/render'; +import { Shape } from '../../../functions/common/shape'; + +storiesOf('renderers/shape', module).add('default', () => { + const config = { + type: 'shape' as 'shape', + border: '#FFEEDD', + borderWidth: 8, + shape: Shape.BOOKMARK, + fill: '#112233', + maintainAspect: true, + }; + + return ; +}); diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts index f7a773f6ca36a..777810cad05ba 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.test.ts +++ b/x-pack/plugins/canvas/common/lib/autocomplete.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); import { functionSpecs } from '../../__tests__/fixtures/function_specs'; import { diff --git a/x-pack/plugins/canvas/public/__tests__/setup.js b/x-pack/plugins/canvas/public/__tests__/setup.js deleted file mode 100644 index 1792397d0614e..0000000000000 --- a/x-pack/plugins/canvas/public/__tests__/setup.js +++ /dev/null @@ -1,15 +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 enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -// this will run before any code that's inside a describe block -// so we can use it to set up whatever we need for our browser tests -before(() => { - // configure enzume - enzyme.configure({ adapter: new Adapter() }); -}); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx index e8b772e0f2fbd..b037d30ada12f 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx @@ -14,7 +14,6 @@ import { import { METRIC_TYPE } from '../../../lib/ui_metric'; import { ResolvedArgType } from '../../../../types'; -jest.mock('ui/new_platform'); const trackMetric = jest.fn(); const Component = withUnconnectedElementsLoadedTelemetry(() =>
, trackMetric); diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts index bf69a862d5c30..e451a39df06db 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); // actions/elements has some dependencies on ui/new_platform. jest.mock('../../../lib/app_state'); import { workpadRefresh } from '../workpad_refresh'; diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts b/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts index 5b1192630897a..08b59e5fedc44 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddables.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); + import { State } from '../../../types'; import { updateEmbeddableExpression } from '../actions/embeddable'; import { embeddableReducer } from './embeddable'; diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts index 9f71edcc05bf2..32665cc42dc4e 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.test.ts @@ -49,6 +49,14 @@ describe('usage collector handle es response data', () => { 'shape', ], }, + variables: { + total: 7, + per_workpad: { + avg: 1.1666666666666667, + min: 0, + max: 3, + }, + }, }); }); @@ -63,6 +71,7 @@ describe('usage collector handle es response data', () => { pages: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, elements: { total: 1, per_page: { avg: 1, min: 1, max: 1 } }, functions: { total: 1, in_use: ['toast'], per_element: { avg: 1, min: 1, max: 1 } }, + variables: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, }); }); @@ -76,6 +85,39 @@ describe('usage collector handle es response data', () => { pages: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } }, elements: undefined, functions: undefined, + variables: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, // Variables still possible even with no pages + }); + }); + + it('should handle cases where the version workpad might not have variables', () => { + const workpad = cloneDeep(workpads[0]); + // @ts-ignore + workpad.variables = undefined; + + const mockWorkpadsOld = [workpad]; + const usage = summarizeWorkpads(mockWorkpadsOld); + expect(usage).toEqual({ + workpads: { total: 1 }, + pages: { total: 1, per_workpad: { avg: 1, min: 1, max: 1 } }, + elements: { total: 1, per_page: { avg: 1, min: 1, max: 1 } }, + functions: { + total: 7, + in_use: [ + 'demodata', + 'ply', + 'rowCount', + 'as', + 'staticColumn', + 'math', + 'mapColumn', + 'sort', + 'pointseries', + 'plot', + 'seriesStyle', + ], + per_element: { avg: 7, min: 7, max: 7 }, + }, + variables: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } }, // Variables still possible even with no pages }); }); diff --git a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts index 4b00d061c17ce..9fa39c580962d 100644 --- a/x-pack/plugins/canvas/server/collectors/workpad_collector.ts +++ b/x-pack/plugins/canvas/server/collectors/workpad_collector.ts @@ -44,6 +44,14 @@ interface WorkpadTelemetry { max: number; }; }; + variables?: { + total: number; + per_workpad: { + avg: number; + min: number; + max: number; + }; + }; } /** @@ -81,7 +89,10 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr }); }, []); - return { pages, elementCounts, functionCounts }; + const variableCount = + workpad.variables && workpad.variables.length ? workpad.variables.length : 0; + + return { pages, elementCounts, functionCounts, variableCount }; }); // combine together info from across the workpads @@ -91,9 +102,10 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr pageCounts: number[]; elementCounts: number[]; functionCounts: number[]; + variableCounts: number[]; }>( (accum, pageInfo) => { - const { pages, elementCounts, functionCounts } = pageInfo; + const { pages, elementCounts, functionCounts, variableCount } = pageInfo; return { pageMin: pages.count < accum.pageMin ? pages.count : accum.pageMin, @@ -101,6 +113,7 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr pageCounts: accum.pageCounts.concat(pages.count), elementCounts: accum.elementCounts.concat(elementCounts), functionCounts: accum.functionCounts.concat(functionCounts), + variableCounts: accum.variableCounts.concat([variableCount]), }; }, { @@ -109,13 +122,23 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr pageCounts: [], elementCounts: [], functionCounts: [], + variableCounts: [], } ); - const { pageCounts, pageMin, pageMax, elementCounts, functionCounts } = combinedWorkpadsInfo; + const { + pageCounts, + pageMin, + pageMax, + elementCounts, + functionCounts, + variableCounts, + } = combinedWorkpadsInfo; const pageTotal = arraySum(pageCounts); const elementsTotal = arraySum(elementCounts); const functionsTotal = arraySum(functionCounts); + const variableTotal = arraySum(variableCounts); + const pagesInfo = workpadsInfo.length > 0 ? { @@ -151,11 +174,21 @@ export function summarizeWorkpads(workpadDocs: CanvasWorkpad[]): WorkpadTelemetr } : undefined; + const variableInfo = { + total: variableTotal, + per_workpad: { + avg: variableTotal / variableCounts.length, + min: arrayMin(variableCounts) || 0, + max: arrayMax(variableCounts) || 0, + }, + }; + return { workpads: { total: workpadsInfo.length }, pages: pagesInfo, elements: elementsInfo, functions: functionsInfo, + variables: variableInfo, }; } diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js index eda275ba50c1a..cdb107016e465 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js @@ -11,8 +11,6 @@ jest.mock('../services/auto_follow_pattern_validators', () => ({ validateLeaderIndexPattern: jest.fn(), })); -jest.mock('ui/new_platform'); - describe(' { describe('updateFormErrors()', () => { it('should merge errors with existing fieldsErrors', () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js index 93da20a8ed93c..72c4832a631a7 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js @@ -6,8 +6,6 @@ import { updateFields, updateFormErrors } from './follower_index_form'; -jest.mock('ui/new_platform'); - describe(' state transitions', () => { it('updateFormErrors() should merge errors with existing fieldsErrors', () => { const errors = { name: 'Some error' }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js index 11ec125c17d59..924bbe708c73a 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js @@ -6,8 +6,6 @@ import { validateAutoFollowPattern } from './auto_follow_pattern_validators'; -jest.mock('ui/new_platform'); - describe('Auto-follow pattern validators', () => { describe('validateAutoFollowPattern()', () => { it('returns empty object when autoFollowPattern is undefined', () => { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js index 01dccf70a21d6..a3655df1f8956 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js @@ -8,7 +8,6 @@ import { reducer, initialState } from './api'; import { API_STATUS } from '../../constants'; import { apiRequestStart, apiRequestEnd, setApiError } from '../actions'; -jest.mock('ui/new_platform'); jest.mock('../../constants', () => ({ API_STATUS: { IDLE: 'idle', diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 3d85c2e9eb751..00d578f5ca866 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -4,23 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ import { coreMock, savedObjectsServiceMock } from 'src/core/server/mocks'; - import { Plugin } from './plugin'; -const initContext = coreMock.createPluginInitializerContext(); -const coreSetup = coreMock.createSetup(); -const coreStart = coreMock.createStart(); -const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); -typeRegistry.getVisibleTypes.mockReturnValue([ - { - name: 'foo', - hidden: false, - mappings: { properties: {} }, - namespaceType: 'single' as 'single', - }, -]); -coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); describe('Features Plugin', () => { + let initContext: ReturnType; + let coreSetup: ReturnType; + let coreStart: ReturnType; + let typeRegistry: ReturnType; + + beforeEach(() => { + initContext = coreMock.createPluginInitializerContext(); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); + typeRegistry.getVisibleTypes.mockReturnValue([ + { + name: 'foo', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'single' as 'single', + }, + ]); + coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); + }); + it('returns OSS + registered features', async () => { const plugin = new Plugin(initContext); const { registerFeature } = await plugin.setup(coreSetup, {}); @@ -88,4 +95,12 @@ describe('Features Plugin', () => { expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('bar')).toBe(false); }); + + it('registers a capabilities provider', async () => { + const plugin = new Plugin(initContext); + await plugin.setup(coreSetup, {}); + + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledTimes(1); + expect(coreSetup.capabilities.registerProvider).toHaveBeenCalledWith(expect.any(Function)); + }); }); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 5783b20eae648..61b66d95ca44f 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -61,10 +61,15 @@ export class Plugin { featureRegistry: this.featureRegistry, }); + const getFeaturesUICapabilities = () => + uiCapabilitiesForFeatures(this.featureRegistry.getAll()); + + core.capabilities.registerProvider(getFeaturesUICapabilities); + return deepFreeze({ registerFeature: this.featureRegistry.register.bind(this.featureRegistry), getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), - getFeaturesUICapabilities: () => uiCapabilitiesForFeatures(this.featureRegistry.getAll()), + getFeaturesUICapabilities, }); } diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 100122af943e1..1b783df1b7d02 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -21,7 +21,6 @@ import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; -jest.mock('ui/new_platform'); jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise((r) => setTimeout(r)); diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index d32bc9a175a47..f28f51544a6ed 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -17,8 +17,6 @@ import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } fro import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; import { IndexPattern } from '../../../../../src/plugins/data/public'; -jest.mock('ui/new_platform'); - export interface MockedGraphEnvironment { store: GraphStore; mockedDeps: jest.Mocked; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts index 7c7c0b70c0eed..81eb1c8cad135 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.test.ts @@ -14,7 +14,6 @@ import { } from '../constants/'; import { getUiMetricsForPhases } from './ui_metric'; -jest.mock('ui/new_platform'); describe('getUiMetricsForPhases', () => { test('gets cold phase', () => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts index a112d73230b82..2a0585d61d6f6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_details.test.ts @@ -12,11 +12,6 @@ import { ComponentTemplateDeserialized } from '../../shared_imports'; const { setup } = pageHelpers.componentTemplateDetails; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - const COMPONENT_TEMPLATE: ComponentTemplateDeserialized = { name: 'comp-1', template: { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts index bd6ac27375836..9bf7df263ec26 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts @@ -14,11 +14,6 @@ import { API_BASE_PATH } from './helpers/constants'; const { setup } = pageHelpers.componentTemplateList; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: ComponentTemplateListTestBed; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx index 6b5a848ce85d3..95575124b6abd 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/edit_field/edit_field.tsx @@ -163,7 +163,7 @@ export const EditField = React.memo(({ form, field, allFields, exitEdit, updateF - {form.isSubmitted && form.isValid === false && ( + {form.isSubmitted && !form.isValid && ( <> {i18n.translate('xpack.idxMgmt.mappingsEditor.editFieldUpdateButtonLabel', { diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx index fcc9795617ebb..56f040fc59a7b 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx @@ -17,13 +17,13 @@ import { i18n } from '@kbn/i18n'; import { useForm, + useFormData, Form, getUseField, getFormRow, Field, Forms, JsonEditorField, - FormDataProvider, } from '../../../../shared_imports'; import { documentationService } from '../../../services/documentation'; import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas'; @@ -118,9 +118,7 @@ interface LogisticsForm { } interface LogisticsFormInternal extends LogisticsForm { - __internal__: { - addMeta: boolean; - }; + addMeta: boolean; } interface Props { @@ -133,14 +131,12 @@ interface Props { function formDeserializer(formData: LogisticsForm): LogisticsFormInternal { return { ...formData, - __internal__: { - addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), - }, + addMeta: Boolean(formData._meta && Object.keys(formData._meta).length), }; } function formSerializer(formData: LogisticsFormInternal): LogisticsForm { - const { __internal__, ...rest } = formData; + const { addMeta, ...rest } = formData; return rest; } @@ -153,7 +149,18 @@ export const StepLogistics: React.FunctionComponent = React.memo( serializer: formSerializer, deserializer: formDeserializer, }); - const { subscribe, submit, isSubmitted, isValid: isFormValid, getErrors: getFormErrors } = form; + const { + submit, + isSubmitted, + isValid: isFormValid, + getErrors: getFormErrors, + getFormData, + } = form; + + const [{ addMeta }] = useFormData({ + form, + watch: 'addMeta', + }); /** * When the consumer call validate() on this step, we submit the form so it enters the "isSubmitted" state @@ -164,15 +171,12 @@ export const StepLogistics: React.FunctionComponent = React.memo( }, [submit]); useEffect(() => { - const subscription = subscribe(({ data, isValid }) => { - onChange({ - isValid, - validate, - getData: data.format, - }); + onChange({ + isValid: isFormValid, + getData: getFormData, + validate, }); - return subscription.unsubscribe; - }, [onChange, validate, subscribe]); + }, [onChange, isFormValid, validate, getFormData]); const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta( documentationService.getEsDocsBase() @@ -296,34 +300,28 @@ export const StepLogistics: React.FunctionComponent = React.memo( defaultMessage="Use the _meta field to store any metadata you want." /> - + } > - - {({ '__internal__.addMeta': addMeta }) => { - return ( - addMeta && ( - - ) - ); - }} - + {addMeta && ( + + )} )} diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 537f421173358..3a03835e85970 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -192,8 +192,8 @@ export const TemplateForm = ({ wizardData: WizardContent ): TemplateDeserialized => { const outputTemplate = { - ...initialTemplate, ...wizardData.logistics, + _kbnMeta: initialTemplate._kbnMeta, composedOf: wizardData.components, template: { settings: wizardData.settings, diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx index 0d9ce57a64c84..c85126f08685e 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx @@ -125,6 +125,7 @@ export const schemas: Record = { { validator: indexPatternField(i18n), type: VALIDATION_TYPES.ARRAY_ITEM, + isBlocking: false, }, ], }, @@ -213,13 +214,11 @@ export const schemas: Record = { } }, }, - __internal__: { - addMeta: { - type: FIELD_TYPES.TOGGLE, - label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { - defaultMessage: 'Add metadata', - }), - }, + addMeta: { + type: FIELD_TYPES.TOGGLE, + label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', { + defaultMessage: 'Add metadata', + }), }, }, }; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index 16dcab18c3caf..f7f992a090501 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -13,7 +13,7 @@ export { Forms, extractQueryParams, GlobalFlyout, -} from '../../../../src/plugins/es_ui_shared/public/'; +} from '../../../../src/plugins/es_ui_shared/public'; export { FormSchema, @@ -21,6 +21,7 @@ export { VALIDATION_TYPES, FieldConfig, useForm, + useFormData, Form, getUseField, UseField, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts index d2a14fcf04dff..af11bc7f6c831 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/cache.ts @@ -8,4 +8,6 @@ const cache: Map = new Map(); export const cacheGet = (key: string) => cache.get(key); export const cacheSet = (key: string, value: Buffer) => cache.set(key, value); export const cacheHas = (key: string) => cache.has(key); +export const cacheClear = () => cache.clear(); +export const cacheDelete = (key: string) => cache.delete(key); export const getCacheKey = (key: string) => key + '.tar.gz'; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts index 8d6a83a625651..c0acc39ca35a1 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -13,11 +13,6 @@ import { PipelineListTestBed } from './helpers/pipelines_list.helpers'; const { setup } = pageHelpers.pipelinesList; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: PipelineListTestBed; diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts index 39f7aa6503b35..943516dc0409a 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts @@ -19,12 +19,22 @@ describe('FeatureUsageService', () => { describe('#setup', () => { describe('#register', () => { - it('throws when registering the same feature twice', () => { + it('does not throw when registering the same feature twice with the same license', () => { const setup = service.setup(); setup.register('foo', 'basic'); expect(() => { setup.register('foo', 'basic'); - }).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); + }).not.toThrow(); + }); + + it('throws when registering the same feature again with a different license', () => { + const setup = service.setup(); + setup.register('foo', 'basic'); + expect(() => { + setup.register('foo', 'enterprise'); + }).toThrowErrorMatchingInlineSnapshot( + `"Feature 'foo' has already been registered with another license type. (current: basic, new: enterprise)"` + ); }); }); }); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.ts index 9bfcb28f36b2a..75a0c7fa78108 100644 --- a/x-pack/plugins/licensing/server/services/feature_usage_service.ts +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.ts @@ -43,14 +43,20 @@ export class FeatureUsageService { public setup(): FeatureUsageServiceSetup { return { register: (featureName, licenseType) => { - if (this.lastUsages.has(featureName)) { - throw new Error(`Feature '${featureName}' has already been registered.`); + const registered = this.lastUsages.get(featureName); + if (registered) { + if (registered.licenseType !== licenseType) { + throw new Error( + `Feature '${featureName}' has already been registered with another license type. (current: ${registered.licenseType}, new: ${licenseType})` + ); + } + } else { + this.lastUsages.set(featureName, { + name: featureName, + lastUsed: null, + licenseType, + }); } - this.lastUsages.set(featureName, { - name: featureName, - lastUsed: null, - licenseType, - }); }, }; } diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json new file mode 100644 index 0000000000000..9f0c306a408f0 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/trusted_app_list_item_agnostic.json @@ -0,0 +1,18 @@ +{ + "list_id": "endpoint_trusted_apps", + "item_id": "endpoint_trusted_apps_item", + "_tags": ["endpoint", "os:linux", "os:windows", "os:macos", "trusted-app"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample agnostic endpoint trusted app entry", + "name": "Sample Endpoint Trusted App Entry", + "namespace_type": "agnostic", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "type": "match", + "value": "Elastic, N.V." + } + ] +} diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index f408896853155..b00594cb7fb23 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -271,7 +271,7 @@ export function triggerRefreshTimer() { }; } -export function updateDrawState(drawState: DrawState) { +export function updateDrawState(drawState: DrawState | null) { return (dispatch: Dispatch) => { if (drawState !== null) { dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx index 52524d0c9a5fa..c5d6ced76b5c0 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx @@ -6,7 +6,6 @@ import { EMSFileSource } from './ems_file_source'; -jest.mock('ui/new_platform'); jest.mock('../../layers/vector_layer/vector_layer', () => {}); function makeEMSFileSource(tooltipProperties: string[]) { diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts index 87abbedfdf50e..cf0170ab7f1bd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts @@ -11,8 +11,6 @@ import _ from 'lodash'; import { AGG_TYPE } from '../../../../common/constants'; import { AggDescriptor } from '../../../../common/descriptor_types'; -jest.mock('ui/new_platform'); - const sumFieldName = 'myFieldGettingSummed'; const metricExamples = [ { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 37193e148bdc7..43bfb74bf54b6 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -6,7 +6,6 @@ import { MapExtent, MapFilters } from '../../../../common/descriptor_types'; jest.mock('../../../kibana_services'); -jest.mock('ui/new_platform'); import { getIndexPatternService, getSearchService } from '../../../kibana_services'; import { ESGeoGridSource } from './es_geo_grid_source'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index f6779206868a5..060096157f578 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -6,7 +6,6 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; -jest.mock('ui/new_platform'); jest.mock('../../layers/vector_layer/vector_layer', () => {}); const indexPatternTitle = 'myIndex'; diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx index aa4dbc67e8e4d..f082e67512099 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx @@ -9,8 +9,6 @@ import { shallow } from 'enzyme'; import { HeatmapStyleEditor } from './heatmap_style_editor'; -jest.mock('ui/new_platform'); - describe('HeatmapStyleEditor', () => { test('is rendered', () => { const component = shallow( diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index 62a6a59dd091b..c3610cbc78e15 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
mockVectorStyleEditor
; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index af93c8e0c9d6d..06987ab8bcc48 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -6,7 +6,6 @@ import { shallow } from 'enzyme'; -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
mockVectorStyleEditor
; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index db44ae0da562d..c5298067f6cbe 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); jest.mock('../components/vector_style_editor', () => ({ VectorStyleEditor: () => { return
mockVectorStyleEditor
; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index a85cd0cc86407..28801a402ca14 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -14,7 +14,6 @@ import { } from '../../../../common/constants'; jest.mock('../../../kibana_services'); -jest.mock('ui/new_platform'); class MockField { constructor({ fieldName }) { diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap similarity index 99% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap index d7fa099fe9dbe..afeee04358e15 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.tsx.snap @@ -20,7 +20,7 @@ exports[`Should render cancel button when drawing 1`] = ` closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="contextMenu" + id="toolsControlPopover" isOpen={false} ownFocus={false} panelPaddingSize="none" @@ -146,7 +146,7 @@ exports[`renders 1`] = ` closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="contextMenu" + id="toolsControlPopover" isOpen={false} ownFocus={false} panelPaddingSize="none" diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts similarity index 63% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts index 5812604a91b87..70101f1ce6eba 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.ts @@ -4,24 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AnyAction, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { ToolsControl } from './tools_control'; import { isDrawingFilter } from '../../../selectors/map_selectors'; import { updateDrawState } from '../../../actions'; +import { MapStoreState } from '../../../reducers/store'; +import { DrawState } from '../../../../common/descriptor_types'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { return { isDrawingFilter: isDrawingFilter(state), }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: Dispatch) { return { - initiateDraw: (options) => { - dispatch(updateDrawState(options)); + initiateDraw: (drawState: DrawState) => { + dispatch(updateDrawState(drawState)); }, cancelDraw: () => { - dispatch(updateDrawState(null)); + dispatch(updateDrawState(null)); }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.tsx similarity index 97% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.tsx index f0e6dd43e68a1..544d789468e89 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.tsx @@ -19,6 +19,7 @@ const defaultProps = { indexPatternId: '1', }, ], + isDrawingFilter: false, }; test('renders', async () => { diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx similarity index 81% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx index 017f0369e0b73..fa0864ce680a2 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.tsx @@ -14,10 +14,14 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../../../common/constants'; +// @ts-expect-error import { GeometryFilterForm } from '../../../components/geometry_filter_form'; import { DistanceFilterForm } from '../../../components/distance_filter_form'; +import { GeoFieldWithIndex } from '../../../components/geo_field_with_index'; +import { DrawState } from '../../../../common/descriptor_types'; const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { defaultMessage: 'Draw shape to filter data', @@ -46,8 +50,21 @@ const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( } ); -export class ToolsControl extends Component { - state = { +interface Props { + cancelDraw: () => void; + geoFields: GeoFieldWithIndex[]; + initiateDraw: (drawState: DrawState) => void; + isDrawingFilter: boolean; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; +} + +interface State { + isPopoverOpen: boolean; +} + +export class ToolsControl extends Component { + state: State = { isPopoverOpen: false, }; @@ -61,7 +78,14 @@ export class ToolsControl extends Component { this.setState({ isPopoverOpen: false }); }; - _initiateShapeDraw = (options) => { + _initiateShapeDraw = (options: { + actionId: string; + geometryLabel: string; + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + relation: ES_SPATIAL_RELATIONS; + }) => { this.props.initiateDraw({ drawType: DRAW_TYPE.POLYGON, ...options, @@ -69,7 +93,14 @@ export class ToolsControl extends Component { this._closePopover(); }; - _initiateBoundsDraw = (options) => { + _initiateBoundsDraw = (options: { + actionId: string; + geometryLabel: string; + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + relation: ES_SPATIAL_RELATIONS; + }) => { this.props.initiateDraw({ drawType: DRAW_TYPE.BOUNDS, ...options, @@ -77,7 +108,12 @@ export class ToolsControl extends Component { this._closePopover(); }; - _initiateDistanceDraw = (options) => { + _initiateDistanceDraw = (options: { + actionId: string; + filterLabel: string; + indexPatternId: string; + geoFieldName: string; + }) => { this.props.initiateDraw({ drawType: DRAW_TYPE.DISTANCE, ...options, @@ -194,7 +230,7 @@ export class ToolsControl extends Component { render() { const toolsPopoverButton = ( { render() { return ( } - > -

- -

- + /> `; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js index 9fcd457df008f..5140fe77ff979 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -47,7 +47,7 @@ export class DeleteRuleModal extends Component { title={ } onCancel={this.closeModal} @@ -66,14 +66,7 @@ export class DeleteRuleModal extends Component { /> } defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- -

- + /> ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js index 1e3ec6241311b..f80578cb18341 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.js @@ -98,7 +98,7 @@ export class DeleteJobModal extends Component { const title = ( -

- -

; } -export interface HostsRequestOptions extends RequestOptionsPaginated { - sort: SortField; +export interface HostsRequestOptions extends RequestOptionsPaginated { defaultIndex: string[]; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index fb4fd762a86bf..d15da4bf07ae7 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -14,6 +14,11 @@ export enum HostPolicyResponseActionStatus { warning = 'warning', } +export enum HostsFields { + lastSeen = 'lastSeen', + hostName = 'hostName', +} + export interface EndpointFields { endpointPolicy?: Maybe; sensorVersion?: Maybe; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts index 93a5c0582fb96..cbabe9dd11115 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts @@ -6,8 +6,10 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { Inspect, Maybe, RequestOptionsPaginated } from '../..'; +import { HostsFields } from '../common'; -export interface HostFirstLastSeenRequestOptions extends Partial { +export interface HostFirstLastSeenRequestOptions + extends Partial> { hostName: string; } export interface HostFirstLastSeenStrategyResponse extends IEsSearchResponse { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts index bc797e7a03fb8..8d54481f56dbd 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts @@ -6,7 +6,7 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; -import { HostItem } from '../common'; +import { HostItem, HostsFields } from '../common'; import { Inspect, Maybe, RequestOptionsPaginated, TimerangeInput } from '../..'; export interface HostOverviewStrategyResponse extends IEsSearchResponse { @@ -14,7 +14,7 @@ export interface HostOverviewStrategyResponse extends IEsSearchResponse { inspect?: Maybe; } -export interface HostOverviewRequestOptions extends Partial { +export interface HostOverviewRequestOptions extends Partial> { hostName: string; skip?: boolean; timerange: TimerangeInput; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 073b96e400f5c..175784bc5ade5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -15,10 +15,13 @@ import { HostsRequestOptions, HostsStrategyResponse, } from './hosts'; +import { NetworkQueries, NetworkTlsStrategyResponse, NetworkTlsRequestOptions } from './network'; + export * from './hosts'; +export * from './network'; export type Maybe = T | null; -export type FactoryQueryTypes = HostsQueries; +export type FactoryQueryTypes = HostsQueries | NetworkQueries; export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; @@ -48,8 +51,8 @@ export enum Direction { desc = 'desc', } -export interface SortField { - field: 'lastSeen' | 'hostName'; +export interface SortField { + field: Field; direction: Direction; } @@ -95,14 +98,14 @@ export interface RequestBasicOptions extends IEsSearchRequest { factoryQueryType?: FactoryQueryTypes; } -export interface RequestOptions extends RequestBasicOptions { +export interface RequestOptions extends RequestBasicOptions { pagination: PaginationInput; - sortField?: SortField; + sort: SortField; } -export interface RequestOptionsPaginated extends RequestBasicOptions { +export interface RequestOptionsPaginated extends RequestBasicOptions { pagination: PaginationInputPaginated; - sortField?: SortField; + sort: SortField; } export type StrategyResponseType = T extends HostsQueries.hosts @@ -111,6 +114,8 @@ export type StrategyResponseType = T extends HostsQ ? HostOverviewStrategyResponse : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenStrategyResponse + : T extends NetworkQueries.tls + ? NetworkTlsStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -119,4 +124,6 @@ export type StrategyRequestType = T extends HostsQu ? HostOverviewRequestOptions : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenRequestOptions + : T extends NetworkQueries.tls + ? NetworkTlsRequestOptions : never; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts new file mode 100644 index 0000000000000..680a3697ef0bd --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './tls'; + +export enum NetworkQueries { + tls = 'tls', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts new file mode 100644 index 0000000000000..c9e593bb7a7d2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts @@ -0,0 +1,64 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { CursorType, Inspect, Maybe, PageInfoPaginated, RequestOptionsPaginated } from '../..'; + +export interface TlsBuckets { + key: string; + timestamp?: { + value: number; + value_as_string: string; + }; + subjects: { + buckets: Readonly>; + }; + ja3: { + buckets: Readonly>; + }; + issuers: { + buckets: Readonly>; + }; + not_after: { + buckets: Readonly>; + }; +} + +export interface TlsNode { + _id?: Maybe; + timestamp?: Maybe; + notAfter?: Maybe; + subjects?: Maybe; + ja3?: Maybe; + issuers?: Maybe; +} + +export enum FlowTargetSourceDest { + destination = 'destination', + source = 'source', +} + +export enum TlsFields { + _id = '_id', +} + +export interface TlsEdges { + node: TlsNode; + cursor: CursorType; +} + +export interface NetworkTlsRequestOptions extends RequestOptionsPaginated { + ip: string; + flowTarget: FlowTargetSourceDest; + defaultIndex: string[]; +} + +export interface NetworkTlsStrategyResponse extends IEsSearchResponse { + edges: TlsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts new file mode 100644 index 0000000000000..d8f96aaf5e563 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timelines_export.spec.ts @@ -0,0 +1,41 @@ +/* + * 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 { exportTimeline, waitForTimelinesPanelToBeLoaded } from '../tasks/timeline'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; + +import { TIMELINES_URL } from '../urls/navigation'; + +const EXPECTED_EXPORTED_TIMELINE_PATH = 'cypress/test_files/expected_timelines_export.ndjson'; + +describe('Export timelines', () => { + before(() => { + esArchiverLoad('timeline'); + cy.server(); + cy.route('POST', '**api/timeline/_export?file_name=timelines_export.ndjson*').as('export'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Exports a custom timeline', () => { + loginAndWaitForPageWithoutDateRange(TIMELINES_URL); + waitForTimelinesPanelToBeLoaded(); + + cy.readFile(EXPECTED_EXPORTED_TIMELINE_PATH).then(($expectedExportedJson) => { + const parsedJson = JSON.parse($expectedExportedJson); + const timelineId = parsedJson.savedObjectId; + exportTimeline(timelineId); + + cy.wait('@export').then((response) => { + cy.wrap(response.status).should('eql', 200); + cy.wrap(response.xhr.responseText).should('eql', $expectedExportedJson); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 26203a8ca3b83..fd41cd63fc090 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; @@ -11,6 +13,8 @@ export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; export const DRAGGABLE_HEADER = '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"] [data-test-subj="draggable-header"]'; +export const EXPORT_TIMELINE_ACTION = '[data-test-subj="export-timeline-action"]'; + export const HEADER = '[data-test-subj="header"]'; export const HEADERS_GROUP = '[data-test-subj="headers-group"]'; @@ -41,6 +45,10 @@ export const TIMELINE = (id: string) => { export const TIMELINE_CHANGES_IN_PROGRESS = '[data-test-subj="timeline"] .euiProgress'; +export const TIMELINE_CHECKBOX = (id: string) => { + return `[data-test-subj="checkboxSelectRow-${id}"]`; +}; + export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; @@ -70,6 +78,8 @@ export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; +export const TIMELINES_TABLE = '[data-test-subj="timelines-table"]'; + export const TIMESTAMP_HEADER_FIELD = '[data-test-subj="header-text-@timestamp"]'; export const TIMESTAMP_TOGGLE_FIELD = '[data-test-subj="toggle-field-@timestamp"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 08624df06e096..6fb8bb5e29ae5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -5,8 +5,11 @@ */ import { + BULK_ACTIONS, CLOSE_TIMELINE_BTN, CREATE_NEW_TIMELINE, + EXPORT_TIMELINE_ACTION, + TIMELINE_CHECKBOX, HEADER, ID_FIELD, ID_HEADER_FIELD, @@ -20,6 +23,7 @@ import { TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, + TIMELINES_TABLE, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, REMOVE_COLUMN, @@ -66,6 +70,12 @@ export const expandFirstTimelineEventDetails = () => { cy.get(TOGGLE_TIMELINE_EXPAND_EVENT).first().click({ force: true }); }; +export const exportTimeline = (timelineId: string) => { + cy.get(TIMELINE_CHECKBOX(timelineId)).click({ force: true }); + cy.get(BULK_ACTIONS).click({ force: true }); + cy.get(EXPORT_TIMELINE_ACTION).click(); +}; + export const openTimelineFieldsBrowser = () => { cy.get(TIMELINE_FIELDS_BUTTON).click({ force: true }); }; @@ -122,6 +132,10 @@ export const resetFields = () => { cy.get(RESET_FIELDS).click({ force: true }); }; +export const waitForTimelinesPanelToBeLoaded = () => { + cy.get(TIMELINES_TABLE).should('exist'); +}; + export const waitForTimelineChanges = () => { cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('exist'); cy.get(TIMELINE_CHANGES_IN_PROGRESS).should('not.exist'); diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson new file mode 100644 index 0000000000000..9cca356a8b052 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_timelines_export.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"0162c130-78be-11ea-9718-118a926974a4","version":"WzcsMV0=","columns":[{"columnHeaderType":"not-filtered","id":"@timestamp"},{"columnHeaderType":"not-filtered","id":"message"},{"columnHeaderType":"not-filtered","id":"event.category"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"source.ip"},{"columnHeaderType":"not-filtered","id":"destination.ip"},{"columnHeaderType":"not-filtered","id":"user.name"}],"created":1586256805054,"createdBy":"elastic","dataProviders":[],"dateRange":{"end":1586256837669,"start":1546343624710},"description":"description","eventType":"all","filters":[],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name:*","kind":"kuery"},"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}"}},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"title":"SIEM test","updated":1586256839298,"updatedBy":"elastic","timelineType":"default","eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} diff --git a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx index 75d396fe384f8..19f1d70e6e230 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_icon/index.tsx @@ -6,7 +6,7 @@ import { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui'; import { LinkAnchorProps } from '@elastic/eui/src/components/link/link'; -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; interface LinkProps { @@ -47,7 +47,7 @@ export const Link = styled(({ iconSide, children, ...rest }) => ( Link.displayName = 'Link'; export interface LinkIconProps extends LinkProps { - children: string; + children: string | ReactNode; iconSize?: IconSize; iconType: IconType; dataTestSubj?: string; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 17506f9a01cb9..77c6446fbb3d0 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -4,38 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - PageInfoPaginated, - TlsEdges, - TlsSortField, - GetTlsQuery, - FlowTargetSourceDest, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { TlsEdges, PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; import { networkModel, networkSelectors } from '../../store'; -import { tlsQuery } from './index.gql_query'; +import { + NetworkQueries, + NetworkTlsRequestOptions, + NetworkTlsStrategyResponse, +} from '../../../../common/search_strategy/security_solution/network'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; -const ID = 'tlsQuery'; +const ID = 'networkTlsQuery'; -export interface TlsArgs { +export interface NetworkTlsArgs { id: string; inspect: inputsModel.InspectQuery; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; @@ -43,121 +38,159 @@ export interface TlsArgs { totalCount: number; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: TlsArgs) => React.ReactNode; +interface UseNetworkTls { flowTarget: FlowTargetSourceDest; ip: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; + id?: string; } -export interface TlsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: TlsSortField; -} +export const useNetworkTls = ({ + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + skip, + startDate, + type, +}: UseNetworkTls): [boolean, NetworkTlsArgs] => { + const getTlsSelector = networkSelectors.tlsSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getTlsSelector(state, type, flowTarget), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkTlsRequest, setHostRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.tls, + filterQuery: createFilter(filterQuery), + flowTarget, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setHostRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); + }, + [limit] + ); -type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; + const [networkTlsResponse, setNetworkTlsResponse] = useState({ + tls: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); -class TlsComponentQuery extends QueryTemplatePaginated< - TlsProps, - GetTlsQuery.Query, - GetTlsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetTlsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate ? startDate : '', - to: endDate ? endDate : new Date(Date.now()).toISOString(), - }, - }; - return ( - - query={tlsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const tls = getOr([], 'source.Tls.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const networkTlsSearch = useCallback( + (request: NetworkTlsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + signal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkTlsResponse((prevResponse) => ({ + ...prevResponse, + tls: response.edges, + inspect: response.inspect ?? prevResponse.inspect, + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_TLS); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ title: i18n.FAIL_NETWORK_TLS, text: msg.message }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Tls: { - ...fetchMoreResult.source.Tls, - edges: [...fetchMoreResult.source.Tls.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Tls.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Tls.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - tls, - totalCount: getOr(-1, 'source.Tls.totalCount', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTlsSelector(state, type, flowTarget), - isInspected, - }; - }; -}; + useEffect(() => { + setHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + + useEffect(() => { + networkTlsSearch(networkTlsRequest); + }, [networkTlsRequest, networkTlsSearch]); -export const TlsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(TlsComponentQuery); + return [loading, networkTlsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/translations.ts b/x-pack/plugins/security_solution/public/network/containers/tls/translations.ts new file mode 100644 index 0000000000000..aafa3ff0a98b0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/tls/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_NETWORK_TLS = i18n.translate( + 'xpack.securitySolution.networkTls.errorSearchDescription', + { + defaultMessage: `An error has occurred on network tls search`, + } +); + +export const FAIL_NETWORK_TLS = i18n.translate( + 'xpack.securitySolution.networkTls.failSearchDescription', + { + defaultMessage: `Failed to run search on network tls`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/tls_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/tls_query_table.tsx index f0c3628af78d8..5184fccecf07a 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/tls_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/tls_query_table.tsx @@ -8,7 +8,7 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import { manageQuery } from '../../../common/components/page/manage_query'; import { TlsTable } from '../../components/tls_table'; -import { TlsQuery } from '../../containers/tls'; +import { useNetworkTls } from '../../containers/tls'; import { TlsQueryTableComponentProps } from './types'; const TlsTableManage = manageQuery(TlsTable); @@ -22,34 +22,36 @@ export const TlsQueryTable = ({ skip, startDate, type, -}: TlsQueryTableComponentProps) => ( - - {({ id, inspect, isInspected, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - -); +}: TlsQueryTableComponentProps) => { + const [ + loading, + { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }, + ] = useNetworkTls({ + endDate, + filterQuery, + flowTarget, + ip, + skip, + startDate, + type, + }); + + return ( + + ); +}; TlsQueryTable.displayName = 'TlsQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx index 00da5496e5440..279891cc181e3 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { TlsQuery } from '../../../network/containers/tls'; +import { useNetworkTls } from '../../../network/containers/tls'; import { TlsTable } from '../../components/tls_table'; import { TlsQueryTabBodyProps } from './types'; const TlsTableManage = manageQuery(TlsTable); -export const TlsQueryTabBody = ({ +const TlsQueryTabBodyComponent: React.FC = ({ endDate, filterQuery, flowTarget, @@ -21,32 +21,38 @@ export const TlsQueryTabBody = ({ skip, startDate, type, -}: TlsQueryTabBodyProps) => ( - - {({ id, inspect, isInspected, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - -); +}) => { + const [ + loading, + { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }, + ] = useNetworkTls({ + endDate, + filterQuery, + flowTarget, + ip, + skip, + startDate, + type, + }); + + return ( + + ); +}; + +TlsQueryTabBodyComponent.displayName = 'TlsQueryTabBodyComponent'; + +export const TlsQueryTabBody = React.memo(TlsQueryTabBodyComponent); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index 1f5f0ccca3b70..e9ae66703f017 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -224,7 +224,7 @@ export const OpenTimeline = React.memo( popoverContent={getBatchItemsPopoverContent} data-test-subj="utility-bar-action" > - {i18n.BATCH_ACTIONS} + {i18n.BATCH_ACTIONS} )} diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts index 73f2124736a79..c28ffcf5b7a3f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.test.ts @@ -49,6 +49,36 @@ describe('ingest_integration tests ', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, }, manifest_version: '1.0.0', schema_version: 'v1', diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index 7f90aa7b91063..457e50b686863 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -14,6 +14,8 @@ export const ArtifactConstants = { GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', SAVED_OBJECT_TYPE: 'endpoint:user-artifact', SUPPORTED_OPERATING_SYSTEMS: ['macos', 'windows'], + SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], + GLOBAL_TRUSTED_APPS_NAME: 'endpoint-trustlist', }; export const ManifestConstants = { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index fea3b2b9a4526..a10ba9d6be38c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -11,6 +11,8 @@ import { getExceptionListItemSchemaMock } from '../../../../../lists/common/sche import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types'; import { buildArtifact, getFullEndpointExceptionList } from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; +import { ArtifactConstants } from './common'; +import { ENDPOINT_LIST_ID } from '../../../../../lists/common'; describe('buildEventTypeSignal', () => { let mockExceptionClient: ExceptionListClient; @@ -47,7 +49,12 @@ describe('buildEventTypeSignal', () => { const first = getFoundExceptionListItemSchemaMock(); mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -88,7 +95,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -134,7 +146,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -182,7 +199,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -229,7 +251,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -267,7 +294,12 @@ describe('buildEventTypeSignal', () => { first.data[1].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -305,7 +337,12 @@ describe('buildEventTypeSignal', () => { first.data[0].entries = testEntries; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp).toEqual({ entries: [expectedEndpointExceptions], }); @@ -329,7 +366,12 @@ describe('buildEventTypeSignal', () => { .mockReturnValueOnce(first) .mockReturnValueOnce(second); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); // Expect 2 exceptions, the first two calls returned the same exception list items expect(resp.entries.length).toEqual(2); @@ -340,7 +382,12 @@ describe('buildEventTypeSignal', () => { exceptionsResponse.data = []; exceptionsResponse.total = 0; mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); - const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1'); + const resp = await getFullEndpointExceptionList( + mockExceptionClient, + 'linux', + 'v1', + ENDPOINT_LIST_ID + ); expect(resp.entries.length).toEqual(0); }); @@ -385,8 +432,18 @@ describe('buildEventTypeSignal', () => { ], }; - const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); - const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + const artifact1 = await buildArtifact( + translatedExceptionList, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); }); @@ -430,8 +487,18 @@ describe('buildEventTypeSignal', () => { entries: translatedItems.reverse(), }; - const artifact1 = await buildArtifact(translatedExceptionList, 'linux', 'v1'); - const artifact2 = await buildArtifact(translatedExceptionListReversed, 'linux', 'v1'); + const artifact1 = await buildArtifact( + translatedExceptionList, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + const artifact2 = await buildArtifact( + translatedExceptionListReversed, + 'linux', + 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e41781dd605a0..731b083f3293c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -28,19 +28,20 @@ import { internalArtifactCompleteSchema, InternalArtifactCompleteSchema, } from '../../schemas'; -import { ArtifactConstants } from './common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, os: string, - schemaVersion: string + schemaVersion: string, + name: string ): Promise { const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); // Keep compression info empty in case its a duplicate. Lazily compress before committing if needed. return { - identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`, + identifier: `${name}-${os}-${schemaVersion}`, compressionAlgorithm: 'none', encryptionAlgorithm: 'none', decodedSha256: sha256, @@ -76,7 +77,8 @@ export function isCompressed(artifact: InternalArtifactSchema) { export async function getFullEndpointExceptionList( eClient: ExceptionListClient, os: string, - schemaVersion: string + schemaVersion: string, + listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID ): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; let page = 1; @@ -84,7 +86,7 @@ export async function getFullEndpointExceptionList( while (paging) { const response = await eClient.findExceptionListItem({ - listId: ENDPOINT_LIST_ID, + listId, namespaceType: 'agnostic', filter: `exception-list-agnostic.attributes._tags:\"os:${os}\"`, perPage: 100, diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts index 3d70f7266277f..507b81b12e10b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -94,6 +94,36 @@ describe('manifest', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + decoded_size: 432, + encoded_sha256: '975382ab55d019cbab0bbac207a54e2a7d489fad6e8f6de34fc6402e5ef37b1e', + encoded_size: 147, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + }, }, manifest_version: '1.0.0', schema_version: 'v1', @@ -107,6 +137,9 @@ describe('manifest', () => { ids: [ 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ], }); }); @@ -119,6 +152,21 @@ describe('manifest', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', @@ -139,6 +187,9 @@ describe('manifest', () => { expect(keys).toEqual([ 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', 'endpoint-exceptionlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', ]); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts index 61850bfb3bc7d..cdfbb551940e1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -16,13 +16,20 @@ import { ArtifactConstants } from './common'; import { Manifest } from './manifest'; export const getMockArtifacts = async (opts?: { compress: boolean }) => { - return Promise.all( - ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( + return Promise.all([ + // Exceptions items + ...ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map>( async (os) => { return getInternalArtifactMock(os, 'v1', opts); } - ) - ); + ), + // Trusted Apps items + ...ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS.map< + Promise + >(async (os) => { + return getInternalArtifactMock(os, 'v1', opts, ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME); + }), + ]); }; export const getMockArtifactsWithDiff = async (opts?: { compress: boolean }) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts index ae565f785c399..5d46fd0f2456b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { buildArtifact, maybeCompressArtifact, isCompressed } from '../../lib/artifacts'; +import { + buildArtifact, + maybeCompressArtifact, + isCompressed, + ArtifactConstants, +} from '../../lib/artifacts'; import { getTranslatedExceptionListMock } from './lists.mock'; import { InternalManifestSchema, @@ -25,9 +30,15 @@ const compressArtifact = async (artifact: InternalArtifactCompleteSchema) => { export const getInternalArtifactMock = async ( os: string, schemaVersion: string, - opts?: { compress: boolean } + opts?: { compress: boolean }, + artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME ): Promise => { - const artifact = await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + const artifact = await buildArtifact( + getTranslatedExceptionListMock(), + os, + schemaVersion, + artifactName + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -36,7 +47,12 @@ export const getEmptyInternalArtifactMock = async ( schemaVersion: string, opts?: { compress: boolean } ): Promise => { - const artifact = await buildArtifact({ entries: [] }, os, schemaVersion); + const artifact = await buildArtifact( + { entries: [] }, + os, + schemaVersion, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; @@ -47,7 +63,12 @@ export const getInternalArtifactMockWithDiffs = async ( ): Promise => { const mock = getTranslatedExceptionListMock(); mock.entries.pop(); - const artifact = await buildArtifact(mock, os, schemaVersion); + const artifact = await buildArtifact( + mock, + os, + schemaVersion, + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); return opts?.compress ? compressArtifact(artifact) : artifact; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index bb6504de6e0a4..40b408166b17f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -24,11 +24,41 @@ describe('manifest_manager', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, + { + id: + 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, ]); }); @@ -44,16 +74,53 @@ describe('manifest_manager', () => { 'endpoint-exceptionlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', type: 'delete', }, + { + id: + 'endpoint-trustlist-macos-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-windows-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, + { + id: + 'endpoint-trustlist-linux-v1-96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', + type: 'delete', + }, { id: 'endpoint-exceptionlist-macos-v1-0a5a2013a79f9e60682472284a1be45ab1ff68b9b43426d00d665016612c15c8', type: 'add', }, + { + id: + 'endpoint-trustlist-macos-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-windows-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, + { + id: + 'endpoint-trustlist-linux-v1-1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + type: 'add', + }, ]); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); - const artifact = newManifest.getArtifact(newArtifactId)!; + const firstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + + // Compress all `add` artifacts + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } + + const artifact = newManifest.getArtifact(firstNewArtifactId)!; if (isCompleteArtifact(artifact)) { await manifestManager.pushArtifacts([artifact]); // caches the artifact @@ -61,7 +128,7 @@ describe('manifest_manager', () => { throw new Error('Artifact is missing a body.'); } - const entry = JSON.parse(inflateSync(cache.get(newArtifactId)! as Buffer).toString()); + const entry = JSON.parse(inflateSync(cache.get(firstNewArtifactId)! as Buffer).toString()); expect(entry).toEqual({ entries: [ { @@ -107,8 +174,12 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } newManifest.bumpSemanticVersion(); @@ -145,6 +216,36 @@ describe('manifest_manager', () => { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/96b76a1a911662053a1562ac14c4ff1e87c2ff550d6fe52e1e0b3790526597d3', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: '1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + decoded_size: 287, + encoded_sha256: 'c3dec543df1177561ab2aa74a37997ea3c1d748d532a597884f5a5c16670d56c', + encoded_size: 133, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/1a8295e6ccb93022c6f5ceb8997b29f2912389b3b38f52a8f5a2ff7b0154b1bc', + }, }, }); }); @@ -155,8 +256,12 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } newManifest.bumpSemanticVersion(); @@ -174,11 +279,17 @@ describe('manifest_manager', () => { const oldManifest = await manifestManager.getLastComputedManifest(); const newManifest = await manifestManager.buildNewManifest(oldManifest!); const diffs = newManifest.diff(oldManifest!); - const oldArtifactId = diffs[0].id; - const newArtifactId = diffs[1].id; - await newManifest.compressArtifact(newArtifactId); + const firstOldArtifactId = diffs.find((diff) => diff.type === 'delete')!.id; + const FirstNewArtifactId = diffs.find((diff) => diff.type === 'add')!.id; + + // Compress all new artifacts + for (const artifactDiff of diffs) { + if (artifactDiff.type === 'add') { + await newManifest.compressArtifact(artifactDiff.id); + } + } - const artifact = newManifest.getArtifact(newArtifactId)!; + const artifact = newManifest.getArtifact(FirstNewArtifactId)!; if (isCompleteArtifact(artifact)) { await manifestManager.pushArtifacts([artifact]); } else { @@ -186,7 +297,7 @@ describe('manifest_manager', () => { } await manifestManager.commit(newManifest); - await manifestManager.deleteArtifacts([oldArtifactId]); + await manifestManager.deleteArtifacts([firstOldArtifactId]); // created new artifact expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( @@ -201,7 +312,7 @@ describe('manifest_manager', () => { // deleted old artifact expect(savedObjectsClient.delete).toHaveBeenCalledWith( ArtifactConstants.SAVED_OBJECT_TYPE, - oldArtifactId + firstOldArtifactId ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 70557886e57c5..f9836c7ecdc30 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -13,11 +13,11 @@ import { manifestDispatchSchema } from '../../../../../common/endpoint/schema/ma import { ArtifactConstants, - Manifest, buildArtifact, + getArtifactId, getFullEndpointExceptionList, + Manifest, ManifestDiff, - getArtifactId, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -25,6 +25,8 @@ import { } from '../../../schemas/artifacts'; import { ArtifactClient } from '../artifact_client'; import { ManifestClient } from '../manifest_client'; +import { ENDPOINT_LIST_ID } from '../../../../../../lists/common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants'; export interface ManifestManagerContext { savedObjectsClient: SavedObjectsClientContract; @@ -87,9 +89,43 @@ export class ManifestManager { const exceptionList = await getFullEndpointExceptionList( this.exceptionListClient, os, - artifactSchemaVersion ?? 'v1' + artifactSchemaVersion ?? 'v1', + ENDPOINT_LIST_ID + ); + const artifact = await buildArtifact( + exceptionList, + os, + artifactSchemaVersion ?? 'v1', + ArtifactConstants.GLOBAL_ALLOWLIST_NAME + ); + artifacts.push(artifact); + } + return artifacts; + } + + /** + * Builds an array of artifacts (one per supported OS) based on the current state of the + * Trusted Apps list (which uses the `exception-list-agnostic` SO type) + * @param artifactSchemaVersion + */ + protected async buildTrustedAppsArtifacts( + artifactSchemaVersion?: string + ): Promise { + const artifacts: InternalArtifactCompleteSchema[] = []; + + for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { + const trustedApps = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + artifactSchemaVersion ?? 'v1', + ENDPOINT_TRUSTED_APPS_LIST_ID + ); + const artifact = await buildArtifact( + trustedApps, + os, + 'v1', + ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME ); - const artifact = await buildArtifact(exceptionList, os, artifactSchemaVersion ?? 'v1'); artifacts.push(artifact); } return artifacts; @@ -205,7 +241,9 @@ export class ManifestManager { */ public async buildNewManifest(baselineManifest?: Manifest): Promise { // Build new exception list artifacts - const artifacts = await this.buildExceptionListArtifacts(); + const artifacts = ( + await Promise.all([this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts()]) + ).flat(); // Build new manifest const manifest = Manifest.fromArtifacts( diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts index a9101f54ada55..ea1b896452c4e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts @@ -10,8 +10,10 @@ import { Direction, HostsRequestOptions, SortField, + HostsFields, } from '../../../../../../common/search_strategy/security_solution'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; +import { assertUnreachable } from '../../../../../../common/utility_types'; export const buildHostsQuery = ({ defaultIndex, @@ -77,11 +79,13 @@ export const buildHostsQuery = ({ type QueryOrder = { lastSeen: Direction } | { _key: Direction }; -const getQueryOrder = (sort: SortField): QueryOrder => { +const getQueryOrder = (sort: SortField): QueryOrder => { switch (sort.field) { - case 'lastSeen': + case HostsFields.lastSeen: return { lastSeen: sort.direction }; - case 'hostName': + case HostsFields.hostName: return { _key: sort.direction }; + default: + return assertUnreachable(sort.field as never); } }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts index 53433dfc208cb..a50c9e4004856 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts @@ -7,6 +7,7 @@ import { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution'; import { hostsFactory } from './hosts'; +import { networkFactory } from './network'; import { SecuritySolutionFactory } from './types'; export const securitySolutionFactory: Record< @@ -14,4 +15,5 @@ export const securitySolutionFactory: Record< SecuritySolutionFactory > = { ...hostsFactory, + ...networkFactory, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts new file mode 100644 index 0000000000000..2c21d9741d648 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution'; +import { NetworkQueries } from '../../../../../common/search_strategy/security_solution/network'; + +import { SecuritySolutionFactory } from '../types'; +import { networkTls } from './tls'; + +export const networkFactory: Record> = { + [NetworkQueries.tls]: networkTls, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts new file mode 100644 index 0000000000000..59359fd35a34e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts @@ -0,0 +1,35 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + TlsBuckets, + TlsEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getTlsEdges = (response: IEsSearchResponse): TlsEdges[] => + formatTlsEdges(getOr([], 'aggregations.sha1.buckets', response.rawResponse)); + +export const formatTlsEdges = (buckets: TlsBuckets[]): TlsEdges[] => + buckets.map((bucket: TlsBuckets) => { + const edge: TlsEdges = { + node: { + _id: bucket.key, + subjects: bucket.subjects.buckets.map(({ key }) => key), + ja3: bucket.ja3.buckets.map(({ key }) => key), + issuers: bucket.issuers.buckets.map(({ key }) => key), + // eslint-disable-next-line @typescript-eslint/naming-convention + notAfter: bucket.not_after.buckets.map(({ key_as_string }) => key_as_string), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + }; + return edge; + }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts new file mode 100644 index 0000000000000..32836c0ef6869 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts @@ -0,0 +1,59 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkTlsStrategyResponse, + NetworkQueries, + NetworkTlsRequestOptions, + TlsEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getTlsEdges } from './helpers'; +import { buildTlsQuery } from './query.tls_network.dsl'; + +export const networkTls: SecuritySolutionFactory = { + buildDsl: (options: NetworkTlsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildTlsQuery(options); + }, + parse: async ( + options: NetworkTlsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.count.value', response.rawResponse); + const tlsEdges: TlsEdges[] = getTlsEdges(response); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = tlsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildTlsQuery(options))], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts new file mode 100644 index 0000000000000..eb4e25c29e3a1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts @@ -0,0 +1,108 @@ +/* + * 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 { assertUnreachable } from '../../../../../../common/utility_types'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +import { + NetworkTlsRequestOptions, + SortField, + Direction, + TlsFields, +} from '../../../../../../common/search_strategy/security_solution'; + +const getAggs = (querySize: number, sort: SortField) => ({ + count: { + cardinality: { + field: 'tls.server.hash.sha1', + }, + }, + sha1: { + terms: { + field: 'tls.server.hash.sha1', + size: querySize, + order: { + ...getQueryOrder(sort), + }, + }, + aggs: { + issuers: { + terms: { + field: 'tls.server.issuer', + }, + }, + subjects: { + terms: { + field: 'tls.server.subject', + }, + }, + not_after: { + terms: { + field: 'tls.server.not_after', + }, + }, + ja3: { + terms: { + field: 'tls.server.ja3s', + }, + }, + }, + }, +}); + +export const buildTlsQuery = ({ + ip, + sort, + filterQuery, + flowTarget, + pagination: { querySize }, + defaultIndex, + timerange: { from, to }, +}: NetworkTlsRequestOptions) => { + const defaultFilter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, + ]; + + const filter = ip ? [...defaultFilter, { term: { [`${flowTarget}.ip`]: ip } }] : defaultFilter; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggs: { + ...getAggs(querySize, sort), + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; + +interface QueryOrder { + _key: Direction; +} + +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { + case TlsFields._id: + return { _key: sort.direction }; + default: + return assertUnreachable(sort.field); + } +}; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 0abc47686a6b4..6021ab2a42c90 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -20,13 +20,6 @@ import { REPOSITORY_NAME } from './helpers/constant'; const { setup } = pageHelpers.home; -jest.mock('ui/new_platform'); - -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - // Mocking FormattedDate and FormattedTime due to timezone differences on CI jest.mock('@kbn/i18n/react', () => { const original = jest.requireActual('@kbn/i18n/react'); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index f480e937f162a..dc568161d4fb4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -17,11 +17,6 @@ import { DEFAULT_POLICY_SCHEDULE } from '../../public/application/constants'; const { setup } = pageHelpers.policyAdd; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - // mock for EuiSelectable's virtualization jest.mock('react-virtualized-auto-sizer', () => { return ({ diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 7eec80890ca86..a5b2ec73b85cd 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -18,11 +18,6 @@ const { setup: setupPolicyAdd } = pageHelpers.policyAdd; const EXPIRE_AFTER_VALUE = '5'; const EXPIRE_AFTER_UNIT = TIME_UNITS.MINUTE; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: PolicyFormTestBed; let testBedPolicyAdd: PolicyFormTestBed; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts index 2a9e17a1c9c4d..e0c9aeffa09e8 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_add.test.ts @@ -11,16 +11,9 @@ import { RepositoryType } from '../../common/types'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { RepositoryAddTestBed } from './helpers/repository_add.helpers'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.repositoryAdd; const repositoryTypes = ['fs', 'url', 'source', 'azure', 'gcs', 's3', 'hdfs']; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: RepositoryAddTestBed; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts index bab276584966b..1606db07b57b4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/repository_edit.test.ts @@ -12,16 +12,9 @@ import { RepositoryEditTestSubjects } from './helpers/repository_edit.helpers'; import { RepositoryAddTestSubjects } from './helpers/repository_add.helpers'; import { REPOSITORY_EDIT } from './helpers/constant'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.repositoryEdit; const { setup: setupRepositoryAdd } = pageHelpers.repositoryAdd; -jest.mock('ui/i18n', () => { - const I18nContext = ({ children }: any) => children; - return { I18nContext }; -}); - describe('', () => { let testBed: TestBed; let testBedRepositoryAdd: TestBed; diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index 473a3392deb3e..e5d8e804691a8 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -4,55 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserializeSnapshotDetails } from './snapshot_serialization'; +import { deserializeSnapshotDetails, serializeSnapshotConfig } from './snapshot_serialization'; -describe('deserializeSnapshotDetails', () => { - test('deserializes a snapshot', () => { - expect( - deserializeSnapshotDetails( - 'repositoryName', - { - snapshot: 'snapshot name', - uuid: 'UUID', - version_id: 5, - version: 'version', - indices: ['index2', 'index3', 'index1'], - include_global_state: false, - state: 'SUCCESS', - start_time: '0', - start_time_in_millis: 0, - end_time: '1', - end_time_in_millis: 1, - duration_in_millis: 1, - shards: { - total: 3, - failed: 1, - successful: 2, - }, - failures: [ - { - index: 'z', - shard: 1, - }, - { - index: 'a', - shard: 3, - }, - { - index: 'a', - shard: 1, - }, - { - index: 'a', - shard: 2, - }, - ], - }, - 'found-snapshots', - [ +describe('Snapshot serialization and deserialization', () => { + describe('deserializeSnapshotDetails', () => { + test('deserializes a snapshot', () => { + expect( + deserializeSnapshotDetails( + 'repositoryName', { - snapshot: 'last_successful_snapshot', - uuid: 'last_successful_snapshot_UUID', + snapshot: 'snapshot name', + uuid: 'UUID', version_id: 5, version: 'version', indices: ['index2', 'index3', 'index1'], @@ -87,56 +49,109 @@ describe('deserializeSnapshotDetails', () => { }, ], }, - ] - ) - ).toEqual({ - repository: 'repositoryName', - snapshot: 'snapshot name', - uuid: 'UUID', - versionId: 5, - version: 'version', - // Indices are sorted. - indices: ['index1', 'index2', 'index3'], - dataStreams: [], - includeGlobalState: false, - // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. - indexFailures: [ - { - index: 'a', - failures: [ + 'found-snapshots', + [ { - shard: 1, - }, - { - shard: 2, - }, - { - shard: 3, - }, - ], - }, - { - index: 'z', - failures: [ - { - shard: 1, + snapshot: 'last_successful_snapshot', + uuid: 'last_successful_snapshot_UUID', + version_id: 5, + version: 'version', + indices: ['index2', 'index3', 'index1'], + include_global_state: false, + state: 'SUCCESS', + start_time: '0', + start_time_in_millis: 0, + end_time: '1', + end_time_in_millis: 1, + duration_in_millis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, + }, + failures: [ + { + index: 'z', + shard: 1, + }, + { + index: 'a', + shard: 3, + }, + { + index: 'a', + shard: 1, + }, + { + index: 'a', + shard: 2, + }, + ], }, - ], + ] + ) + ).toEqual({ + repository: 'repositoryName', + snapshot: 'snapshot name', + uuid: 'UUID', + versionId: 5, + version: 'version', + // Indices are sorted. + indices: ['index1', 'index2', 'index3'], + dataStreams: [], + includeGlobalState: false, + // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. + indexFailures: [ + { + index: 'a', + failures: [ + { + shard: 1, + }, + { + shard: 2, + }, + { + shard: 3, + }, + ], + }, + { + index: 'z', + failures: [ + { + shard: 1, + }, + ], + }, + ], + state: 'SUCCESS', + startTime: '0', + startTimeInMillis: 0, + endTime: '1', + endTimeInMillis: 1, + durationInMillis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, }, - ], - state: 'SUCCESS', - startTime: '0', - startTimeInMillis: 0, - endTime: '1', - endTimeInMillis: 1, - durationInMillis: 1, - shards: { - total: 3, - failed: 1, - successful: 2, - }, - managedRepository: 'found-snapshots', - isLastSuccessfulSnapshot: false, + managedRepository: 'found-snapshots', + isLastSuccessfulSnapshot: false, + }); + }); + }); + + describe('serializeSnapshotConfig', () => { + test('serializes config as expected', () => { + const metadata = { test: 'what have you' }; + expect(serializeSnapshotConfig({ metadata, indices: '.k*' })).toEqual({ + metadata, + indices: ['.k*'], + }); + }); + test('serializes empty config as expected', () => { + expect(serializeSnapshotConfig({})).toEqual({}); }); }); }); diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index a85b49430eecd..61a57c9288f03 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -131,10 +131,10 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs { const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig; - const indicesArray = csvToArray(indices); + const maybeIndicesArray = csvToArray(indices); const snapshotConfigEs: SnapshotConfigEs = { - indices: indicesArray, + indices: maybeIndicesArray, ignore_unavailable: ignoreUnavailable, include_global_state: includeGlobalState, partial, diff --git a/x-pack/plugins/snapshot_restore/common/lib/utils.ts b/x-pack/plugins/snapshot_restore/common/lib/utils.ts index 96eb7cb6908d8..3d07aa8de3a55 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/utils.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/utils.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export const csvToArray = (indices?: string | string[]): string[] => { +export const csvToArray = (indices?: string | string[]): string[] | undefined => { return indices && Array.isArray(indices) ? indices : typeof indices === 'string' ? indices.split(',') - : []; + : undefined; }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts index 275915c5760af..7ec34dd7b3984 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/use_collapsible_list.ts @@ -24,7 +24,7 @@ const maximumItemPreviewCount = 10; export const useCollapsibleList = ({ items }: Arg): ReturnValue => { const [isShowingFullList, setIsShowingFullList] = useState(false); - const itemsArray = csvToArray(items); + const itemsArray = csvToArray(items) ?? []; const displayItems: ChildItems = items === undefined ? 'all' diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx index ae22464c8a52b..407b9be14e3c1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_retention.tsx @@ -35,11 +35,17 @@ export const PolicyStepRetention: React.FunctionComponent = ({ }) => { const { retention = {} } = policy; - const updatePolicyRetention = (updatedFields: Partial): void => { + const updatePolicyRetention = ( + updatedFields: Partial, + validationHelperData = {} + ): void => { const newRetention = { ...retention, ...updatedFields }; - updatePolicy({ - retention: newRetention, - }); + updatePolicy( + { + retention: newRetention, + }, + validationHelperData + ); }; // State for touched inputs diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx index 94854905e6686..6f89427516453 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx @@ -25,7 +25,7 @@ import { import { SlmPolicyPayload } from '../../../../../../../../common/types'; import { useServices } from '../../../../../../app_context'; -import { PolicyValidation } from '../../../../../../services/validation'; +import { PolicyValidation, ValidatePolicyHelperData } from '../../../../../../services/validation'; import { orderDataStreamsAndIndices } from '../../../../../lib'; import { DataStreamBadge } from '../../../../../data_stream_badge'; @@ -34,12 +34,16 @@ import { mapSelectionToIndicesOptions, determineListMode } from './helpers'; import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text'; +interface IndicesConfig { + indices?: string[] | string; +} + interface Props { isManagedPolicy: boolean; policy: SlmPolicyPayload; indices: string[]; dataStreams: string[]; - onUpdate: (arg: { indices?: string[] | string }) => void; + onUpdate: (arg: IndicesConfig, validateHelperData: ValidatePolicyHelperData) => void; errors: PolicyValidation['errors']; } @@ -53,7 +57,7 @@ export const IndicesAndDataStreamsField: FunctionComponent = ({ dataStreams, indices, policy, - onUpdate, + onUpdate: _onUpdate, errors, }) => { const { i18n } = useServices(); @@ -66,6 +70,12 @@ export const IndicesAndDataStreamsField: FunctionComponent = ({ !config.indices || (Array.isArray(config.indices) && config.indices.length === 0) ); + const onUpdate = (data: IndicesConfig) => { + _onUpdate(data, { + validateIndicesCount: !isAllIndices, + }); + }; + const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState( () => Array.isArray(config.indices) && !isAllIndices diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index 9d43c45d17ea7..f65156bada278 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -31,11 +31,17 @@ export const PolicyStepSettings: React.FunctionComponent = ({ }) => { const { config = {}, isManagedPolicy } = policy; - const updatePolicyConfig = (updatedFields: Partial): void => { + const updatePolicyConfig = ( + updatedFields: Partial, + validationHelperData = {} + ): void => { const newConfig = { ...config, ...updatedFields }; - updatePolicy({ - config: newConfig, - }); + updatePolicy( + { + config: newConfig, + }, + validationHelperData + ); }; const renderIgnoreUnavailableField = () => ( diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index d9fd4cca0d614..855abb04b7c2b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -311,7 +311,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = }); } }} - selectedIndicesAndDataStreams={csvToArray(restoreIndices)} + selectedIndicesAndDataStreams={csvToArray(restoreIndices) ?? []} indices={snapshotIndices} dataStreams={snapshotDataStreams} /> diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/attempt_to_uri_decode.ts b/x-pack/plugins/snapshot_restore/public/application/lib/attempt_to_uri_decode.ts new file mode 100644 index 0000000000000..6f99fae118cd7 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/lib/attempt_to_uri_decode.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 attemptToURIDecode = (value: string) => { + let result: string; + + try { + result = decodeURI(value); + result = decodeURIComponent(result); + } catch (e1) { + try { + result = decodeURIComponent(value); + } catch (e2) { + result = value; + } + } + + return result; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/index.ts new file mode 100644 index 0000000000000..c544df4365606 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { useDecodedParams } from './use_decoded_params'; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/use_decoded_params.ts b/x-pack/plugins/snapshot_restore/public/application/lib/use_decoded_params.ts new file mode 100644 index 0000000000000..d4582de4084ba --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/lib/use_decoded_params.ts @@ -0,0 +1,23 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import { attemptToURIDecode } from './attempt_to_uri_decode'; + +export const useDecodedParams = < + Params extends { [K in keyof Params]?: string } = {} +>(): Params => { + const params = useParams>(); + const decodedParams = {} as Params; + + for (const [key, value] of Object.entries(params)) { + if (value) { + (decodedParams as any)[key] = attemptToURIDecode(value); + } + } + + return decodedParams; +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx index 962dfa73e95c7..12e7e2de9383d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/home.tsx @@ -89,7 +89,7 @@ export const SnapshotRestoreHome: React.FunctionComponent { - history.push(`${BASE_PATH}/${newSection}`); + history.push(encodeURI(`${BASE_PATH}/${encodeURIComponent(newSection)}`)); }; // Set breadcrumb and page title diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx index 5959ad6441f5d..f67e8eb586238 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx @@ -24,6 +24,8 @@ import { EuiSpacer, } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; + import { SlmPolicy } from '../../../../../../common/types'; import { useServices } from '../../../../app_context'; import { SectionError, Error } from '../../../../../shared_imports'; @@ -41,8 +43,6 @@ import { } from '../../../../components'; import { TabSummary, TabHistory } from './tabs'; -import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; - interface Props { policyName: SlmPolicy['name']; onClose: () => void; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx index 39ef66eb0658c..655bd0e9d8bb9 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx @@ -9,6 +9,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiEmptyPrompt, EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; + import { SectionError, Error, @@ -20,6 +22,7 @@ import { SlmPolicy } from '../../../../../common/types'; import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants'; +import { useDecodedParams } from '../../../lib'; import { useLoadPolicies, useLoadRetentionSettings } from '../../../services/http'; import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation'; import { useServices } from '../../../app_context'; @@ -28,18 +31,14 @@ import { PolicyDetails } from './policy_details'; import { PolicyTable } from './policy_table'; import { PolicyRetentionSchedule } from './policy_retention_schedule'; -import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; - interface MatchParams { policyName?: SlmPolicy['name']; } export const PolicyList: React.FunctionComponent> = ({ - match: { - params: { policyName }, - }, history, }) => { + const { policyName } = useDecodedParams(); const { error, isLoading, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx index f6b9ac08cc0a2..9afdad3806def 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx @@ -7,11 +7,14 @@ import React, { Fragment, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; - import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; + import { Repository } from '../../../../../common/types'; import { SectionError, Error } from '../../../../shared_imports'; import { SectionLoading } from '../../../components'; +import { useDecodedParams } from '../../../lib'; import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants'; import { useServices } from '../../../app_context'; import { useLoadRepositories } from '../../../services/http'; @@ -20,18 +23,14 @@ import { linkToAddRepository, linkToRepository } from '../../../services/navigat import { RepositoryDetails } from './repository_details'; import { RepositoryTable } from './repository_table'; -import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; - interface MatchParams { repositoryName?: Repository['name']; } export const RepositoryList: React.FunctionComponent> = ({ - match: { - params: { repositoryName }, - }, history, }) => { + const { repositoryName } = useDecodedParams(); const { error, isLoading, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx index 2b8df7294c374..d13188fc44730 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx @@ -24,6 +24,7 @@ import { linkToSnapshot, } from '../../../services/navigation'; import { useServices } from '../../../app_context'; +import { useDecodedParams } from '../../../lib'; import { SnapshotDetails } from './snapshot_details'; import { SnapshotTable } from './snapshot_table'; @@ -35,12 +36,10 @@ interface MatchParams { } export const SnapshotList: React.FunctionComponent> = ({ - match: { - params: { repositoryName, snapshotId }, - }, location: { search }, history, }) => { + const { repositoryName, snapshotId } = useDecodedParams(); const { error, isLoading, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx index 90cd26c821c5e..dead9abb6ef0c 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx @@ -43,7 +43,7 @@ export const PolicyAdd: React.FunctionComponent = ({ if (error) { setSaveError(error); } else { - history.push(`${BASE_PATH}/policies/${name}`); + history.push(encodeURI(`${BASE_PATH}/policies/${encodeURIComponent(name)}`)); } }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx index 915b1cbef1233..7af663b29957d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx @@ -10,6 +10,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; import { SectionError, Error } from '../../../shared_imports'; +import { useDecodedParams } from '../../lib'; import { TIME_UNITS } from '../../../../common/constants'; import { SectionLoading, PolicyForm } from '../../components'; import { BASE_PATH } from '../../constants'; @@ -22,12 +23,10 @@ interface MatchParams { } export const PolicyEdit: React.FunctionComponent> = ({ - match: { - params: { name }, - }, history, location: { pathname }, }) => { + const { name } = useDecodedParams(); const { i18n } = useServices(); // Set breadcrumb and page title @@ -83,12 +82,12 @@ export const PolicyEdit: React.FunctionComponent { - history.push(`${BASE_PATH}/policies/${name}`); + history.push(encodeURI(`${BASE_PATH}/policies/${encodeURIComponent(name)}`)); }; const renderLoading = () => { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx index 08bfde833c368..a66d137471eba 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx @@ -44,7 +44,11 @@ export const RepositoryAdd: React.FunctionComponent = ({ } else { const { redirect } = parse(search.replace(/^\?/, ''), { sort: false }); - history.push(redirect ? (redirect as string) : `${BASE_PATH}/${section}/${name}`); + history.push( + redirect + ? (redirect as string) + : encodeURI(`${BASE_PATH}/${encodeURIComponent(section)}/${encodeURIComponent(name)}`) + ); } }; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx index 95f8b9b8bde7d..3e8cff5793572 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx @@ -16,18 +16,17 @@ import { BASE_PATH, Section } from '../../constants'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { editRepository, useLoadRepository } from '../../services/http'; +import { useDecodedParams } from '../../lib'; interface MatchParams { name: string; } export const RepositoryEdit: React.FunctionComponent> = ({ - match: { - params: { name }, - }, history, }) => { const { i18n } = useServices(); + const { name } = useDecodedParams(); const section = 'repositories' as Section; // Set breadcrumb and page title @@ -70,7 +69,9 @@ export const RepositoryEdit: React.FunctionComponent> = ({ - match: { - params: { repositoryName, snapshotId }, - }, history, }) => { const { i18n } = useServices(); + const { repositoryName, snapshotId } = useDecodedParams(); // Set breadcrumb and page title useEffect(() => { diff --git a/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts b/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts index 503704c6fe820..b498dc280d395 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/navigation/links.ts @@ -13,33 +13,37 @@ export function linkToRepositories() { } export function linkToRepository(repositoryName: string) { - return `/repositories/${encodeURIComponent(repositoryName)}`; + return encodeURI(`/repositories/${encodeURIComponent(repositoryName)}`); } export function linkToEditRepository(repositoryName: string) { - return `/edit_repository/${encodeURIComponent(repositoryName)}`; + return encodeURI(`/edit_repository/${encodeURIComponent(repositoryName)}`); } export function linkToAddRepository(redirect?: string) { - return `/add_repository${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''}`; + return encodeURI(`/add_repository${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''}`); } export function linkToSnapshots(repositoryName?: string, policyName?: string) { if (repositoryName) { - return `/snapshots?repository=${encodeURIComponent(repositoryName)}`; + return encodeURI(`/snapshots?repository=${encodeURIComponent(repositoryName)}`); } if (policyName) { - return `/snapshots?policy=${encodeURIComponent(policyName)}`; + return encodeURI(`/snapshots?policy=${encodeURIComponent(policyName)}`); } return `/snapshots`; } export function linkToSnapshot(repositoryName: string, snapshotName: string) { - return `/snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}`; + return encodeURI( + `/snapshots/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}` + ); } export function linkToRestoreSnapshot(repositoryName: string, snapshotName: string) { - return `/restore/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}`; + return encodeURI( + `/restore/${encodeURIComponent(repositoryName)}/${encodeURIComponent(snapshotName)}` + ); } export function linkToPolicies() { @@ -47,11 +51,11 @@ export function linkToPolicies() { } export function linkToPolicy(policyName: string) { - return `/policies/${encodeURIComponent(policyName)}`; + return encodeURI(`/policies/${encodeURIComponent(policyName)}`); } export function linkToEditPolicy(policyName: string) { - return `/edit_policy/${encodeURIComponent(policyName)}`; + return encodeURI(`/edit_policy/${encodeURIComponent(policyName)}`); } export function linkToAddPolicy() { diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts index 7fd755497eec6..82bf0ef06c400 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/index.ts @@ -12,4 +12,4 @@ export { export { RestoreValidation, validateRestore } from './validate_restore'; -export { PolicyValidation, validatePolicy } from './validate_policy'; +export { PolicyValidation, validatePolicy, ValidatePolicyHelperData } from './validate_policy'; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts index 24960b2533230..4314b703722f6 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts @@ -25,21 +25,32 @@ const isSnapshotNameNotLowerCase = (str: string): boolean => { return strExcludeDate !== strExcludeDate.toLowerCase() ? true : false; }; +export interface ValidatePolicyHelperData { + managedRepository?: { + name: string; + policy: string; + }; + isEditing?: boolean; + policyName?: string; + /** + * Whether to block on the indices configured for this snapshot. + * + * By default ES will back up all indices and data streams if this is an empty array or left blank. + * However, in the UI, under certain conditions, like when displaying indices to select for backup, + * we want to block users from submitting an empty array, but not block the entire form if they + * are not configuring this value - like when they are on a previous step. + */ + validateIndicesCount?: boolean; +} + export const validatePolicy = ( policy: SlmPolicyPayload, - validationHelperData: { - managedRepository?: { - name: string; - policy: string; - }; - isEditing?: boolean; - policyName?: string; - } + validationHelperData: ValidatePolicyHelperData ): PolicyValidation => { const i18n = textService.i18n; const { name, snapshotName, schedule, repository, config, retention } = policy; - const { managedRepository, isEditing, policyName } = validationHelperData; + const { managedRepository, isEditing, policyName, validateIndicesCount } = validationHelperData; const validation: PolicyValidation = { isValid: true, @@ -96,7 +107,12 @@ export const validatePolicy = ( ); } - if (config && typeof config.indices === 'string' && config.indices.trim().length === 0) { + if ( + validateIndicesCount && + config && + typeof config.indices === 'string' && + config.indices.trim().length === 0 + ) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.policyValidation.indexPatternRequiredErrorMessage', { defaultMessage: 'At least one index pattern is required.', @@ -104,7 +120,12 @@ export const validatePolicy = ( ); } - if (config && Array.isArray(config.indices) && config.indices.length === 0) { + if ( + validateIndicesCount && + config && + Array.isArray(config.indices) && + config.indices.length === 0 + ) { validation.errors.indices.push( i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredErrorMessage', { defaultMessage: 'You must select at least one data stream or index.', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 83dac4912d4ac..b70ab0bb25561 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -253,31 +253,6 @@ "charts.controls.rangeErrorMessage": "値は{min}と{max}の間でなければなりません", "charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "common.ui.flotCharts.aprLabel": "4 月", - "common.ui.flotCharts.augLabel": "8 月", - "common.ui.flotCharts.decLabel": "12 月", - "common.ui.flotCharts.febLabel": "2 月", - "common.ui.flotCharts.friLabel": "金", - "common.ui.flotCharts.janLabel": "1 月", - "common.ui.flotCharts.julLabel": "7 月", - "common.ui.flotCharts.junLabel": "6 月", - "common.ui.flotCharts.marLabel": "3 月", - "common.ui.flotCharts.mayLabel": "5 月", - "common.ui.flotCharts.monLabel": "月", - "common.ui.flotCharts.novLabel": "11 月", - "common.ui.flotCharts.octLabel": "10 月", - "common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage": "キャンバス内のラベルではパイを作成できません", - "common.ui.flotCharts.satLabel": "土", - "common.ui.flotCharts.sepLabel": "9 月", - "common.ui.flotCharts.sunLabel": "日", - "common.ui.flotCharts.thuLabel": "木", - "common.ui.flotCharts.tueLabel": "火", - "common.ui.flotCharts.wedLabel": "水", - "common.ui.stateManagement.unableToParseUrlErrorMessage": "URL をパースできません", - "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", - "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", - "common.ui.url.replacementFailedErrorMessage": "置換に失敗、未解決の表現式: {expr}", - "common.ui.url.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", "console.autocomplete.addMethodMetaText": "メソド", "console.consoleDisplayName": "コンソール", "console.consoleMenu.copyAsCurlMessage": "リクエストが URL としてコピーされました", @@ -5067,7 +5042,6 @@ "xpack.apm.transactionActionMenu.viewInUptime": "ステータス", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示", "xpack.apm.transactionBreakdown.chartTitle": "スパンタイプ別時間", - "xpack.apm.transactionBreakdown.noData": "この時間範囲のデータがありません。", "xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", @@ -11634,7 +11608,6 @@ "xpack.ml.jobsList.deleteJobModal.cancelButtonLabel": "キャンセル", "xpack.ml.jobsList.deleteJobModal.closeButtonLabel": "閉じる", "xpack.ml.jobsList.deleteJobModal.deleteButtonLabel": "削除", - "xpack.ml.jobsList.deleteJobModal.deleteJobsDescription": "{jobsCount, plural, one {このジョブ} other {これらのジョブ}}を削除してよろしいですか?", "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "{jobsCount, plural, one {{jobId}} other {# 件のジョブ}}を削除", "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "{jobsCount, plural, one {ジョブ} other {複数ジョブ}}の削除には時間がかかる場合があります。{jobsCount, plural, one {} other {}}バックグラウンドで削除され、ジョブリストからすぐに消えない場合があります", "xpack.ml.jobsList.deleteJobModal.deletingJobsStatusLabel": "ジョブを削除中", @@ -12303,7 +12276,6 @@ "xpack.ml.ruleEditor.deleteJobRule.ruleNoLongerExistsErrorMessage": "ジョブ {jobId} の検知器インデックス {detectorIndex} のルールが現在存在しません", "xpack.ml.ruleEditor.deleteRuleModal.cancelButtonLabel": "キャンセル", "xpack.ml.ruleEditor.deleteRuleModal.deleteButtonLabel": "削除", - "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleDescription": "このルールを削除してよろしいですか?", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleLinkText": "ルールを削除", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "ルールの削除", "xpack.ml.ruleEditor.detectorDescriptionList.detectorTitle": "検知器", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8b3a1941f450a..72010e00dc820 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -253,31 +253,6 @@ "charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", "charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "common.ui.flotCharts.aprLabel": "四月", - "common.ui.flotCharts.augLabel": "八月", - "common.ui.flotCharts.decLabel": "十二月", - "common.ui.flotCharts.febLabel": "二月", - "common.ui.flotCharts.friLabel": "周五", - "common.ui.flotCharts.janLabel": "一月", - "common.ui.flotCharts.julLabel": "七月", - "common.ui.flotCharts.junLabel": "六月", - "common.ui.flotCharts.marLabel": "三月", - "common.ui.flotCharts.mayLabel": "五月", - "common.ui.flotCharts.monLabel": "周一", - "common.ui.flotCharts.novLabel": "十一月", - "common.ui.flotCharts.octLabel": "十月", - "common.ui.flotCharts.pie.unableToDrawLabelsInsideCanvasErrorMessage": "无法用画布内包含的标签绘制饼图", - "common.ui.flotCharts.satLabel": "周六", - "common.ui.flotCharts.sepLabel": "九月", - "common.ui.flotCharts.sunLabel": "周日", - "common.ui.flotCharts.thuLabel": "周四", - "common.ui.flotCharts.tueLabel": "周二", - "common.ui.flotCharts.wedLabel": "周三", - "common.ui.stateManagement.unableToParseUrlErrorMessage": "无法解析 URL", - "common.ui.stateManagement.unableToRestoreUrlErrorMessage": "无法完整还原 URL,确保使用共享功能。", - "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", - "common.ui.url.replacementFailedErrorMessage": "替换失败,未解析的表达式:{expr}", - "common.ui.url.savedObjectIsMissingNotificationMessage": "已保存对象缺失", "console.autocomplete.addMethodMetaText": "方法", "console.consoleDisplayName": "控制台", "console.consoleMenu.copyAsCurlMessage": "请求已复制为 cURL", @@ -5069,7 +5044,6 @@ "xpack.apm.transactionActionMenu.viewInUptime": "状态", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档", "xpack.apm.transactionBreakdown.chartTitle": "跨度类型花费的时间", - "xpack.apm.transactionBreakdown.noData": "此时间范围内没有数据。", "xpack.apm.transactionCardinalityWarning.body": "唯一事务名称的数目超过 {bucketSize} 的已配置值。尝试重新配置您的代理以对类似的事务分组或增大 {codeBlock} 的值", "xpack.apm.transactionCardinalityWarning.docsLink": "在文档中了解详情", "xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。", @@ -11638,7 +11612,6 @@ "xpack.ml.jobsList.deleteJobModal.cancelButtonLabel": "取消", "xpack.ml.jobsList.deleteJobModal.closeButtonLabel": "关闭", "xpack.ml.jobsList.deleteJobModal.deleteButtonLabel": "删除", - "xpack.ml.jobsList.deleteJobModal.deleteJobsDescription": "是否确定要删除{jobsCount, plural, one {此作业} other {这些作业}}?", "xpack.ml.jobsList.deleteJobModal.deleteJobsTitle": "删除 {jobsCount, plural, one {{jobId}} other {# 个作业}}", "xpack.ml.jobsList.deleteJobModal.deleteMultipleJobsDescription": "删除{jobsCount, plural, one {一个作业} other {多个作业}}会非常耗时。将在后台删除{jobsCount, plural, one {该作业} other {这些作业}},但删除的作业可能不会立即从作业列表中消失", "xpack.ml.jobsList.deleteJobModal.deletingJobsStatusLabel": "正在删除作业", @@ -12307,7 +12280,6 @@ "xpack.ml.ruleEditor.deleteJobRule.ruleNoLongerExistsErrorMessage": "作业 {jobId} 中不再存在检测工具索引 {detectorIndex} 的规则", "xpack.ml.ruleEditor.deleteRuleModal.cancelButtonLabel": "取消", "xpack.ml.ruleEditor.deleteRuleModal.deleteButtonLabel": "删除", - "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleDescription": "是否确定要删除此规则?", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleLinkText": "删除规则", "xpack.ml.ruleEditor.deleteRuleModal.deleteRuleTitle": "删除规则", "xpack.ml.ruleEditor.detectorDescriptionList.detectorTitle": "检测工具", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx index 417a9e09086a2..b56ae35df5d06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -72,17 +72,31 @@ describe('index connector validation with minimal config', () => { describe('action params validation', () => { test('action params validation succeeds when action params is valid', () => { const actionParams = { - documents: ['test'], + documents: [{ test: 1234 }], }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: {}, + errors: { + documents: [], + }, }); const emptyActionParams = {}; expect(actionTypeModel.validateParams(emptyActionParams)).toEqual({ - errors: {}, + errors: { + documents: ['Document is required and should be a valid JSON object.'], + }, + }); + + const invalidDocumentActionParams = { + documents: [{}], + }; + + expect(actionTypeModel.validateParams(invalidDocumentActionParams)).toEqual({ + errors: { + documents: ['Document is required and should be a valid JSON object.'], + }, }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx index 3ee663a5fc8a0..c0255650e0f37 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx @@ -44,8 +44,23 @@ export function getActionType(): ActionTypeModel import('./es_index_connector')), actionParamsFields: lazy(() => import('./es_index_params')), - validateParams: (): ValidationResult => { - return { errors: {} }; + validateParams: (actionParams: IndexActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + documents: new Array(), + }; + validationResult.errors = errors; + if (!actionParams.documents?.length || Object.keys(actionParams.documents[0]).length === 0) { + errors.documents.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson', + { + defaultMessage: 'Document is required and should be a valid JSON object.', + } + ) + ); + } + return validationResult; }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index e8e8cc582512e..495707db4975c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -17,6 +17,7 @@ export const IndexParamsFields = ({ editAction, messageVariables, docLinks, + errors, }: ActionParamsProps) => { const { documents } = actionParams; @@ -24,8 +25,10 @@ export const IndexParamsFields = ({ try { const documentsJSON = JSON.parse(updatedDocuments); editAction('documents', [documentsJSON], index); - // eslint-disable-next-line no-empty - } catch (e) {} + } catch (e) { + // set document as empty to turn on the validation for non empty valid JSON object + editAction('documents', [{}], index); + } }; return ( @@ -34,7 +37,7 @@ export const IndexParamsFields = ({ messageVariables={messageVariables} paramsProperty={'documents'} inputTargetValue={ - documents && documents.length > 0 ? ((documents[0] as unknown) as string) : '' + documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined } label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', @@ -48,6 +51,7 @@ export const IndexParamsFields = ({ defaultMessage: 'Code editor', } )} + errors={errors.documents as string[]} onDocumentsChange={onDocumentsChange} helpText={ } + onBlur={() => { + if ( + !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) + ) { + // set document as empty to turn on the validation for non empty valid JSON object + onDocumentsChange('{}'); + } + }} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx index 1dfd9e3edc2c5..ff9ad936f224f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.tsx @@ -21,7 +21,7 @@ const WebhookParamsFields: React.FunctionComponent { editAction('body', json, index); }} + onBlur={() => { + if (!body) { + editAction('body', '', index); + } + }} /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx index 0b8184fc441fd..5ea15deb53161 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/json_editor_with_message_variables.tsx @@ -14,12 +14,13 @@ import { ActionVariable } from '../../types'; interface Props { messageVariables?: ActionVariable[]; paramsProperty: string; - inputTargetValue: string; + inputTargetValue?: string; label: string; errors?: string[]; areaLabel?: string; onDocumentsChange: (data: string) => void; helpText?: JSX.Element; + onBlur?: () => void; } export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ @@ -31,6 +32,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ areaLabel, onDocumentsChange, helpText, + onBlur, }) => { const [cursorPosition, setCursorPosition] = useState(null); @@ -84,6 +86,7 @@ export const JsonEditorWithMessageVariables: React.FunctionComponent = ({ onDocumentsChange(convertToJson(xjson)); }} onCursorChange={(_value: any) => onClickWithMessageVariable(_value)} + onBlur={onBlur} /> ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts index 5f2e9d07c4bcb..9fb3a54d5a182 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts @@ -10,8 +10,6 @@ import { HelloWorldEmbeddable } from '../../../../examples/embeddable_examples/p /** eslint-enable */ import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers'; -jest.mock('ui/new_platform'); - test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => { const embeddable = new TimeRangeEmbeddable( { id: '1234', timeRange: { from: 'noxw-15m', to: 'now' } }, diff --git a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts index 7919d2d2482ae..9af79c9ac5259 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts @@ -21,8 +21,6 @@ import { import { nextTick } from 'test_utils/enzyme_helpers'; import { ReactElement } from 'react'; -jest.mock('ui/new_platform'); - const createOpenModalMock = () => { const mock = jest.fn(); mock.mockReturnValue({ close: jest.fn() }); diff --git a/x-pack/plugins/watcher/public/application/lib/api.ts b/x-pack/plugins/watcher/public/application/lib/api.ts index 82ec2925ba6dc..4f42b26d51c46 100644 --- a/x-pack/plugins/watcher/public/application/lib/api.ts +++ b/x-pack/plugins/watcher/public/application/lib/api.ts @@ -35,44 +35,53 @@ export const getSavedObjectsClient = () => savedObjectsClient; const basePath = ROUTES.API_ROOT; +const loadWatchesDeserializer = ({ watches = [] }: { watches: any[] }) => { + return watches.map((watch: any) => Watch.fromUpstreamJson(watch)); +}; + export const useLoadWatches = (pollIntervalMs: number) => { return useRequest({ path: `${basePath}/watches`, method: 'get', pollIntervalMs, - deserializer: ({ watches = [] }: { watches: any[] }) => { - return watches.map((watch: any) => Watch.fromUpstreamJson(watch)); - }, + deserializer: loadWatchesDeserializer, }); }; +const loadWatchDetailDeserializer = ({ watch = {} }: { watch: any }) => + Watch.fromUpstreamJson(watch); + export const useLoadWatchDetail = (id: string) => { return useRequest({ path: `${basePath}/watch/${id}`, method: 'get', - deserializer: ({ watch = {} }: { watch: any }) => Watch.fromUpstreamJson(watch), + deserializer: loadWatchDetailDeserializer, }); }; +const loadWatchHistoryDeserializer = ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => { + return watchHistoryItems.map((historyItem: any) => + WatchHistoryItem.fromUpstreamJson(historyItem) + ); +}; + export const useLoadWatchHistory = (id: string, startTime: string) => { return useRequest({ query: startTime ? { startTime } : undefined, path: `${basePath}/watch/${id}/history`, method: 'get', - deserializer: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => { - return watchHistoryItems.map((historyItem: any) => - WatchHistoryItem.fromUpstreamJson(historyItem) - ); - }, + deserializer: loadWatchHistoryDeserializer, }); }; +const loadWatchHistoryDetailDeserializer = ({ watchHistoryItem }: { watchHistoryItem: any }) => + WatchHistoryItem.fromUpstreamJson(watchHistoryItem); + export const useLoadWatchHistoryDetail = (id: string | undefined) => { return useRequest({ path: !id ? '' : `${basePath}/history/${id}`, method: 'get', - deserializer: ({ watchHistoryItem }: { watchHistoryItem: any }) => - WatchHistoryItem.fromUpstreamJson(watchHistoryItem), + deserializer: loadWatchHistoryDetailDeserializer, }); }; @@ -148,6 +157,8 @@ export const loadIndexPatterns = async () => { return savedObjects; }; +const getWatchVisualizationDataDeserializer = (data: { visualizeData: any }) => data?.visualizeData; + export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => { return useRequest({ path: `${basePath}/watch/visualize`, @@ -156,21 +167,23 @@ export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOpt watch: watchModel.upstreamJson, options: visualizeOptions.upstreamJson, }), - deserializer: (data: { visualizeData: any }) => data?.visualizeData, + deserializer: getWatchVisualizationDataDeserializer, }); }; +const loadSettingsDeserializer = (data: { + action_types: { + [key: string]: { + enabled: boolean; + }; + }; +}) => Settings.fromUpstreamJson(data); + export const useLoadSettings = () => { return useRequest({ path: `${basePath}/settings`, method: 'get', - deserializer: (data: { - action_types: { - [key: string]: { - enabled: boolean; - }; - }; - }) => Settings.fromUpstreamJson(data), + deserializer: loadSettingsDeserializer, }); }; diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index c0d906114277e..2ff0f53d07e91 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useContext, useEffect } from 'react'; +import React, { Fragment, useContext, useEffect, useMemo } from 'react'; import { AnnotationDomainTypes, Axis, @@ -105,7 +105,9 @@ export const WatchVisualization = () => { threshold, } = watch; - const domain = getDomain(watch); + // Only recalculate the domain if the watch configuration changes. This prevents the visualization + // request's resolution from re-triggering itself in an infinite loop. + const domain = useMemo(() => getDomain(watch), [watch]); const timeBuckets = createTimeBuckets(); timeBuckets.setBounds(domain); const interval = timeBuckets.getInterval().expression; diff --git a/x-pack/plugins/watcher/public/application/shared_imports.ts b/x-pack/plugins/watcher/public/application/shared_imports.ts index a9e07b80a9b22..766e8e659c8ae 100644 --- a/x-pack/plugins/watcher/public/application/shared_imports.ts +++ b/x-pack/plugins/watcher/public/application/shared_imports.ts @@ -10,6 +10,6 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/'; +} from '../../../../../src/plugins/es_ui_shared/public'; export { useXJsonMode } from '../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 421e709444d0f..6271c4b601307 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -56,7 +56,6 @@ const onlyNotInCoverageTests = [ require.resolve('../test/upgrade_assistant_integration/config.js'), require.resolve('../test/licensing_plugin/config.ts'), require.resolve('../test/licensing_plugin/config.public.ts'), - require.resolve('../test/licensing_plugin/config.legacy.ts'), require.resolve('../test/endpoint_api_integration_no_ingest/config.ts'), require.resolve('../test/reporting_api_integration/reporting_and_security.config.ts'), require.resolve('../test/reporting_api_integration/reporting_without_security.config.ts'), diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 67dd8c877e378..f9fdfaed1c79b 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -63,7 +63,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) const actionsProxyUrl = options.enableActionsProxy ? [ `--xpack.actions.proxyUrl=http://localhost:${proxyPort}`, - '--xpack.actions.rejectUnauthorizedCertificates=false', + '--xpack.actions.proxyRejectUnauthorizedCertificates=false', ] : []; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts index 5b61112a374c1..0b94abaa15890 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import expectedBreakdown from './expectation/breakdown.json'; -import expectedBreakdownWithTransactionName from './expectation/breakdown_transaction_name.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -25,7 +24,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ kpis: [], timeseries: [] }); + expect(response.body).to.eql({ timeseries: [] }); }); }); @@ -47,15 +46,32 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedBreakdownWithTransactionName); + const { timeseries } = response.body; + const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; + expect(data).to.eql([ + { x: 1593413100000, y: null }, + { x: 1593413130000, y: null }, + { x: 1593413160000, y: null }, + { x: 1593413190000, y: null }, + { x: 1593413220000, y: null }, + { x: 1593413250000, y: null }, + { x: 1593413280000, y: null }, + { x: 1593413310000, y: 1 }, + { x: 1593413340000, y: null }, + ]); + expect(title).to.be('app'); + expect(color).to.be('#54b399'); + expect(type).to.be('areaStacked'); + expect(hideLegend).to.be(false); + expect(legendValue).to.be('100%'); }); - it('returns the top 4 by percentage and sorts them by name', async () => { + it('returns the transaction breakdown sorted by name', async () => { const response = await supertest.get( `/api/apm/services/opbeans-node/transaction_groups/breakdown?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionType=${transactionType}` ); expect(response.status).to.be(200); - expect(response.body.kpis.map((kpi: { name: string }) => kpi.name)).to.eql([ + expect(response.body.timeseries.map((serie: { title: string }) => serie.title)).to.eql([ 'app', 'http', 'postgresql', diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json index 3b884a9eb7907..8ffbba64ec7ab 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json @@ -1,26 +1,4 @@ { - "kpis":[ - { - "name":"app", - "percentage":0.16700861715223636, - "color":"#54b399" - }, - { - "name":"http", - "percentage":0.7702092736971686, - "color":"#6092c0" - }, - { - "name":"postgresql", - "percentage":0.0508822322527698, - "color":"#d36086" - }, - { - "name":"redis", - "percentage":0.011899876897825195, - "color":"#9170b8" - } - ], "timeseries":[ { "title":"app", @@ -64,7 +42,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "17%" }, { "title":"http", @@ -108,7 +87,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "77%" }, { "title":"postgresql", @@ -152,7 +132,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "5.1%" }, { "title":"redis", @@ -196,7 +177,8 @@ "y":null } ], - "hideLegend":true + "hideLegend":false, + "legendValue": "1.2%" } ] } \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json deleted file mode 100644 index b4f8e376d3609..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown_transaction_name.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "kpis":[ - { - "name":"app", - "percentage":1, - "color":"#54b399" - } - ], - "timeseries":[ - { - "title":"app", - "color":"#54b399", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":1 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":true - } - ] -} \ No newline at end of file diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index a022b7c79c079..c682c1f1f4640 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -49,7 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllTimelines(es); }); - it('should contain two output keys of rules_installed and rules_updated', async () => { + it('should contain rules_installed, rules_updated, timelines_installed, and timelines_updated', async () => { const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') @@ -74,6 +74,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_installed).to.be.greaterThan(0); }); + it('should create the prepackaged timelines and return a count greater than zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.be.greaterThan(0); + }); + it('should create the prepackaged rules that the rules_updated is of size zero', async () => { const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) @@ -84,6 +94,16 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_updated).to.eql(0); }); + it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_updated).to.eql(0); + }); + it('should be possible to call the API twice and the second time the number of rules installed should be zero', async () => { await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) @@ -109,6 +129,30 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.rules_installed).to.eql(0); }); + + it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.timelines_not_installed === 0; + }); + + const { body } = await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.eql(0); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts new file mode 100644 index 0000000000000..209c7974a7c2a --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_timelines.ts @@ -0,0 +1,403 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { TIMELINE_IMPORT_URL } from '../../../../plugins/security_solution/common/constants'; + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { deleteAllTimelines } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('import timelines', () => { + describe('creating a timeline', () => { + const getTimeline = () => { + return Buffer.from( + JSON.stringify({ + savedObjectId: '67664480-d191-11ea-ae67-4f4be8c1847b', + version: 'WzU1NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'default', + kqlQuery: { + filterQuery: { + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"@timestamp"}}],"minimum_should_match":1}}', + kuery: { + expression: '@timestamp : * ', + kind: 'kuery', + }, + }, + }, + title: 'x2', + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + created: 1596036895488, + createdBy: 'angela', + updated: 1596491470411, + updatedBy: 'elastic', + templateTimelineId: null, + templateTimelineVersion: null, + dateRange: { + start: '2020-04-10T14:10:58.373Z', + end: '2020-05-30T14:16:58.373Z', + }, + savedQueryId: null, + eventNotes: [ + { + noteId: '7d875ba0-d5d3-11ea-9899-ebec3d084fe0', + version: 'WzU1NiwxXQ==', + eventId: '8KtMKnIBOS_moQ_K9fAe', + note: 'hi Xavier', + timelineId: '67664480-d191-11ea-ae67-4f4be8c1847b', + created: 1596491490806, + createdBy: 'elastic', + updated: 1596491490806, + updatedBy: 'elastic', + }, + ], + globalNotes: [], + pinnedEventIds: [ + 'K99zy3EBDTDlbwBfpf6x', + 'GKpFKnIBOS_moQ_Ke5AO', + '8KtMKnIBOS_moQ_K9fAe', + ], + }) + ); + }; + beforeEach(async () => {}); + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimeline(), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + }); + + describe('creating a timeline template', () => { + const getTimelineTemplate = () => { + return Buffer.from( + JSON.stringify({ + savedObjectId: 'cab434d0-d26c-11ea-b887-3b103296472a', + version: 'WzQ0NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'desc', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'template', + kqlQuery: { filterQuery: null }, + title: 'my t template', + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + templateTimelineId: '46a50505-0a48-49cb-9ab2-d15d683efa3b', + templateTimelineVersion: 1, + created: 1596473742379, + createdBy: 'elastic', + updated: 1596473909169, + updatedBy: 'elastic', + dateRange: { start: '2020-08-02T16:55:22.160Z', end: '2020-08-03T16:55:22.161Z' }, + savedQueryId: null, + eventNotes: [], + globalNotes: [ + { + noteId: '358f45c0-d5aa-11ea-9b6d-53d136d390dc', + version: 'WzQzOCwxXQ==', + note: 'I have a global note', + timelineId: 'cab434d0-d26c-11ea-b887-3b103296472a', + created: 1596473760688, + createdBy: 'elastic', + updated: 1596473760688, + updatedBy: 'elastic', + }, + ], + pinnedEventIds: [], + }) + ); + }; + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 1, + timelines_updated: 0, + }); + }); + }); + + describe('Updating a timeline template', () => { + const getTimelineTemplate = (templateTimelineVersion: number) => { + return Buffer.from( + JSON.stringify({ + savedObjectId: 'cab434d0-d26c-11ea-b887-3b103296472a', + version: 'WzQ0NSwxXQ==', + columns: [ + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: '@timestamp', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'message', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.category', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'event.action', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'host.name', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'source.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'destination.ip', + searchable: null, + }, + { + indexes: null, + name: null, + columnHeaderType: 'not-filtered', + id: 'user.name', + searchable: null, + }, + ], + dataProviders: [], + description: 'desc', + eventType: 'all', + filters: [], + kqlMode: 'filter', + timelineType: 'template', + kqlQuery: { filterQuery: null }, + title: 'my t template', + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + templateTimelineId: '46a50505-0a48-49cb-9ab2-d15d683efa3b', + templateTimelineVersion, + created: 1596473742379, + createdBy: 'elastic', + updated: 1596473909169, + updatedBy: 'elastic', + dateRange: { start: '2020-08-02T16:55:22.160Z', end: '2020-08-03T16:55:22.161Z' }, + savedQueryId: null, + eventNotes: [], + globalNotes: [ + { + noteId: '358f45c0-d5aa-11ea-9b6d-53d136d390dc', + version: 'WzQzOCwxXQ==', + note: 'I have a global note', + timelineId: 'cab434d0-d26c-11ea-b887-3b103296472a', + created: 1596473760688, + createdBy: 'elastic', + updated: 1596473760688, + updatedBy: 'elastic', + }, + ], + pinnedEventIds: [], + }) + ); + }; + + afterEach(async () => { + await deleteAllTimelines(es); + }); + + it("if it doesn't exists", async () => { + await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(1), 'timelines.ndjson') + .expect(200); + const { body } = await supertest + .post(`${TIMELINE_IMPORT_URL}`) + .set('kbn-xsrf', 'true') + .attach('file', getTimelineTemplate(2), 'timelines.ndjson') + .expect(200); + expect(body).to.eql({ + errors: [], + success: true, + success_count: 1, + timelines_installed: 0, + timelines_updated: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index a480e63ff4a92..09feb7ac5e0ec 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -28,5 +28,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./patch_rules')); loadTestFile(require.resolve('./query_signals')); loadTestFile(require.resolve('./open_close_signals')); + loadTestFile(require.resolve('./import_timelines')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts new file mode 100644 index 0000000000000..556217877968b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/basic/tests/install_prepackaged_timelines.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { TIMELINE_PREPACKAGED_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteAllTimelines, + deleteSignalsIndex, + waitFor, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('install_prepackaged_timelines', () => { + describe('creating prepackaged rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllTimelines(es); + }); + + it('should contain timelines_installed, and timelines_updated', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(Object.keys(body)).to.eql(['timelines_installed', 'timelines_updated']); + }); + + it('should create the prepackaged timelines and return a count greater than zero', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.be.greaterThan(0); + }); + + it('should create the prepackaged timelines and the timelines_updated is of size zero', async () => { + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_updated).to.eql(0); + }); + + it('should be possible to call the API twice and the second time the number of timelines installed should be zero', async () => { + await supertest.put(TIMELINE_PREPACKAGED_URL).set('kbn-xsrf', 'true').send().expect(200); + + await waitFor(async () => { + const { body } = await supertest + .get(`${TIMELINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.timelines_not_installed === 0; + }); + + const { body } = await supertest + .put(TIMELINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.timelines_installed).to.eql(0); + }); + }); + }); +}; diff --git a/x-pack/test/licensing_plugin/config.legacy.ts b/x-pack/test/licensing_plugin/config.legacy.ts deleted file mode 100644 index 14eadc3194f9e..0000000000000 --- a/x-pack/test/licensing_plugin/config.legacy.ts +++ /dev/null @@ -1,15 +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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; - -export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const commonConfig = await readConfigFile(require.resolve('./config')); - - return { - ...commonConfig.getAll(), - testFiles: [require.resolve('./legacy')], - }; -} diff --git a/x-pack/test/licensing_plugin/legacy/index.ts b/x-pack/test/licensing_plugin/legacy/index.ts deleted file mode 100644 index 6274bd3969042..0000000000000 --- a/x-pack/test/licensing_plugin/legacy/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FtrProviderContext } from '../services'; - -// eslint-disable-next-line import/no-default-export -export default function ({ loadTestFile }: FtrProviderContext) { - describe('Legacy licensing plugin', function () { - this.tags('ciGroup2'); - // MUST BE LAST! CHANGES LICENSE TYPE! - loadTestFile(require.resolve('./updates')); - }); -} diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts deleted file mode 100644 index 1de8659672d2f..0000000000000 --- a/x-pack/test/licensing_plugin/legacy/updates.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../services'; -import { createScenario } from '../scenario'; -import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; - -// eslint-disable-next-line import/no-default-export -export default function (ftrContext: FtrProviderContext) { - const { getService } = ftrContext; - const supertest = getService('supertest'); - const testSubjects = getService('testSubjects'); - - const scenario = createScenario(ftrContext); - - describe('changes in license types', () => { - after(async () => { - await scenario.teardown(); - }); - - it('provides changes in license types', async () => { - await scenario.setup(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { - body: legacyInitialLicense, - header: legacyInitialLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyInitialLicense.license?.type).to.be('basic'); - expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); - - await scenario.startTrial(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { body: legacyTrialLicense, header: legacyTrialLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - - expect(legacyTrialLicense.license?.type).to.be('trial'); - expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.startBasic(); - await scenario.waitForPluginToDetectLicenseUpdate(); - - const { body: legacyBasicLicense } = await supertest.get('/api/xpack/v1/info').expect(200); - expect(legacyBasicLicense.license?.type).to.be('basic'); - - // banner shown only when license expired not just deleted - await testSubjects.missingOrFail('licenseExpiredBanner'); - }); - }); -} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 3e04a507d3810..a0998f1a838ba 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -142,6 +142,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { relative_url: '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-trustlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-trustlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/endpoint/artifacts/download/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, }, // The manifest version could have changed when the Policy was updated because the // policy details page ensures that a save action applies the udpated policy on top diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts index 7f831973aea5c..e567828598b31 100644 --- a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import axios, { AxiosInstance } from 'axios'; -import { UICapabilities } from 'ui/capabilities'; +import type { Capabilities as UICapabilities } from 'src/core/types'; import { format as formatUrl } from 'url'; import util from 'util'; import { ToolingLog } from '@kbn/dev-utils'; diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index 7bb073023b7f8..c94fe02d2f4bd 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -17,7 +17,6 @@ export default { ], collectCoverageFrom: ['legacy/plugins/**/*.js', 'legacy/common/**/*.js', 'legacy/server/**/*.js'], moduleNameMapper: { - '^ui/(.*)': '**/public/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/../src/dev/jest/mocks/style_mock.js', diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index e978702a35634..35e1800c6fbd1 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -22,9 +22,6 @@ "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "ui/*": [ - "src/legacy/ui/public/*" - ], "plugins/xpack_main/*": [ "x-pack/legacy/plugins/xpack_main/public/*" ], diff --git a/yarn.lock b/yarn.lock index bd02196e48796..c0c2305609f58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12673,18 +12673,7 @@ extract-zip@1.7.0, extract-zip@^1.6.6, extract-zip@^1.7.0: mkdirp "^0.5.4" yauzl "^2.10.0" -extract-zip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.0.tgz#f53b71d44f4ff5a4527a2259ade000fb8b303492" - integrity sha512-i42GQ498yibjdvIhivUsRslx608whtGoFIhF26Z7O4MYncBxp8CwalOs1lnHy21A9sIohWO2+uiE4SRtC9JXDg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extract-zip@^2.0.1: +extract-zip@^2.0.0, extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==