diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc
index aba65f2e921c2..e58d9c39ee8c4 100644
--- a/docs/api/using-api.asciidoc
+++ b/docs/api/using-api.asciidoc
@@ -10,7 +10,7 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte
[float]
[[api-authentication]]
=== Authentication
-{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console.
+{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, which is where the username and password are stored in order to be passed as part of the call.
[float]
[[api-calls]]
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md
new file mode 100644
index 0000000000000..3f2d81cc97c7c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md)
+
+## SavedObjectsComplexFieldMapping.doc\_values property
+
+Signature:
+
+```typescript
+doc_values?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
index a7d13b0015e3f..cb81686b424ec 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
@@ -18,6 +18,7 @@ export interface SavedObjectsComplexFieldMapping
| Property | Type | Description |
| --- | --- | --- |
+| [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md) | boolean
| |
| [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties
| |
| [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md
new file mode 100644
index 0000000000000..2a79eafd85a6c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md)
+
+## SavedObjectsCoreFieldMapping.doc\_values property
+
+Signature:
+
+```typescript
+doc_values?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
index 9a31d37b3ff30..b9e726eac799d 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
@@ -16,6 +16,7 @@ export interface SavedObjectsCoreFieldMapping
| Property | Type | Description |
| --- | --- | --- |
+| [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md) | boolean
| |
| [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean
| |
| [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
ignore_above?: number;
};
}
| |
| [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md
index 1e0e89767c4e6..c839dd16d9a47 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md
@@ -4,7 +4,9 @@
## SavedObjectTypeRegistry.getAllTypes() method
-Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered.
+Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones.
+
+To only get the visible types (which is the most common use case), use `getVisibleTypes` instead.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md
new file mode 100644
index 0000000000000..a773c6a0a674f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [getVisibleTypes](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md)
+
+## SavedObjectTypeRegistry.getVisibleTypes() method
+
+Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md).
+
+A visible type is a type that doesn't explicitly define `hidden=true` during registration.
+
+Signature:
+
+```typescript
+getVisibleTypes(): SavedObjectsType[];
+```
+Returns:
+
+`SavedObjectsType[]`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
index 69a94e4ad8c88..55ad7ca137de0 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
@@ -16,10 +16,11 @@ export declare class SavedObjectTypeRegistry
| Method | Modifiers | Description |
| --- | --- | --- |
-| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered. |
+| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones.To only get the visible types (which is the most common use case), use getVisibleTypes
instead. |
| [getImportableAndExportableTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered that are importable/exportable. |
| [getIndex(type)](./kibana-plugin-core-server.savedobjecttyperegistry.getindex.md) | | Returns the indexPattern
property for given type, or undefined
if the type is not registered. |
| [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. |
+| [getVisibleTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md) | | Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md).A visible type is a type that doesn't explicitly define hidden=true
during registration. |
| [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the hidden
property for given type, or false
if the type is not registered. |
| [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable
property for given type, or false
if the type is not registered or does not define a management section. |
| [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to false
if the type is not registered |
diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc
index cd07596ad37ef..8fc2b7381de83 100644
--- a/docs/plugins/known-plugins.asciidoc
+++ b/docs/plugins/known-plugins.asciidoc
@@ -59,6 +59,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea
* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn)
* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek)
* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity.
+* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan)
[float]
=== Other
diff --git a/package.json b/package.json
index b520be4df6969..b1dd8686f818b 100644
--- a/package.json
+++ b/package.json
@@ -122,7 +122,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
"@babel/register": "^7.10.1",
"@elastic/apm-rum": "^5.2.0",
- "@elastic/charts": "19.5.2",
+ "@elastic/charts": "19.6.3",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.9.3",
"@elastic/eui": "24.1.0",
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 0e3bb235c3d9f..ff09d8d4fc5ab 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -9,7 +9,7 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
- "@elastic/charts": "19.5.2",
+ "@elastic/charts": "19.6.3",
"@elastic/eui": "24.1.0",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts
index c037ed733549e..7521e4a4bee86 100644
--- a/src/core/server/saved_objects/mappings/types.ts
+++ b/src/core/server/saved_objects/mappings/types.ts
@@ -133,6 +133,7 @@ export interface SavedObjectsCoreFieldMapping {
type: string;
null_value?: number | boolean | string;
index?: boolean;
+ doc_values?: boolean;
enabled?: boolean;
fields?: {
[subfield: string]: {
@@ -153,6 +154,7 @@ export interface SavedObjectsCoreFieldMapping {
* @public
*/
export interface SavedObjectsComplexFieldMapping {
+ doc_values?: boolean;
type?: string;
properties: SavedObjectsMappingProperties;
}
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
index 5636dcadb444e..44490228490cc 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
@@ -25,6 +25,7 @@ const createRegistryMock = (): jest.Mocked<
const mock = {
registerType: jest.fn(),
getType: jest.fn(),
+ getVisibleTypes: jest.fn(),
getAllTypes: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
isNamespaceAgnostic: jest.fn(),
@@ -35,6 +36,7 @@ const createRegistryMock = (): jest.Mocked<
isImportableAndExportable: jest.fn(),
};
+ mock.getVisibleTypes.mockReturnValue([]);
mock.getAllTypes.mockReturnValue([]);
mock.getImportableAndExportableTypes.mockReturnValue([]);
mock.getIndex.mockReturnValue('.kibana-test');
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
index e0f4d6fa28e50..25c94324c8f01 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
@@ -99,10 +99,37 @@ describe('SavedObjectTypeRegistry', () => {
});
});
+ describe('#getVisibleTypes', () => {
+ it('returns only visible registered types', () => {
+ const typeA = createType({ name: 'typeA', hidden: false });
+ const typeB = createType({ name: 'typeB', hidden: true });
+ const typeC = createType({ name: 'typeC', hidden: false });
+ registry.registerType(typeA);
+ registry.registerType(typeB);
+ registry.registerType(typeC);
+
+ const registered = registry.getVisibleTypes();
+ expect(registered.length).toEqual(2);
+ expect(registered).toContainEqual(typeA);
+ expect(registered).toContainEqual(typeC);
+ });
+
+ it('does not mutate the registered types when altering the list', () => {
+ registry.registerType(createType({ name: 'typeA', hidden: false }));
+ registry.registerType(createType({ name: 'typeB', hidden: true }));
+ registry.registerType(createType({ name: 'typeC', hidden: false }));
+
+ const types = registry.getVisibleTypes();
+ types.splice(0, 2);
+
+ expect(registry.getVisibleTypes().length).toEqual(2);
+ });
+ });
+
describe('#getAllTypes', () => {
it('returns all registered types', () => {
const typeA = createType({ name: 'typeA' });
- const typeB = createType({ name: 'typeB' });
+ const typeB = createType({ name: 'typeB', hidden: true });
const typeC = createType({ name: 'typeC' });
registry.registerType(typeA);
registry.registerType(typeB);
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts
index 99262d7a31e21..d0035294226ea 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.ts
@@ -54,7 +54,18 @@ export class SavedObjectTypeRegistry {
}
/**
- * Return all {@link SavedObjectsType | types} currently registered.
+ * Returns all visible {@link SavedObjectsType | types}.
+ *
+ * A visible type is a type that doesn't explicitly define `hidden=true` during registration.
+ */
+ public getVisibleTypes() {
+ return [...this.types.values()].filter((type) => !this.isHidden(type.name));
+ }
+
+ /**
+ * Return all {@link SavedObjectsType | types} currently registered, including the hidden ones.
+ *
+ * To only get the visible types (which is the most common use case), use `getVisibleTypes` instead.
*/
public getAllTypes() {
return [...this.types.values()];
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9cc5a8a386b0b..cb413be2c19b8 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1978,6 +1978,8 @@ export interface SavedObjectsClientWrapperOptions {
// @public
export interface SavedObjectsComplexFieldMapping {
+ // (undocumented)
+ doc_values?: boolean;
// (undocumented)
properties: SavedObjectsMappingProperties;
// (undocumented)
@@ -1986,6 +1988,8 @@ export interface SavedObjectsComplexFieldMapping {
// @public
export interface SavedObjectsCoreFieldMapping {
+ // (undocumented)
+ doc_values?: boolean;
// (undocumented)
enabled?: boolean;
// (undocumented)
@@ -2468,6 +2472,7 @@ export class SavedObjectTypeRegistry {
getImportableAndExportableTypes(): SavedObjectsType[];
getIndex(type: string): string | undefined;
getType(type: string): SavedObjectsType | undefined;
+ getVisibleTypes(): SavedObjectsType[];
isHidden(type: string): boolean;
isImportableAndExportable(type: string): boolean;
isMultiNamespace(type: string): boolean;
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js
index 63839b9d0f1d7..185c8807ae8b5 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.js
@@ -34,8 +34,8 @@ export function savedObjectsMixin(kbnServer, server) {
const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry();
const mappings = migrator.getActiveMappings();
const allTypes = typeRegistry.getAllTypes().map((t) => t.name);
+ const visibleTypes = typeRegistry.getVisibleTypes().map((t) => t.name);
const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes()));
- const visibleTypes = allTypes.filter((type) => !schema.isHiddenType(type));
server.decorate('server', 'kibanaMigrator', migrator);
diff --git a/src/plugins/charts/README.md b/src/plugins/charts/README.md
index 319da67981aa9..31727b7acb7a1 100644
--- a/src/plugins/charts/README.md
+++ b/src/plugins/charts/README.md
@@ -18,7 +18,7 @@ Color mappings in `value`/`text` form
### `getHeatmapColors`
-Funciton to retrive heatmap related colors based on `value` and `colorSchemaName`
+Function to retrieve heatmap related colors based on `value` and `colorSchemaName`
### `truncatedColorSchemas`
@@ -26,72 +26,4 @@ Truncated color mappings in `value`/`text` form
## Theme
-the `theme` service offers utilities to interact with theme of kibana. EUI provides a light and dark theme object to work with Elastic-Charts. However, every instance of a Chart would need to pass down this the correctly EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct theme.
-
-> The current theme (light or dark) of Kibana is typically taken into account for the functions below.
-
-### `useChartsTheme`
-
-The simple fetching of the correct EUI theme; a **React hook**.
-
-```js
-import { npStart } from 'ui/new_platform';
-import { Chart, Settings } from '@elastic/charts';
-
-export const YourComponent = () => (
-
-
-
-);
-```
-
-### `chartsTheme$`
-
-An **observable** of the current charts theme. Use this implementation for more flexible updates to the chart theme without full page refreshes.
-
-```tsx
-import { npStart } from 'ui/new_platform';
-import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes';
-import { Subscription } from 'rxjs';
-import { Chart, Settings } from '@elastic/charts';
-
-interface YourComponentProps {};
-
-interface YourComponentState {
- chartsTheme: EuiChartThemeType['theme'];
-}
-
-export class YourComponent extends Component {
- private subscription?: Subscription;
- public state = {
- chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme,
- };
-
- componentDidMount() {
- this.subscription = npStart.plugins.charts.theme
- .chartsTheme$
- .subscribe(chartsTheme => this.setState({ chartsTheme }));
- }
-
- componentWillUnmount() {
- if (this.subscription) {
- this.subscription.unsubscribe();
- this.subscription = undefined;
- }
- }
-
- public render() {
- const { chartsTheme } = this.state;
-
- return (
-
-
-
- );
- }
-}
-```
-
-### `chartsDefaultTheme`
-
-Returns default charts theme (i.e. light).
+See Theme service [docs](public/services/theme/README.md)
diff --git a/src/plugins/charts/public/services/theme/README.md b/src/plugins/charts/public/services/theme/README.md
new file mode 100644
index 0000000000000..fb4f941f79344
--- /dev/null
+++ b/src/plugins/charts/public/services/theme/README.md
@@ -0,0 +1,92 @@
+# Theme Service
+
+The `theme` service offers utilities to interact with the kibana theme. EUI provides a light and dark theme object to supplement the Elastic-Charts `baseTheme`. However, every instance of a Chart would need to pass down the correct EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct shared `theme` and `baseTheme`.
+
+> The current theme (light or dark) of Kibana is typically taken into account for the functions below.
+
+## `chartsDefaultBaseTheme`
+
+Default `baseTheme` from `@elastic/charts` (i.e. light).
+
+## `chartsDefaultTheme`
+
+Default `theme` from `@elastic/eui` (i.e. light).
+
+## `useChartsTheme` and `useChartsBaseTheme`
+
+A **React hook** for simple fetching of the correct EUI `theme` and `baseTheme`.
+
+```js
+import { npStart } from 'ui/new_platform';
+import { Chart, Settings } from '@elastic/charts';
+
+export const YourComponent = () => (
+
+
+ {/* ... */}
+
+);
+```
+
+## `chartsTheme$` and `chartsBaseTheme$`
+
+An **`Observable`** of the current charts `theme` and `baseTheme`. Use this implementation for more flexible updates to the chart theme without full page refreshes.
+
+```tsx
+import { npStart } from 'ui/new_platform';
+import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes';
+import { Subscription, combineLatest } from 'rxjs';
+import { Chart, Settings, Theme } from '@elastic/charts';
+
+interface YourComponentProps {};
+
+interface YourComponentState {
+ chartsTheme: EuiChartThemeType['theme'];
+ chartsBaseTheme: Theme;
+}
+
+export class YourComponent extends Component {
+ private subscriptions: Subscription[] = [];
+
+ public state = {
+ chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme,
+ chartsBaseTheme: npStart.plugins.charts.theme.chartsDefaultBaseTheme,
+ };
+
+ componentDidMount() {
+ this.subscription = combineLatest(
+ npStart.plugins.charts.theme.chartsTheme$,
+ npStart.plugins.charts.theme.chartsBaseTheme$
+ ).subscribe(([chartsTheme, chartsBaseTheme]) =>
+ this.setState({ chartsTheme, chartsBaseTheme })
+ );
+ }
+
+ componentWillUnmount() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ public render() {
+ const { chartsBaseTheme, chartsTheme } = this.state;
+
+ return (
+
+
+ {/* ... */}
+
+ );
+ }
+}
+```
+
+## Why have `theme` and `baseTheme`?
+
+The `theme` prop is a recursive partial `Theme` that overrides properties from the `baseTheme`. This allows changes to the `Theme` TS type in `@elastic/charts` without having to update the `@elastic/eui` themes for every ``.
diff --git a/src/plugins/charts/public/services/theme/mock.ts b/src/plugins/charts/public/services/theme/mock.ts
index 8aa1a4f2368ac..7fecb862a3c65 100644
--- a/src/plugins/charts/public/services/theme/mock.ts
+++ b/src/plugins/charts/public/services/theme/mock.ts
@@ -21,9 +21,17 @@ import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { ThemeService } from './theme';
export const themeServiceMock: ThemeService = {
+ chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme,
chartsTheme$: jest.fn(() => ({
- subsribe: jest.fn(),
+ subscribe: jest.fn(),
})),
- chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme,
- useChartsTheme: jest.fn(),
+ chartsBaseTheme$: jest.fn(() => ({
+ subscribe: jest.fn(),
+ })),
+ darkModeEnabled$: jest.fn(() => ({
+ subscribe: jest.fn(),
+ })),
+ useDarkMode: jest.fn().mockReturnValue(false),
+ useChartsTheme: jest.fn().mockReturnValue({}),
+ useChartsBaseTheme: jest.fn().mockReturnValue({}),
} as any;
diff --git a/src/plugins/charts/public/services/theme/theme.test.tsx b/src/plugins/charts/public/services/theme/theme.test.tsx
index fca503e387ea2..52bc78dfec7df 100644
--- a/src/plugins/charts/public/services/theme/theme.test.tsx
+++ b/src/plugins/charts/public/services/theme/theme.test.tsx
@@ -25,15 +25,35 @@ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist
import { ThemeService } from './theme';
import { coreMock } from '../../../../../core/public/mocks';
+import { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
const { uiSettings: setupMockUiSettings } = coreMock.createSetup();
describe('ThemeService', () => {
- describe('chartsTheme$', () => {
+ describe('darkModeEnabled$', () => {
it('should throw error if service has not been initialized', () => {
const themeService = new ThemeService();
- expect(() => themeService.chartsTheme$).toThrowError();
+ expect(() => themeService.darkModeEnabled$).toThrowError();
+ });
+
+ it('returns the false when not in dark mode', async () => {
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+
+ expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toBe(false);
+ });
+
+ it('returns the true when in dark mode', async () => {
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+
+ expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toBe(true);
});
+ });
+
+ describe('chartsTheme$', () => {
it('returns the light theme when not in dark mode', async () => {
setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
const themeService = new ThemeService();
@@ -58,6 +78,28 @@ describe('ThemeService', () => {
});
});
+ describe('chartsBaseTheme$', () => {
+ it('returns the light theme when not in dark mode', async () => {
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+
+ expect(await themeService.chartsBaseTheme$.pipe(take(1)).toPromise()).toEqual(LIGHT_THEME);
+ });
+
+ describe('in dark mode', () => {
+ it(`returns the dark theme`, async () => {
+ // Fake dark theme turned returning true
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+ const result = await themeService.chartsBaseTheme$.pipe(take(1)).toPromise();
+
+ expect(result).toEqual(DARK_THEME);
+ });
+ });
+ });
+
describe('useChartsTheme', () => {
it('updates when the uiSettings change', () => {
const darkMode$ = new BehaviorSubject(false);
@@ -75,4 +117,22 @@ describe('ThemeService', () => {
expect(result.current).toBe(EUI_CHARTS_THEME_LIGHT.theme);
});
});
+
+ describe('useBaseChartTheme', () => {
+ it('updates when the uiSettings change', () => {
+ const darkMode$ = new BehaviorSubject(false);
+ setupMockUiSettings.get$.mockReturnValue(darkMode$);
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+ const { useChartsBaseTheme } = themeService;
+
+ const { result } = renderHook(() => useChartsBaseTheme());
+ expect(result.current).toBe(LIGHT_THEME);
+
+ act(() => darkMode$.next(true));
+ expect(result.current).toBe(DARK_THEME);
+ act(() => darkMode$.next(false));
+ expect(result.current).toBe(LIGHT_THEME);
+ });
+ });
});
diff --git a/src/plugins/charts/public/services/theme/theme.ts b/src/plugins/charts/public/services/theme/theme.ts
index e1e71573caa3a..2d0c4de883218 100644
--- a/src/plugins/charts/public/services/theme/theme.ts
+++ b/src/plugins/charts/public/services/theme/theme.ts
@@ -18,34 +18,56 @@
*/
import { useEffect, useState } from 'react';
-import { map } from 'rxjs/operators';
-import { Observable } from 'rxjs';
+import { Observable, BehaviorSubject } from 'rxjs';
import { CoreSetup } from 'kibana/public';
-import { RecursivePartial, Theme } from '@elastic/charts';
+import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
export class ThemeService {
- private _chartsTheme$?: Observable>;
-
/** Returns default charts theme */
public readonly chartsDefaultTheme = EUI_CHARTS_THEME_LIGHT.theme;
+ public readonly chartsDefaultBaseTheme = LIGHT_THEME;
+
+ private _uiSettingsDarkMode$?: Observable;
+ private _chartsTheme$ = new BehaviorSubject(this.chartsDefaultTheme);
+ private _chartsBaseTheme$ = new BehaviorSubject(this.chartsDefaultBaseTheme);
/** An observable of the current charts theme */
- public get chartsTheme$(): Observable> {
- if (!this._chartsTheme$) {
+ public chartsTheme$ = this._chartsTheme$.asObservable();
+
+ /** An observable of the current charts base theme */
+ public chartsBaseTheme$ = this._chartsBaseTheme$.asObservable();
+
+ /** An observable boolean for dark mode of kibana */
+ public get darkModeEnabled$(): Observable {
+ if (!this._uiSettingsDarkMode$) {
throw new Error('ThemeService not initialized');
}
- return this._chartsTheme$;
+ return this._uiSettingsDarkMode$;
}
+ /** A React hook for consuming the dark mode value */
+ public useDarkMode = (): boolean => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [value, update] = useState(false);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ const s = this.darkModeEnabled$.subscribe(update);
+ return () => s.unsubscribe();
+ }, []);
+
+ return value;
+ };
+
/** A React hook for consuming the charts theme */
- public useChartsTheme = () => {
- /* eslint-disable-next-line react-hooks/rules-of-hooks */
+ public useChartsTheme = (): PartialTheme => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
const [value, update] = useState(this.chartsDefaultTheme);
- /* eslint-disable-next-line react-hooks/rules-of-hooks */
+ // eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
const s = this.chartsTheme$.subscribe(update);
return () => s.unsubscribe();
@@ -54,12 +76,28 @@ export class ThemeService {
return value;
};
+ /** A React hook for consuming the charts theme */
+ public useChartsBaseTheme = (): Theme => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [value, update] = useState(this.chartsDefaultBaseTheme);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ const s = this.chartsBaseTheme$.subscribe(update);
+ return () => s.unsubscribe();
+ }, []);
+
+ return value;
+ };
+
/** initialize service with uiSettings */
public init(uiSettings: CoreSetup['uiSettings']) {
- this._chartsTheme$ = uiSettings
- .get$('theme:darkMode')
- .pipe(
- map((darkMode) => (darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme))
+ this._uiSettingsDarkMode$ = uiSettings.get$('theme:darkMode');
+ this._uiSettingsDarkMode$.subscribe((darkMode) => {
+ this._chartsTheme$.next(
+ darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme
);
+ this._chartsBaseTheme$.next(darkMode ? DARK_THEME : LIGHT_THEME);
+ });
}
}
diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx
index 9afe5e48bc5b8..4c39c8bb25542 100644
--- a/src/plugins/discover/public/application/angular/directives/histogram.tsx
+++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx
@@ -40,12 +40,13 @@ import {
ElementClickListener,
XYChartElementEvent,
BrushEndListener,
+ Theme,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme';
-import { Subscription } from 'rxjs';
+import { Subscription, combineLatest } from 'rxjs';
import { getServices } from '../../../kibana_services';
import { Chart as IChart } from '../helpers/point_series';
@@ -56,6 +57,7 @@ export interface DiscoverHistogramProps {
interface DiscoverHistogramState {
chartsTheme: EuiChartThemeType['theme'];
+ chartsBaseTheme: Theme;
}
function findIntervalFromDuration(
@@ -126,18 +128,21 @@ export class DiscoverHistogram extends Component this.setState({ chartsTheme })
+ this.subscription = combineLatest(
+ getServices().theme.chartsTheme$,
+ getServices().theme.chartsBaseTheme$
+ ).subscribe(([chartsTheme, chartsBaseTheme]) =>
+ this.setState({ chartsTheme, chartsBaseTheme })
);
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
- this.subscription = undefined;
}
}
@@ -204,7 +209,7 @@ export class DiscoverHistogram extends Component
({
+export const createLegacyUrlForwardApp = (
+ core: CoreSetup<{}, KibanaLegacyStart>,
+ forwards: ForwardDefinition[]
+): App => ({
id: 'kibana',
chromeless: true,
title: 'Legacy URL migration',
@@ -31,7 +34,8 @@ export const createLegacyUrlForwardApp = (core: CoreSetup, forwards: ForwardDefi
const hash = params.history.location.hash.substr(1);
if (!hash) {
- core.fatalErrors.add('Could not forward URL');
+ const [, , kibanaLegacyStart] = await core.getStartServices();
+ kibanaLegacyStart.navigateToDefaultApp();
}
const [
@@ -44,7 +48,8 @@ export const createLegacyUrlForwardApp = (core: CoreSetup, forwards: ForwardDefi
const result = await navigateToLegacyKibanaUrl(hash, forwards, basePath, application);
if (!result.navigated) {
- core.fatalErrors.add('Could not forward URL');
+ const [, , kibanaLegacyStart] = await core.getStartServices();
+ kibanaLegacyStart.navigateToDefaultApp();
}
return () => {};
diff --git a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx
index 63b9b48ec809e..45592c8a703af 100644
--- a/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx
+++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
-import React, { Component } from 'react';
+import React, { Component, ReactNode } from 'react';
import { EuiFormRow, EuiDualRange } from '@elastic/eui';
import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row';
import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range';
@@ -32,7 +32,7 @@ export type ValueMember = EuiDualRangeProps['value'][0];
interface Props extends Omit {
value?: Value;
allowEmptyRange?: boolean;
- label?: string;
+ label?: string | ReactNode;
formRowDisplay?: EuiFormRowDisplayKeys;
onChange?: (val: [string, string]) => void;
min?: number;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss
index 3db09bace079f..c445d456a1703 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/_vis_types.scss
@@ -2,7 +2,11 @@
display: flex;
flex-direction: column;
flex: 1 1 100%;
- padding: $euiSizeS;
+
+ // border used in lieu of padding to prevent overlapping background-color
+ border-width: $euiSizeS;
+ border-style: solid;
+ border-color: transparent;
.tvbVisTimeSeries {
overflow: hidden;
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
index ddfaf3c1428d9..612a7a48bade1 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js
@@ -34,7 +34,7 @@ import { getInterval } from '../../lib/get_interval';
import { areFieldsDifferent } from '../../lib/charts';
import { createXaxisFormatter } from '../../lib/create_xaxis_formatter';
import { STACKED_OPTIONS } from '../../../visualizations/constants';
-import { getCoreStart, getUISettings } from '../../../../services';
+import { getCoreStart } from '../../../../services';
export class TimeseriesVisualization extends Component {
static propTypes = {
@@ -154,7 +154,7 @@ export class TimeseriesVisualization extends Component {
const styles = reactCSS({
default: {
tvbVis: {
- backgroundColor: get(model, 'background_color'),
+ borderColor: get(model, 'background_color'),
},
},
});
@@ -237,7 +237,6 @@ export class TimeseriesVisualization extends Component {
}
});
- const darkMode = getUISettings().get('theme:darkMode');
return (
values.map(({ key, docs }) => ({
@@ -56,7 +56,6 @@ const handleCursorUpdate = (cursor) => {
};
export const TimeSeries = ({
- darkMode,
backgroundColor,
showGrid,
legend,
@@ -90,15 +89,15 @@ export const TimeSeries = ({
const timeZone = getTimezone(uiSettings);
const hasBarChart = series.some(({ bars }) => bars?.show);
- // compute the theme based on the bg color
- const theme = getTheme(darkMode, backgroundColor);
// apply legend style change if bgColor is configured
const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor));
// If the color isn't configured by the user, use the color mapping service
// to assign a color from the Kibana palette. Colors will be shared across the
// session, including dashboards.
- const { colors } = getChartsSetup();
+ const { colors, theme: themeService } = getChartsSetup();
+ const baseTheme = getBaseTheme(themeService.useChartsBaseTheme(), backgroundColor);
+
colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label));
const onBrushEndListener = ({ x }) => {
@@ -118,7 +117,7 @@ export const TimeSeries = ({
onBrushEnd={onBrushEndListener}
animateData={false}
onPointerUpdate={handleCursorUpdate}
- theme={
+ theme={[
hasBarChart
? {}
: {
@@ -127,9 +126,14 @@ export const TimeSeries = ({
fill: '#F00',
},
},
- }
- }
- baseTheme={theme}
+ },
+ {
+ background: {
+ color: backgroundColor,
+ },
+ },
+ ]}
+ baseTheme={baseTheme}
tooltip={{
snap: true,
type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor,
@@ -269,7 +273,6 @@ TimeSeries.defaultProps = {
};
TimeSeries.propTypes = {
- darkMode: PropTypes.bool,
backgroundColor: PropTypes.string,
showGrid: PropTypes.bool,
legend: PropTypes.bool,
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts
index 57ca38168ac27..d7e6560a8dc97 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts
@@ -17,28 +17,30 @@
* under the License.
*/
-import { getTheme } from './theme';
+import { getBaseTheme } from './theme';
import { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
describe('TSVB theme', () => {
it('should return the basic themes if no bg color is specified', () => {
// use original dark/light theme
- expect(getTheme(false)).toEqual(LIGHT_THEME);
- expect(getTheme(true)).toEqual(DARK_THEME);
+ expect(getBaseTheme(LIGHT_THEME)).toEqual(LIGHT_THEME);
+ expect(getBaseTheme(DARK_THEME)).toEqual(DARK_THEME);
// discard any wrong/missing bg color
- expect(getTheme(true, null)).toEqual(DARK_THEME);
- expect(getTheme(true, '')).toEqual(DARK_THEME);
- expect(getTheme(true, undefined)).toEqual(DARK_THEME);
+ expect(getBaseTheme(DARK_THEME, null)).toEqual(DARK_THEME);
+ expect(getBaseTheme(DARK_THEME, '')).toEqual(DARK_THEME);
+ expect(getBaseTheme(DARK_THEME, undefined)).toEqual(DARK_THEME);
});
it('should return a highcontrast color theme for a different background', () => {
// red use a near full-black color
- expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)');
+ expect(getBaseTheme(LIGHT_THEME, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)');
// violet increased the text color to full white for higer contrast
- expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)');
+ expect(getBaseTheme(LIGHT_THEME, '#ba26ff').axes.axisTitleStyle.fill).toEqual(
+ 'rgb(255,255,255)'
+ );
// light yellow, prefer the LIGHT_THEME fill color because already with a good contrast
- expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333');
+ expect(getBaseTheme(LIGHT_THEME, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333');
});
});
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts
index 2694732aa381d..0e13fd7ef68f9 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts
@@ -94,9 +94,15 @@ function isValidColor(color: string | null | undefined): color is string {
}
}
-export function getTheme(darkMode: boolean, bgColor?: string | null): Theme {
+/**
+ * compute base chart theme based on the background color
+ *
+ * @param baseTheme
+ * @param bgColor
+ */
+export function getBaseTheme(baseTheme: Theme, bgColor?: string | null): Theme {
if (!isValidColor(bgColor)) {
- return darkMode ? DARK_THEME : LIGHT_THEME;
+ return baseTheme;
}
const bgLuminosity = computeRelativeLuminosity(bgColor);
diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts
index 408cdd387cbd8..5f23a9329a583 100644
--- a/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts
+++ b/x-pack/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts
@@ -56,7 +56,6 @@ describe('timeseriesFetcher', () => {
apmAgentConfigurationIndex: '.apm-agent-configuration',
apmCustomLinkIndex: '.apm-custom-link',
},
- dynamicIndexPattern: null as any,
},
});
});
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
index c9e9db13cecae..b34d5535d58cc 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_ui_filters_es.ts
@@ -11,15 +11,9 @@ import {
localUIFilters,
localUIFilterNames,
} from '../../ui_filters/local_ui_filters/config';
-import {
- esKuery,
- IIndexPattern,
-} from '../../../../../../../src/plugins/data/server';
+import { esKuery } from '../../../../../../../src/plugins/data/server';
-export function getUiFiltersES(
- indexPattern: IIndexPattern | undefined,
- uiFilters: UIFilters
-) {
+export function getUiFiltersES(uiFilters: UIFilters) {
const { kuery, environment, ...localFilterValues } = uiFilters;
const mappedFilters = localUIFilterNames
.filter((name) => name in localFilterValues)
@@ -35,7 +29,7 @@ export function getUiFiltersES(
// remove undefined items from list
const esFilters = [
- getKueryUiFilterES(indexPattern, uiFilters.kuery),
+ getKueryUiFilterES(uiFilters.kuery),
getEnvironmentUiFilterES(uiFilters.environment),
]
.filter((filter) => !!filter)
@@ -44,14 +38,11 @@ export function getUiFiltersES(
return esFilters;
}
-function getKueryUiFilterES(
- indexPattern: IIndexPattern | undefined,
- kuery?: string
-) {
- if (!kuery || !indexPattern) {
+function getKueryUiFilterES(kuery?: string) {
+ if (!kuery) {
return;
}
const ast = esKuery.fromKueryExpression(kuery);
- return esKuery.toElasticsearchQuery(ast, indexPattern) as ESFilter;
+ return esKuery.toElasticsearchQuery(ast) as ESFilter;
}
diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts
index 892f8f0ddd105..2d730933e2473 100644
--- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts
@@ -19,11 +19,10 @@ import {
ESSearchRequest,
ESSearchResponse,
} from '../../../typings/elasticsearch';
-import { UI_SETTINGS } from '../../../../../../src/plugins/data/server';
import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames';
import { pickKeys } from '../../../common/utils/pick_keys';
import { APMRequestHandlerContext } from '../../routes/typings';
-import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
+import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices';
// `type` was deprecated in 7.0
export type APMIndexDocumentParams = Omit, 'type'>;
@@ -85,20 +84,19 @@ function addFilterForLegacyData(
}
// add additional params for search (aka: read) requests
-async function getParamsForSearchRequest(
- context: APMRequestHandlerContext,
- params: ESSearchRequest,
- apmOptions?: APMOptions
-) {
- const { uiSettings } = context.core;
- const [indices, includeFrozen] = await Promise.all([
- getApmIndices({
- savedObjectsClient: context.core.savedObjects.client,
- config: context.config,
- }),
- uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
- ]);
-
+function getParamsForSearchRequest({
+ context,
+ params,
+ indices,
+ includeFrozen,
+ includeLegacyData,
+}: {
+ context: APMRequestHandlerContext;
+ params: ESSearchRequest;
+ indices: ApmIndicesConfig;
+ includeFrozen: boolean;
+ includeLegacyData?: boolean;
+}) {
// Get indices for legacy data filter (only those which apply)
const apmIndices = Object.values(
pickKeys(
@@ -112,7 +110,7 @@ async function getParamsForSearchRequest(
)
);
return {
- ...addFilterForLegacyData(apmIndices, params, apmOptions), // filter out pre-7.0 data
+ ...addFilterForLegacyData(apmIndices, params, { includeLegacyData }), // filter out pre-7.0 data
ignore_throttled: !includeFrozen, // whether to query frozen indices or not
};
}
@@ -123,6 +121,8 @@ interface APMOptions {
interface ClientCreateOptions {
clientAsInternalUser?: boolean;
+ indices: ApmIndicesConfig;
+ includeFrozen: boolean;
}
export type ESClient = ReturnType;
@@ -134,7 +134,7 @@ function formatObj(obj: Record) {
export function getESClient(
context: APMRequestHandlerContext,
request: KibanaRequest,
- { clientAsInternalUser = false }: ClientCreateOptions = {}
+ { clientAsInternalUser = false, indices, includeFrozen }: ClientCreateOptions
) {
const {
callAsCurrentUser,
@@ -194,11 +194,13 @@ export function getESClient(
params: TSearchRequest,
apmOptions?: APMOptions
): Promise> => {
- const nextParams = await getParamsForSearchRequest(
+ const nextParams = await getParamsForSearchRequest({
context,
params,
- apmOptions
- );
+ indices,
+ includeFrozen,
+ ...apmOptions,
+ });
return callEs('search', nextParams);
},
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index 2dd8ed01082fd..14c9378d99192 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
@@ -5,8 +5,8 @@
*/
import moment from 'moment';
+import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
import { KibanaRequest } from '../../../../../../src/core/server';
-import { IIndexPattern } from '../../../../../../src/plugins/data/common';
import { APMConfig } from '../..';
import {
getApmIndices,
@@ -18,17 +18,13 @@ import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es';
import { APMRequestHandlerContext } from '../../routes/typings';
import { getESClient } from './es_client';
import { ProcessorEvent } from '../../../common/processor_event';
-import { getDynamicIndexPattern } from '../index_pattern/get_dynamic_index_pattern';
-function decodeUiFilters(
- indexPattern: IIndexPattern | undefined,
- uiFiltersEncoded?: string
-) {
- if (!uiFiltersEncoded || !indexPattern) {
+function decodeUiFilters(uiFiltersEncoded?: string) {
+ if (!uiFiltersEncoded) {
return [];
}
const uiFilters = JSON.parse(uiFiltersEncoded);
- return getUiFiltersES(indexPattern, uiFilters);
+ return getUiFiltersES(uiFilters);
}
// Explicitly type Setup to prevent TS initialization errors
// https://github.com/microsoft/TypeScript/issues/34933
@@ -39,7 +35,6 @@ export interface Setup {
ml?: ReturnType;
config: APMConfig;
indices: ApmIndicesConfig;
- dynamicIndexPattern?: IIndexPattern;
}
export interface SetupTimeRange {
@@ -75,28 +70,33 @@ export async function setupRequest(
const { config } = context;
const { query } = context.params;
- const indices = await getApmIndices({
- savedObjectsClient: context.core.savedObjects.client,
- config,
- });
+ const [indices, includeFrozen] = await Promise.all([
+ getApmIndices({
+ savedObjectsClient: context.core.savedObjects.client,
+ config,
+ }),
+ context.core.uiSettings.client.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN),
+ ]);
- const dynamicIndexPattern = await getDynamicIndexPattern({
- context,
+ const createClientOptions = {
indices,
- processorEvent: query.processorEvent,
- });
+ includeFrozen,
+ };
- const uiFiltersES = decodeUiFilters(dynamicIndexPattern, query.uiFilters);
+ const uiFiltersES = decodeUiFilters(query.uiFilters);
const coreSetupRequest = {
indices,
- client: getESClient(context, request, { clientAsInternalUser: false }),
+ client: getESClient(context, request, {
+ clientAsInternalUser: false,
+ ...createClientOptions,
+ }),
internalClient: getESClient(context, request, {
clientAsInternalUser: true,
+ ...createClientOptions,
}),
ml: getMlSetup(context, request),
config,
- dynamicIndexPattern,
};
return {
diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts
index d1f473b485dc3..fb357040f5781 100644
--- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts
+++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts
@@ -44,7 +44,6 @@ describe('timeseriesFetcher', () => {
apmAgentConfigurationIndex: 'myIndex',
apmCustomLinkIndex: 'myIndex',
},
- dynamicIndexPattern: null as any,
},
});
});
diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts
index 5fdd6de06089b..1cecf14f2eeb8 100644
--- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts
+++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts
@@ -5,7 +5,6 @@
*/
import { omit } from 'lodash';
-import { IIndexPattern } from 'src/plugins/data/server';
import { mergeProjection } from '../../../../common/projections/util/merge_projection';
import { Projection } from '../../../../common/projections/typings';
import { UIFilters } from '../../../../typings/ui_filters';
@@ -13,18 +12,16 @@ import { getUiFiltersES } from '../../helpers/convert_ui_filters/get_ui_filters_
import { localUIFilters, LocalUIFilterName } from './config';
export const getLocalFilterQuery = ({
- indexPattern,
uiFilters,
projection,
localUIFilterName,
}: {
- indexPattern: IIndexPattern | undefined;
uiFilters: UIFilters;
projection: Projection;
localUIFilterName: LocalUIFilterName;
}) => {
const field = localUIFilters[localUIFilterName];
- const filter = getUiFiltersES(indexPattern, omit(uiFilters, field.name));
+ const filter = getUiFiltersES(omit(uiFilters, field.name));
const bucketCountAggregation = projection.body.aggs
? {
diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts
index 967314644c246..31bc0563ec13f 100644
--- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts
+++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts
@@ -26,7 +26,7 @@ export async function getLocalUIFilters({
uiFilters: UIFilters;
localFilterNames: LocalUIFilterName[];
}) {
- const { client, dynamicIndexPattern } = setup;
+ const { client } = setup;
const projectionWithoutAggs = cloneDeep(projection);
@@ -35,7 +35,6 @@ export async function getLocalUIFilters({
return Promise.all(
localFilterNames.map(async (name) => {
const query = getLocalFilterQuery({
- indexPattern: dynamicIndexPattern,
uiFilters,
projection,
localUIFilterName: name,
diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts
index 018a14ac76689..18bc2986d4061 100644
--- a/x-pack/plugins/apm/server/routes/index_pattern.ts
+++ b/x-pack/plugins/apm/server/routes/index_pattern.ts
@@ -9,6 +9,8 @@ import { createRoute } from './create_route';
import { setupRequest } from '../lib/helpers/setup_request';
import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title';
+import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern';
+import { getApmIndices } from '../lib/settings/apm_indices/get_apm_indices';
export const staticIndexPatternRoute = createRoute((core) => ({
method: 'POST',
@@ -34,8 +36,17 @@ export const dynamicIndexPatternRoute = createRoute(() => ({
]),
}),
},
- handler: async ({ context, request }) => {
- const { dynamicIndexPattern } = await setupRequest(context, request);
+ handler: async ({ context }) => {
+ const indices = await getApmIndices({
+ config: context.config,
+ savedObjectsClient: context.core.savedObjects.client,
+ });
+
+ const dynamicIndexPattern = await getDynamicIndexPattern({
+ context,
+ indices,
+ });
+
return { dynamicIndexPattern };
},
}));
diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts
index 280645d4de8d0..a47d72751dfc4 100644
--- a/x-pack/plugins/apm/server/routes/ui_filters.ts
+++ b/x-pack/plugins/apm/server/routes/ui_filters.ts
@@ -97,10 +97,7 @@ function createLocalFiltersRoute<
query,
setup: {
...setup,
- uiFiltersES: getUiFiltersES(
- setup.dynamicIndexPattern,
- omit(parsedUiFilters, filterNames)
- ),
+ uiFiltersES: getUiFiltersES(omit(parsedUiFilters, filterNames)),
},
});
diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts
index 79fd012337b00..3d85c2e9eb751 100644
--- a/x-pack/plugins/features/server/plugin.test.ts
+++ b/x-pack/plugins/features/server/plugin.test.ts
@@ -10,19 +10,13 @@ const initContext = coreMock.createPluginInitializerContext();
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock();
-typeRegistry.getAllTypes.mockReturnValue([
+typeRegistry.getVisibleTypes.mockReturnValue([
{
name: 'foo',
hidden: false,
mappings: { properties: {} },
namespaceType: 'single' as 'single',
},
- {
- name: 'bar',
- hidden: true,
- mappings: { properties: {} },
- namespaceType: 'agnostic' as 'agnostic',
- },
]);
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts
index 149c1acfb5086..5783b20eae648 100644
--- a/x-pack/plugins/features/server/plugin.ts
+++ b/x-pack/plugins/features/server/plugin.ts
@@ -80,10 +80,7 @@ export class Plugin {
private registerOssFeatures(savedObjects: SavedObjectsServiceStart) {
const registry = savedObjects.getTypeRegistry();
- const savedObjectTypes = registry
- .getAllTypes()
- .filter((t) => !t.hidden)
- .map((t) => t.name);
+ const savedObjectTypes = registry.getVisibleTypes().map((t) => t.name);
this.logger.debug(
`Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 5b68cd2beeed4..bf6a8de15182d 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -32,10 +32,10 @@ export enum KibanaAssetType {
}
export enum ElasticsearchAssetType {
- componentTemplate = 'component-template',
- ingestPipeline = 'ingest-pipeline',
- indexTemplate = 'index-template',
- ilmPolicy = 'ilm-policy',
+ componentTemplate = 'component_template',
+ ingestPipeline = 'ingest_pipeline',
+ indexTemplate = 'index_template',
+ ilmPolicy = 'ilm_policy',
}
export enum AgentAssetType {
@@ -243,13 +243,10 @@ export type AssetReference = Pick & {
* Types of assets which can be installed/removed
*/
export enum IngestAssetType {
- DataFrameTransform = 'data-frame-transform',
- IlmPolicy = 'ilm-policy',
- IndexTemplate = 'index-template',
- ComponentTemplate = 'component-template',
- IngestPipeline = 'ingest-pipeline',
- MlJob = 'ml-job',
- RollupJob = 'rollup-job',
+ IlmPolicy = 'ilm_policy',
+ IndexTemplate = 'index_template',
+ ComponentTemplate = 'component_template',
+ IngestPipeline = 'ingest_pipeline',
}
export enum DefaultPackages {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
index 54cb5171f5a3e..31c6d76446447 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
@@ -17,11 +17,11 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
export const AssetTitleMap: Record = {
dashboard: 'Dashboard',
- 'ilm-policy': 'ILM Policy',
- 'ingest-pipeline': 'Ingest Pipeline',
+ ilm_policy: 'ILM Policy',
+ ingest_pipeline: 'Ingest Pipeline',
'index-pattern': 'Index Pattern',
- 'index-template': 'Index Template',
- 'component-template': 'Component Template',
+ index_template: 'Index Template',
+ component_template: 'Component Template',
search: 'Saved Search',
visualization: 'Visualization',
input: 'Agent input',
diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json
index 346a5a24c269f..7da5eaed5155e 100644
--- a/x-pack/plugins/lens/kibana.json
+++ b/x-pack/plugins/lens/kibana.json
@@ -10,7 +10,8 @@
"navigation",
"kibanaLegacy",
"visualizations",
- "dashboard"
+ "dashboard",
+ "charts"
],
"optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"],
"configPath": ["xpack", "lens"],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index f70df855fe0cb..0d60bd588f710 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -17,6 +17,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { ChangeIndexPattern } from './change_indexpattern';
import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui';
import { documentField } from './document_field';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
const initialState: IndexPatternPrivateState = {
indexPatternRefs: [],
@@ -230,6 +231,7 @@ describe('IndexPattern Data Panel', () => {
fromDate: 'now-7d',
toDate: 'now',
},
+ charts: chartPluginMock.createSetupContract(),
query: { query: '', language: 'lucene' },
filters: [],
showNoDataPopover: jest.fn(),
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index 87fbf81fceba0..0e7cefb58fc28 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -47,9 +47,11 @@ export type Props = DatasourceDataPanelProps & {
state: IndexPatternPrivateState,
setState: StateSetter
) => void;
+ charts: ChartsPluginSetup;
};
import { LensFieldIcon } from './lens_field_icon';
import { ChangeIndexPattern } from './change_indexpattern';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted
const FixedEuiContextMenuPanel = (EuiContextMenuPanel as unknown) as React.FunctionComponent<
@@ -82,6 +84,7 @@ export function IndexPatternDataPanel({
filters,
dateRange,
changeIndexPattern,
+ charts,
showNoDataPopover,
}: Props) {
const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state;
@@ -170,6 +173,7 @@ export function IndexPatternDataPanel({
dragDropContext={dragDropContext}
core={core}
data={data}
+ charts={charts}
onChangeIndexPattern={onChangeIndexPattern}
existingFields={state.existingFields}
/>
@@ -214,6 +218,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
core,
data,
existingFields,
+ charts,
}: Omit & {
data: DataPublicPluginStart;
currentIndexPatternId: string;
@@ -222,6 +227,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
dragDropContext: DragContextState;
onChangeIndexPattern: (newId: string) => void;
existingFields: IndexPatternPrivateState['existingFields'];
+ charts: ChartsPluginSetup;
}) {
const [localState, setLocalState] = useState({
nameFilter: '',
@@ -376,6 +382,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
dateRange,
query,
filters,
+ chartsThemeService: charts.theme,
}),
[core, data, currentIndexPattern, dateRange, query, filters, localState.nameFilter]
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
index e8dfbc250c539..0a3af97f8ad75 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
@@ -13,6 +13,9 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { IndexPattern } from './types';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
+
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
describe('IndexPattern Field Item', () => {
let defaultProps: FieldItemProps;
@@ -80,6 +83,7 @@ describe('IndexPattern Field Item', () => {
searchable: true,
},
exists: true,
+ chartsThemeService,
};
data.fieldFormats = ({
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 1a1a34d30f8a8..815725f4331a6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -20,7 +20,6 @@ import {
EuiText,
EuiToolTip,
} from '@elastic/eui';
-import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import {
Axis,
BarSeries,
@@ -41,6 +40,7 @@ import {
esQuery,
IIndexPattern,
} from '../../../../../src/plugins/data/public';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { DraggedField } from './indexpattern';
import { DragDrop } from '../drag_drop';
import { DatasourceDataPanelProps, DataType } from '../types';
@@ -60,6 +60,7 @@ export interface FieldItemProps {
exists: boolean;
query: Query;
dateRange: DatasourceDataPanelProps['dateRange'];
+ chartsThemeService: ChartsPluginSetup['theme'];
filters: Filter[];
hideDetails?: boolean;
}
@@ -254,11 +255,12 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
dateRange,
core,
sampledValues,
+ chartsThemeService,
data: { fieldFormats },
} = props;
- const IS_DARK_THEME = core.uiSettings.get('theme:darkMode');
- const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme;
+ const chartTheme = chartsThemeService.useChartsTheme();
+ const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
let histogramDefault = !!props.histogram;
const totalValuesCount =
@@ -410,6 +412,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
-
+
{
let defaultProps: FieldsAccordionProps;
@@ -56,6 +57,7 @@ describe('Fields Accordion', () => {
},
query: { query: '', language: 'lucene' },
filters: [],
+ chartsThemeService: chartPluginMock.createSetupContract().theme,
};
defaultProps = {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
index b756cf81a9073..7cc049c107b87 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
@@ -19,10 +19,12 @@ import { FieldItem } from './field_item';
import { Query, Filter } from '../../../../../src/plugins/data/public';
import { DatasourceDataPanelProps } from '../types';
import { IndexPattern } from './types';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export interface FieldItemSharedProps {
core: DatasourceDataPanelProps['core'];
data: DataPublicPluginStart;
+ chartsThemeService: ChartsPluginSetup['theme'];
indexPattern: IndexPattern;
highlight?: string;
query: Query;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
index 73fd144b9c7f8..45d0ee45fab4c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
@@ -9,6 +9,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { getIndexPatternDatasource } from './indexpattern';
import { renameColumns } from './rename_columns';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import {
DataPublicPluginSetup,
DataPublicPluginStart,
@@ -19,6 +20,7 @@ export interface IndexPatternDatasourceSetupPlugins {
expressions: ExpressionsSetup;
data: DataPublicPluginSetup;
editorFrame: EditorFrameSetup;
+ charts: ChartsPluginSetup;
}
export interface IndexPatternDatasourceStartPlugins {
@@ -30,7 +32,7 @@ export class IndexPatternDatasource {
setup(
core: CoreSetup,
- { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins
+ { expressions, editorFrame, charts }: IndexPatternDatasourceSetupPlugins
) {
expressions.registerFunction(renameColumns);
@@ -40,6 +42,7 @@ export class IndexPatternDatasource {
core: coreStart,
storage: new Storage(localStorage),
data,
+ charts,
})
) as Promise
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index 6a79ce450cd9a..3bd0685551a4c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -11,6 +11,7 @@ import { coreMock } from 'src/core/public/mocks';
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { Ast } from '@kbn/interpreter/common';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
jest.mock('./loader');
jest.mock('../id_generator');
@@ -140,6 +141,7 @@ describe('IndexPattern Data Source', () => {
storage: {} as IStorageWrapper,
core: coreMock.createStart(),
data: dataPluginMock.createStartContract(),
+ charts: chartPluginMock.createSetupContract(),
});
persistedState = {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index a98f63cf9b360..e9d095bfbcef1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -46,6 +46,7 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { deleteColumn } from './state_helpers';
import { Datasource, StateSetter } from '../index';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export { OperationType, IndexPatternColumn } from './operations';
@@ -102,10 +103,12 @@ export function getIndexPatternDatasource({
core,
storage,
data,
+ charts,
}: {
core: CoreStart;
storage: IStorageWrapper;
data: DataPublicPluginStart;
+ charts: ChartsPluginSetup;
}) {
const savedObjectsClient = core.savedObjects.client;
const uiSettings = core.uiSettings;
@@ -212,6 +215,7 @@ export function getIndexPatternDatasource({
});
}}
data={data}
+ charts={charts}
{...props}
/>
,
diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts
index dd828c6c35300..401b6d634c696 100644
--- a/x-pack/plugins/lens/public/pie_visualization/index.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/index.ts
@@ -4,18 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { CoreSetup } from 'src/core/public';
import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { pieVisualization } from './pie_visualization';
import { pie, getPieRenderer } from './register_expression';
import { EditorFrameSetup, FormatFactory } from '../types';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export interface PieVisualizationPluginSetupPlugins {
editorFrame: EditorFrameSetup;
expressions: ExpressionsSetup;
formatFactory: Promise;
+ charts: ChartsPluginSetup;
}
export interface PieVisualizationPluginStartPlugins {
@@ -27,17 +28,14 @@ export class PieVisualization {
setup(
core: CoreSetup,
- { expressions, formatFactory, editorFrame }: PieVisualizationPluginSetupPlugins
+ { expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins
) {
expressions.registerFunction(() => pie);
expressions.registerRenderer(
getPieRenderer({
formatFactory,
- chartTheme: core.uiSettings.get('theme:darkMode')
- ? EUI_CHARTS_THEME_DARK.theme
- : EUI_CHARTS_THEME_LIGHT.theme,
- isDarkMode: core.uiSettings.get('theme:darkMode'),
+ chartsThemeService: charts.theme,
})
);
diff --git a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
index bbc6a1dc75c3a..cea84db8b2794 100644
--- a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
@@ -8,7 +8,6 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
-import { PartialTheme } from '@elastic/charts';
import {
IInterpreterRenderHandlers,
ExpressionRenderDefinition,
@@ -17,6 +16,7 @@ import {
import { LensMultiTable, FormatFactory, LensFilterEvent } from '../types';
import { PieExpressionProps, PieExpressionArgs } from './types';
import { PieComponent } from './render_function';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export interface PieRender {
type: 'render';
@@ -93,8 +93,7 @@ export const pie: ExpressionFunctionDefinition<
export const getPieRenderer = (dependencies: {
formatFactory: Promise;
- chartTheme: PartialTheme;
- isDarkMode: boolean;
+ chartsThemeService: ChartsPluginSetup['theme'];
}): ExpressionRenderDefinition => ({
name: 'lens_pie_renderer',
displayName: i18n.translate('xpack.lens.pie.visualizationName', {
@@ -116,10 +115,9 @@ export const getPieRenderer = (dependencies: {
,
domNode,
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
index 2e29513ba548b..cfbeb27efb3d0 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
@@ -11,6 +11,9 @@ import { LensMultiTable } from '../types';
import { PieComponent } from './render_function';
import { PieExpressionArgs } from './types';
import { EmptyPlaceholder } from '../shared_components';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
+
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
describe('PieVisualization component', () => {
let getFormatSpy: jest.Mock;
@@ -57,9 +60,8 @@ describe('PieVisualization component', () => {
return {
data,
formatFactory: getFormatSpy,
- isDarkMode: false,
- chartTheme: {},
onClickValue: jest.fn(),
+ chartsThemeService,
};
}
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index 36e8d9660ab70..f349cc4dfd648 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -19,7 +19,6 @@ import {
PartitionConfig,
PartitionLayer,
PartitionLayout,
- PartialTheme,
PartitionFillLabel,
RecursivePartial,
LayerValue,
@@ -32,6 +31,7 @@ import { getSliceValueWithFallback, getFilterContext } from './render_helpers';
import { EmptyPlaceholder } from '../shared_components';
import './visualization.scss';
import { desanitizeFilterContext } from '../utils';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
const EMPTY_SLICE = Symbol('empty_slice');
@@ -40,15 +40,14 @@ const sortedColors = euiPaletteColorBlindBehindText();
export function PieComponent(
props: PieExpressionProps & {
formatFactory: FormatFactory;
- chartTheme: Exclude;
- isDarkMode: boolean;
+ chartsThemeService: ChartsPluginSetup['theme'];
onClickValue: (data: LensFilterEvent['data']) => void;
}
) {
const [firstTable] = Object.values(props.data.tables);
const formatters: Record> = {};
- const { chartTheme, isDarkMode, onClickValue } = props;
+ const { chartsThemeService, onClickValue } = props;
const {
shape,
groups,
@@ -60,6 +59,9 @@ export function PieComponent(
percentDecimals,
hideLabels,
} = props.args;
+ const isDarkMode = chartsThemeService.useDarkMode();
+ const chartTheme = chartsThemeService.useChartsTheme();
+ const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
if (!hideLabels) {
firstTable.columns.forEach((column) => {
@@ -245,6 +247,8 @@ export function PieComponent(
onClickValue(desanitizeFilterContext(context));
}}
+ theme={chartTheme}
+ baseTheme={chartBaseTheme}
/>
,
- { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies
+ {
+ kibanaLegacy,
+ expressions,
+ data,
+ embeddable,
+ visualizations,
+ charts,
+ }: LensPluginSetupDependencies
) {
const editorFrameSetupInterface = this.editorFrameService.setup(core, {
data,
embeddable,
expressions,
});
- const dependencies = {
+ const dependencies: IndexPatternDatasourceSetupPlugins &
+ XyVisualizationPluginSetupPlugins &
+ DatatableVisualizationPluginSetupPlugins &
+ MetricVisualizationPluginSetupPlugins &
+ PieVisualizationPluginSetupPlugins = {
expressions,
data,
+ charts,
editorFrame: editorFrameSetupInterface,
formatFactory: core
.getStartServices()
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
index 48c70e0a4a05b..8cb30037379da 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
renderer="canvas"
>
;
editorFrame: EditorFrameSetup;
+ charts: ChartsPluginSetup;
}
function getTimeZone(uiSettings: IUiSettingsClient) {
@@ -34,7 +35,7 @@ export class XyVisualization {
setup(
core: CoreSetup,
- { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins
+ { expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins
) {
expressions.registerFunction(() => legendConfig);
expressions.registerFunction(() => yAxisConfig);
@@ -44,9 +45,7 @@ export class XyVisualization {
expressions.registerRenderer(
getXyChartRenderer({
formatFactory,
- chartTheme: core.uiSettings.get('theme:darkMode')
- ? EUI_CHARTS_THEME_DARK.theme
- : EUI_CHARTS_THEME_LIGHT.theme,
+ chartsThemeService: charts.theme,
timeZone: getTimeZone(core.uiSettings),
histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
})
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
index 34f2a9111253b..f433a88e3bdbd 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
@@ -24,10 +24,13 @@ import { shallow } from 'enzyme';
import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types';
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
const onClickValue = jest.fn();
const onSelectRange = jest.fn();
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
+
const dateHistogramData: LensMultiTable = {
type: 'lens_multitable',
tables: {
@@ -324,7 +327,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -347,7 +350,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -398,7 +401,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -434,7 +437,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -471,7 +474,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -509,7 +512,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -554,7 +557,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -589,7 +592,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -606,7 +609,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -626,7 +629,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -646,7 +649,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -671,7 +674,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -721,7 +724,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -758,7 +761,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -778,7 +781,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -801,7 +804,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -822,7 +825,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="CEST"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -842,7 +845,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [firstLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -869,7 +872,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -890,7 +893,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1196,7 +1199,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1215,7 +1218,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1234,7 +1237,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1252,7 +1255,7 @@ describe('xy_expression', () => {
data={{ ...data }}
args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }}
formatFactory={getFormatSpy}
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
timeZone="UTC"
onClickValue={onClickValue}
@@ -1274,7 +1277,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1359,7 +1362,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1417,7 +1420,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1473,7 +1476,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
index 17ed04aa0e9c4..3ff7bd7fda304 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
@@ -15,7 +15,6 @@ import {
AreaSeries,
BarSeries,
Position,
- PartialTheme,
GeometryValue,
XYChartSeriesIdentifier,
} from '@elastic/charts';
@@ -38,6 +37,7 @@ import { XYArgs, SeriesType, visualizationTypes } from './types';
import { VisualizationContainer } from '../visualization_container';
import { isHorizontalChart } from './state_helpers';
import { parseInterval } from '../../../../../src/plugins/data/common';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { EmptyPlaceholder } from '../shared_components';
import { desanitizeFilterContext } from '../utils';
import { getAxesConfiguration } from './axes_configuration';
@@ -59,7 +59,7 @@ export interface XYRender {
}
type XYChartRenderProps = XYChartProps & {
- chartTheme: PartialTheme;
+ chartsThemeService: ChartsPluginSetup['theme'];
formatFactory: FormatFactory;
timeZone: string;
histogramBarTarget: number;
@@ -115,7 +115,7 @@ export const xyChart: ExpressionFunctionDefinition<
export const getXyChartRenderer = (dependencies: {
formatFactory: Promise;
- chartTheme: PartialTheme;
+ chartsThemeService: ChartsPluginSetup['theme'];
histogramBarTarget: number;
timeZone: string;
}): ExpressionRenderDefinition => ({
@@ -144,7 +144,7 @@ export const getXyChartRenderer = (dependencies: {
{
return !(
@@ -276,6 +278,7 @@ export function XYChart({
legendPosition={legend.position}
showLegendExtra={false}
theme={chartTheme}
+ baseTheme={chartBaseTheme}
tooltip={{
headerFormatter: (d) => xAxisFormatter.convert(d.value),
}}
diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts
index 25f10c7794fdd..98464427cc348 100644
--- a/x-pack/plugins/maps/common/constants.ts
+++ b/x-pack/plugins/maps/common/constants.ts
@@ -223,6 +223,11 @@ export enum SCALING_TYPES {
export const RGBA_0000 = 'rgba(0,0,0,0)';
+export enum MVT_FIELD_TYPE {
+ STRING = 'String',
+ NUMBER = 'Number',
+}
+
export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID';
export enum INITIAL_LOCATION {
diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
similarity index 100%
rename from x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts
rename to x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts
diff --git a/x-pack/plugins/maps/common/descriptor_types/index.ts b/x-pack/plugins/maps/common/descriptor_types/index.ts
index af0f4487f471b..b0ae065856a5d 100644
--- a/x-pack/plugins/maps/common/descriptor_types/index.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/index.ts
@@ -5,6 +5,6 @@
*/
export * from './data_request_descriptor_types';
-export * from './descriptor_types';
+export * from './sources';
export * from './map_descriptor';
export * from './style_property_descriptor_types';
diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts
index 00380ca12a486..027cc886cd7f7 100644
--- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts
@@ -5,6 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
+import { GeoJsonProperties } from 'geojson';
import { Query } from '../../../../../src/plugins/data/common';
import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants';
@@ -39,8 +40,9 @@ export type Goto = {
};
export type TooltipFeature = {
- id: number;
+ id?: number | string;
layerId: string;
+ mbProperties: GeoJsonProperties;
};
export type TooltipState = {
diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/sources.ts
similarity index 87%
rename from x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
rename to x-pack/plugins/maps/common/descriptor_types/sources.ts
index c7a706ea64f74..86ace0e32cc84 100644
--- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/sources.ts
@@ -7,7 +7,14 @@
import { FeatureCollection } from 'geojson';
import { Query } from 'src/plugins/data/public';
-import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants';
+import {
+ AGG_TYPE,
+ GRID_RESOLUTION,
+ RENDER_AS,
+ SORT_ORDER,
+ SCALING_TYPES,
+ MVT_FIELD_TYPE,
+} from '../constants';
import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types';
import { DataRequestDescriptor } from './data_request_descriptor_types';
@@ -96,18 +103,34 @@ export type XYZTMSSourceDescriptor = AbstractSourceDescriptor &
urlTemplate: string;
};
-export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor & {
+export type MVTFieldDescriptor = {
+ name: string;
+ type: MVT_FIELD_TYPE;
+};
+
+export type TiledSingleLayerVectorSourceSettings = {
urlTemplate: string;
layerName: string;
// These are the min/max zoom levels of the availability of the a particular layerName in the tileset at urlTemplate.
// These are _not_ the visible zoom-range of the data on a map.
- // Tiled data can be displayed at higher levels of zoom than that they are stored in the tileset.
- // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels
+ // These are important so mapbox does not issue invalid requests based on the zoom level.
+
+ // Tiled layer data cannot be displayed at lower levels of zoom than that they are stored in the tileset.
+ // e.g. building footprints at level 14 cannot be displayed at level 0.
minSourceZoom: number;
+ // Tiled layer data can be displayed at higher levels of zoom than that they are stored in the tileset.
+ // e.g. EMS basemap data from level 14 is at most detailed resolution and can be displayed at higher levels
maxSourceZoom: number;
+
+ fields: MVTFieldDescriptor[];
};
+export type TiledSingleLayerVectorSourceDescriptor = AbstractSourceDescriptor &
+ TiledSingleLayerVectorSourceSettings & {
+ tooltipProperties: string[];
+ };
+
export type GeojsonFileSourceDescriptor = {
__featureCollection: FeatureCollection;
name: string;
diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts
similarity index 100%
rename from x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts
rename to x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.ts
diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts
index 60d437d2321b5..e0f5c79f1d427 100644
--- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts
+++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts
@@ -128,6 +128,10 @@ export class ESAggField implements IESAggField {
async getCategoricalFieldMetaRequest(size: number): Promise {
return this._esDocField ? this._esDocField.getCategoricalFieldMetaRequest(size) : null;
}
+
+ supportsAutoDomain(): boolean {
+ return true;
+ }
}
export function esAggFieldsFactory(
diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts
index dfd5dc05f7b83..410b38e79ffe4 100644
--- a/x-pack/plugins/maps/public/classes/fields/field.ts
+++ b/x-pack/plugins/maps/public/classes/fields/field.ts
@@ -20,6 +20,12 @@ export interface IField {
isValid(): boolean;
getOrdinalFieldMetaRequest(): Promise;
getCategoricalFieldMetaRequest(size: number): Promise;
+
+ // Determines whether Maps-app can automatically determine the domain of the field-values
+ // if this is not the case (e.g. for .mvt tiled data),
+ // then styling properties that require the domain to be known cannot use this property.
+ supportsAutoDomain(): boolean;
+
supportsFieldMeta(): boolean;
}
@@ -80,4 +86,8 @@ export class AbstractField implements IField {
async getCategoricalFieldMetaRequest(size: number): Promise {
return null;
}
+
+ supportsAutoDomain(): boolean {
+ return true;
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/fields/mvt_field.ts b/x-pack/plugins/maps/public/classes/fields/mvt_field.ts
new file mode 100644
index 0000000000000..eb2bb94b36a69
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/fields/mvt_field.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 { AbstractField, IField } from './field';
+import { FIELD_ORIGIN, MVT_FIELD_TYPE } from '../../../common/constants';
+import { ITiledSingleLayerVectorSource, IVectorSource } from '../sources/vector_source';
+import { MVTFieldDescriptor } from '../../../common/descriptor_types';
+
+export class MVTField extends AbstractField implements IField {
+ private readonly _source: ITiledSingleLayerVectorSource;
+ private readonly _type: MVT_FIELD_TYPE;
+ constructor({
+ fieldName,
+ type,
+ source,
+ origin,
+ }: {
+ fieldName: string;
+ source: ITiledSingleLayerVectorSource;
+ origin: FIELD_ORIGIN;
+ type: MVT_FIELD_TYPE;
+ }) {
+ super({ fieldName, origin });
+ this._source = source;
+ this._type = type;
+ }
+
+ getMVTFieldDescriptor(): MVTFieldDescriptor {
+ return {
+ type: this._type,
+ name: this.getName(),
+ };
+ }
+
+ getSource(): IVectorSource {
+ return this._source;
+ }
+
+ async getDataType(): Promise {
+ if (this._type === MVT_FIELD_TYPE.STRING) {
+ return 'string';
+ } else if (this._type === MVT_FIELD_TYPE.NUMBER) {
+ return 'number';
+ } else {
+ throw new Error(`Unrecognized MVT field-type ${this._type}`);
+ }
+ }
+
+ async getLabel(): Promise {
+ return this.getName();
+ }
+
+ supportsAutoDomain() {
+ return false;
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts
index 6c504daf3e192..f4625e42ab5de 100644
--- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts
+++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts
@@ -60,6 +60,10 @@ export class TopTermPercentageField implements IESAggField {
return 0;
}
+ supportsAutoDomain(): boolean {
+ return true;
+ }
+
supportsFieldMeta(): boolean {
return false;
}
diff --git a/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts b/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts
new file mode 100644
index 0000000000000..8c4eb49d5040d
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/layers/__tests__/mock_sync_context.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import sinon from 'sinon';
+import { DataRequestContext } from '../../../actions';
+import { DataMeta, MapFilters } from '../../../../common/descriptor_types';
+
+export class MockSyncContext implements DataRequestContext {
+ dataFilters: MapFilters;
+ isRequestStillActive: (dataId: string, requestToken: symbol) => boolean;
+ onLoadError: (dataId: string, requestToken: symbol, errorMessage: string) => void;
+ registerCancelCallback: (requestToken: symbol, callback: () => void) => void;
+ startLoading: (dataId: string, requestToken: symbol, meta: DataMeta) => void;
+ stopLoading: (dataId: string, requestToken: symbol, data: object, meta: DataMeta) => void;
+ updateSourceData: (newData: unknown) => void;
+
+ constructor({ dataFilters }: { dataFilters: Partial }) {
+ const mapFilters: MapFilters = {
+ filters: [],
+ timeFilters: {
+ from: 'now',
+ to: '15m',
+ mode: 'relative',
+ },
+ zoom: 0,
+ ...dataFilters,
+ };
+
+ this.dataFilters = mapFilters;
+ this.isRequestStillActive = sinon.spy();
+ this.onLoadError = sinon.spy();
+ this.registerCancelCallback = sinon.spy();
+ this.startLoading = sinon.spy();
+ this.stopLoading = sinon.spy();
+ this.updateSourceData = sinon.spy();
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx
index 859d6092dc64d..368dcda6b3a5f 100644
--- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx
@@ -16,7 +16,7 @@ import {
import { getFileUploadComponent } from '../../../kibana_services';
import { GeojsonFileSource } from '../../sources/geojson_file_source';
import { VectorLayer } from '../../layers/vector_layer/vector_layer';
-// @ts-ignore
+// @ts-expect-error
import { createDefaultLayerDescriptor } from '../../sources/es_search_source';
import { RenderWizardArguments } from '../../layers/layer_wizard_registry';
diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js
index f6b9bd6280290..adcc86b9d1546 100644
--- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js
+++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js
@@ -91,7 +91,7 @@ export class HeatmapLayer extends VectorLayer {
resolution: this.getSource().getGridResolution(),
});
mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha());
- mbMap.setLayerZoomRange(heatmapLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom());
}
getLayerTypeIconName() {
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index 2250d5663378c..e122d1cda3ed9 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -325,27 +325,28 @@ export class AbstractLayer implements ILayer {
return this._source.getMinZoom();
}
+ _getMbSourceId() {
+ return this.getId();
+ }
+
_requiresPrevSourceCleanup(mbMap: unknown) {
return false;
}
_removeStaleMbSourcesAndLayers(mbMap: unknown) {
if (this._requiresPrevSourceCleanup(mbMap)) {
- // @ts-ignore
+ // @ts-expect-error
const mbStyle = mbMap.getStyle();
- // @ts-ignore
+ // @ts-expect-error
mbStyle.layers.forEach((mbLayer) => {
- // @ts-ignore
if (this.ownsMbLayerId(mbLayer.id)) {
- // @ts-ignore
+ // @ts-expect-error
mbMap.removeLayer(mbLayer.id);
}
});
- // @ts-ignore
Object.keys(mbStyle.sources).some((mbSourceId) => {
- // @ts-ignore
if (this.ownsMbSourceId(mbSourceId)) {
- // @ts-ignore
+ // @ts-expect-error
mbMap.removeSource(mbSourceId);
}
});
@@ -429,7 +430,7 @@ export class AbstractLayer implements ILayer {
throw new Error('Should implement AbstractLayer#ownsMbLayerId');
}
- ownsMbSourceId(sourceId: string): boolean {
+ ownsMbSourceId(mbSourceId: string): boolean {
throw new Error('Should implement AbstractLayer#ownsMbSourceId');
}
diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js
index 02df8acbfffad..3e2009c24a2e4 100644
--- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js
+++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js
@@ -74,8 +74,8 @@ export class TileLayer extends AbstractLayer {
return;
}
- const sourceId = this.getId();
- mbMap.addSource(sourceId, {
+ const mbSourceId = this._getMbSourceId();
+ mbMap.addSource(mbSourceId, {
type: 'raster',
tiles: [tmsSourceData.url],
tileSize: 256,
@@ -85,7 +85,7 @@ export class TileLayer extends AbstractLayer {
mbMap.addLayer({
id: mbLayerId,
type: 'raster',
- source: sourceId,
+ source: mbSourceId,
minzoom: this._descriptor.minZoom,
maxzoom: this._descriptor.maxZoom,
});
diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap
new file mode 100644
index 0000000000000..f0ae93601ce8a
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/__snapshots__/tiled_vector_layer.test.tsx.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`icon should use vector icon 1`] = `
+
+`;
diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx
new file mode 100644
index 0000000000000..ecd625db34411
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.test.tsx
@@ -0,0 +1,163 @@
+/*
+ * 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 { MockSyncContext } from '../__tests__/mock_sync_context';
+import sinon from 'sinon';
+
+jest.mock('../../../kibana_services', () => {
+ return {
+ getUiSettings() {
+ return {
+ get() {
+ return false;
+ },
+ };
+ },
+ };
+});
+
+import { shallow } from 'enzyme';
+
+import { Feature } from 'geojson';
+import { MVTSingleLayerVectorSource } from '../../sources/mvt_single_layer_vector_source';
+import {
+ DataRequestDescriptor,
+ TiledSingleLayerVectorSourceDescriptor,
+ VectorLayerDescriptor,
+} from '../../../../common/descriptor_types';
+import { SOURCE_TYPES } from '../../../../common/constants';
+import { TiledVectorLayer } from './tiled_vector_layer';
+
+const defaultConfig = {
+ urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
+ layerName: 'foobar',
+ minSourceZoom: 4,
+ maxSourceZoom: 14,
+};
+
+function createLayer(
+ layerOptions: Partial = {},
+ sourceOptions: Partial = {}
+): TiledVectorLayer {
+ const sourceDescriptor: TiledSingleLayerVectorSourceDescriptor = {
+ type: SOURCE_TYPES.MVT_SINGLE_LAYER,
+ ...defaultConfig,
+ fields: [],
+ tooltipProperties: [],
+ ...sourceOptions,
+ };
+ const mvtSource = new MVTSingleLayerVectorSource(sourceDescriptor);
+
+ const defaultLayerOptions = {
+ ...layerOptions,
+ sourceDescriptor,
+ };
+ const layerDescriptor = TiledVectorLayer.createDescriptor(defaultLayerOptions);
+ return new TiledVectorLayer({ layerDescriptor, source: mvtSource });
+}
+
+describe('visiblity', () => {
+ it('should get minzoom from source', async () => {
+ const layer: TiledVectorLayer = createLayer({}, {});
+ expect(layer.getMinZoom()).toEqual(4);
+ });
+ it('should get maxzoom from default', async () => {
+ const layer: TiledVectorLayer = createLayer({}, {});
+ expect(layer.getMaxZoom()).toEqual(24);
+ });
+ it('should get maxzoom from layer options', async () => {
+ const layer: TiledVectorLayer = createLayer({ maxZoom: 10 }, {});
+ expect(layer.getMaxZoom()).toEqual(10);
+ });
+});
+
+describe('icon', () => {
+ it('should use vector icon', async () => {
+ const layer: TiledVectorLayer = createLayer({}, {});
+
+ const iconAndTooltipContent = layer.getCustomIconAndTooltipContent();
+ const component = shallow(iconAndTooltipContent.icon);
+ expect(component).toMatchSnapshot();
+ });
+});
+
+describe('getFeatureById', () => {
+ it('should return null feature', async () => {
+ const layer: TiledVectorLayer = createLayer({}, {});
+ const feature = layer.getFeatureById('foobar') as Feature;
+ expect(feature).toEqual(null);
+ });
+});
+
+describe('syncData', () => {
+ it('Should sync with source-params', async () => {
+ const layer: TiledVectorLayer = createLayer({}, {});
+
+ const syncContext = new MockSyncContext({ dataFilters: {} });
+
+ await layer.syncData(syncContext);
+ // @ts-expect-error
+ sinon.assert.calledOnce(syncContext.startLoading);
+ // @ts-expect-error
+ sinon.assert.calledOnce(syncContext.stopLoading);
+
+ // @ts-expect-error
+ const call = syncContext.stopLoading.getCall(0);
+ expect(call.args[2]).toEqual(defaultConfig);
+ });
+
+ it('Should not resync when no changes to source params', async () => {
+ const layer1: TiledVectorLayer = createLayer({}, {});
+ const syncContext1 = new MockSyncContext({ dataFilters: {} });
+
+ await layer1.syncData(syncContext1);
+
+ const dataRequestDescriptor: DataRequestDescriptor = {
+ data: { ...defaultConfig },
+ dataId: 'source',
+ };
+ const layer2: TiledVectorLayer = createLayer(
+ {
+ __dataRequests: [dataRequestDescriptor],
+ },
+ {}
+ );
+ const syncContext2 = new MockSyncContext({ dataFilters: {} });
+ await layer2.syncData(syncContext2);
+ // @ts-expect-error
+ sinon.assert.notCalled(syncContext2.startLoading);
+ // @ts-expect-error
+ sinon.assert.notCalled(syncContext2.stopLoading);
+ });
+
+ it('Should resync when changes to source params', async () => {
+ const layer1: TiledVectorLayer = createLayer({}, {});
+ const syncContext1 = new MockSyncContext({ dataFilters: {} });
+
+ await layer1.syncData(syncContext1);
+
+ const dataRequestDescriptor: DataRequestDescriptor = {
+ data: defaultConfig,
+ dataId: 'source',
+ };
+ const layer2: TiledVectorLayer = createLayer(
+ {
+ __dataRequests: [dataRequestDescriptor],
+ },
+ { layerName: 'barfoo' }
+ );
+ const syncContext2 = new MockSyncContext({ dataFilters: {} });
+ await layer2.syncData(syncContext2);
+
+ // @ts-expect-error
+ sinon.assert.calledOnce(syncContext2.startLoading);
+ // @ts-expect-error
+ sinon.assert.calledOnce(syncContext2.stopLoading);
+
+ // @ts-expect-error
+ const call = syncContext2.stopLoading.getCall(0);
+ expect(call.args[2]).toEqual({ ...defaultConfig, layerName: 'barfoo' });
+ });
+});
diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
index a00639aa5fec5..c9ae1c805fa30 100644
--- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx
@@ -6,31 +6,30 @@
import React from 'react';
import { EuiIcon } from '@elastic/eui';
+import { Feature } from 'geojson';
import { VectorStyle } from '../../styles/vector/vector_style';
import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE } from '../../../../common/constants';
import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer';
-import { canSkipSourceUpdate } from '../../util/can_skip_fetch';
import { ITiledSingleLayerVectorSource } from '../../sources/vector_source';
import { DataRequestContext } from '../../../actions';
-import { ISource } from '../../sources/source';
import {
VectorLayerDescriptor,
VectorSourceRequestMeta,
} from '../../../../common/descriptor_types';
-import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor';
+import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/types';
export class TiledVectorLayer extends VectorLayer {
static type = LAYER_TYPE.TILED_VECTOR;
static createDescriptor(
descriptor: Partial,
- mapColors: string[]
+ mapColors?: string[]
): VectorLayerDescriptor {
const layerDescriptor = super.createDescriptor(descriptor, mapColors);
layerDescriptor.type = TiledVectorLayer.type;
if (!layerDescriptor.style) {
- const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors);
+ const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []);
layerDescriptor.style = VectorStyle.createDescriptor(styleProperties);
}
@@ -64,13 +63,16 @@ export class TiledVectorLayer extends VectorLayer {
);
const prevDataRequest = this.getSourceDataRequest();
- const canSkip = await canSkipSourceUpdate({
- source: this._source as ISource,
- prevDataRequest,
- nextMeta: searchFilters,
- });
- if (canSkip) {
- return null;
+ if (prevDataRequest) {
+ const data: MVTSingleLayerVectorSourceConfig = prevDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
+ const canSkipBecauseNoChanges =
+ data.layerName === this._source.getLayerName() &&
+ data.minSourceZoom === this._source.getMinZoom() &&
+ data.maxSourceZoom === this._source.getMaxZoom();
+
+ if (canSkipBecauseNoChanges) {
+ return null;
+ }
}
startLoading(SOURCE_DATA_REQUEST_ID, requestToken, searchFilters);
@@ -89,37 +91,41 @@ export class TiledVectorLayer extends VectorLayer {
}
_syncSourceBindingWithMb(mbMap: unknown) {
- // @ts-ignore
- const mbSource = mbMap.getSource(this.getId());
- if (!mbSource) {
- const sourceDataRequest = this.getSourceDataRequest();
- if (!sourceDataRequest) {
- // this is possible if the layer was invisible at startup.
- // the actions will not perform any data=syncing as an optimization when a layer is invisible
- // when turning the layer back into visible, it's possible the url has not been resovled yet.
- return;
- }
+ // @ts-expect-error
+ const mbSource = mbMap.getSource(this._getMbSourceId());
+ if (mbSource) {
+ return;
+ }
+ const sourceDataRequest = this.getSourceDataRequest();
+ if (!sourceDataRequest) {
+ // this is possible if the layer was invisible at startup.
+ // the actions will not perform any data=syncing as an optimization when a layer is invisible
+ // when turning the layer back into visible, it's possible the url has not been resovled yet.
+ return;
+ }
- const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
- if (!sourceMeta) {
- return;
- }
+ const sourceMeta: MVTSingleLayerVectorSourceConfig | null = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
+ if (!sourceMeta) {
+ return;
+ }
- const sourceId = this.getId();
+ const mbSourceId = this._getMbSourceId();
+ // @ts-expect-error
+ mbMap.addSource(mbSourceId, {
+ type: 'vector',
+ tiles: [sourceMeta.urlTemplate],
+ minzoom: sourceMeta.minSourceZoom,
+ maxzoom: sourceMeta.maxSourceZoom,
+ });
+ }
- // @ts-ignore
- mbMap.addSource(sourceId, {
- type: 'vector',
- tiles: [sourceMeta.urlTemplate],
- minzoom: sourceMeta.minSourceZoom,
- maxzoom: sourceMeta.maxSourceZoom,
- });
- }
+ ownsMbSourceId(mbSourceId: string): boolean {
+ return this._getMbSourceId() === mbSourceId;
}
_syncStylePropertiesWithMb(mbMap: unknown) {
// @ts-ignore
- const mbSource = mbMap.getSource(this.getId());
+ const mbSource = mbMap.getSource(this._getMbSourceId());
if (!mbSource) {
return;
}
@@ -129,32 +135,52 @@ export class TiledVectorLayer extends VectorLayer {
return;
}
const sourceMeta: MVTSingleLayerVectorSourceConfig = sourceDataRequest.getData() as MVTSingleLayerVectorSourceConfig;
+ if (sourceMeta.layerName === '') {
+ return;
+ }
this._setMbPointsProperties(mbMap, sourceMeta.layerName);
this._setMbLinePolygonProperties(mbMap, sourceMeta.layerName);
}
_requiresPrevSourceCleanup(mbMap: unknown): boolean {
- // @ts-ignore
- const mbTileSource = mbMap.getSource(this.getId());
+ // @ts-expect-error
+ const mbTileSource = mbMap.getSource(this._getMbSourceId());
if (!mbTileSource) {
return false;
}
+
const dataRequest = this.getSourceDataRequest();
if (!dataRequest) {
return false;
}
const tiledSourceMeta: MVTSingleLayerVectorSourceConfig | null = dataRequest.getData() as MVTSingleLayerVectorSourceConfig;
- if (
- mbTileSource.tiles[0] === tiledSourceMeta.urlTemplate &&
- mbTileSource.minzoom === tiledSourceMeta.minSourceZoom &&
- mbTileSource.maxzoom === tiledSourceMeta.maxSourceZoom
- ) {
- // TileURL and zoom-range captures all the state. If this does not change, no updates are required.
+
+ if (!tiledSourceMeta) {
return false;
}
- return true;
+ const isSourceDifferent =
+ mbTileSource.tiles[0] !== tiledSourceMeta.urlTemplate ||
+ mbTileSource.minzoom !== tiledSourceMeta.minSourceZoom ||
+ mbTileSource.maxzoom !== tiledSourceMeta.maxSourceZoom;
+
+ if (isSourceDifferent) {
+ return true;
+ }
+
+ const layerIds = this.getMbLayerIds();
+ for (let i = 0; i < layerIds.length; i++) {
+ // @ts-expect-error
+ const mbLayer = mbMap.getLayer(layerIds[i]);
+ if (mbLayer && mbLayer.sourceLayer !== tiledSourceMeta.layerName) {
+ // If the source-pointer of one of the layers is stale, they will all be stale.
+ // In this case, all the mb-layers need to be removed and re-added.
+ return true;
+ }
+ }
+
+ return false;
}
syncLayerWithMB(mbMap: unknown) {
@@ -171,4 +197,8 @@ export class TiledVectorLayer extends VectorLayer {
// higher resolution vector tiles cannot be displayed at lower-res
return Math.max(this._source.getMinZoom(), super.getMinZoom());
}
+
+ getFeatureById(id: string | number): Feature | null {
+ return null;
+ }
}
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts
index e420087628bc8..77daf9c9af570 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts
@@ -5,6 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
+import { Feature, GeoJsonProperties } from 'geojson';
import { AbstractLayer } from '../layer';
import { IVectorSource } from '../../sources/vector_source';
import {
@@ -17,6 +18,7 @@ import { IJoin } from '../../joins/join';
import { IVectorStyle } from '../../styles/vector/vector_style';
import { IField } from '../../fields/field';
import { DataRequestContext } from '../../../actions';
+import { ITooltipProperty } from '../../tooltips/tooltip_property';
export type VectorLayerArguments = {
source: IVectorSource;
@@ -31,6 +33,8 @@ export interface IVectorLayer extends ILayer {
getValidJoins(): IJoin[];
getSource(): IVectorSource;
getStyle(): IVectorStyle;
+ getFeatureById(id: string | number): Feature | null;
+ getPropertiesForTooltip(properties: GeoJsonProperties): Promise;
}
export class VectorLayer extends AbstractLayer implements IVectorLayer {
@@ -75,4 +79,6 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer {
_setMbLinePolygonProperties(mbMap: unknown, mvtSourceLayer?: string): void;
getSource(): IVectorSource;
getStyle(): IVectorStyle;
+ getFeatureById(id: string | number): Feature | null;
+ getPropertiesForTooltip(properties: GeoJsonProperties): Promise;
}
diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js
index 524ab245c6760..0a4fcfc23060c 100644
--- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js
+++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js
@@ -672,10 +672,10 @@ export class VectorLayer extends AbstractLayer {
}
this.syncVisibilityWithMb(mbMap, markerLayerId);
- mbMap.setLayerZoomRange(markerLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ mbMap.setLayerZoomRange(markerLayerId, this.getMinZoom(), this.getMaxZoom());
if (markerLayerId !== textLayerId) {
this.syncVisibilityWithMb(mbMap, textLayerId);
- mbMap.setLayerZoomRange(textLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ mbMap.setLayerZoomRange(textLayerId, this.getMinZoom(), this.getMaxZoom());
}
}
@@ -802,14 +802,14 @@ export class VectorLayer extends AbstractLayer {
});
this.syncVisibilityWithMb(mbMap, fillLayerId);
- mbMap.setLayerZoomRange(fillLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom());
const fillFilterExpr = getFillFilterExpression(hasJoins);
if (fillFilterExpr !== mbMap.getFilter(fillLayerId)) {
mbMap.setFilter(fillLayerId, fillFilterExpr);
}
this.syncVisibilityWithMb(mbMap, lineLayerId);
- mbMap.setLayerZoomRange(lineLayerId, this._descriptor.minZoom, this._descriptor.maxZoom);
+ mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom());
const lineFilterExpr = getLineFilterExpression(hasJoins);
if (lineFilterExpr !== mbMap.getFilter(lineLayerId)) {
mbMap.setFilter(lineLayerId, lineFilterExpr);
@@ -822,9 +822,9 @@ export class VectorLayer extends AbstractLayer {
}
_syncSourceBindingWithMb(mbMap) {
- const mbSource = mbMap.getSource(this.getId());
+ const mbSource = mbMap.getSource(this._getMbSourceId());
if (!mbSource) {
- mbMap.addSource(this.getId(), {
+ mbMap.addSource(this._getMbSourceId(), {
type: 'geojson',
data: EMPTY_FEATURE_COLLECTION,
});
@@ -891,16 +891,17 @@ export class VectorLayer extends AbstractLayer {
}
async getPropertiesForTooltip(properties) {
- let allTooltips = await this.getSource().filterAndFormatPropertiesToHtml(properties);
- this._addJoinsToSourceTooltips(allTooltips);
+ const vectorSource = this.getSource();
+ let allProperties = await vectorSource.filterAndFormatPropertiesToHtml(properties);
+ this._addJoinsToSourceTooltips(allProperties);
for (let i = 0; i < this.getJoins().length; i++) {
const propsFromJoin = await this.getJoins()[i].filterAndFormatPropertiesForTooltip(
properties
);
- allTooltips = [...allTooltips, ...propsFromJoin];
+ allProperties = [...allProperties, ...propsFromJoin];
}
- return allTooltips;
+ return allProperties;
}
canShowTooltip() {
@@ -912,7 +913,7 @@ export class VectorLayer extends AbstractLayer {
getFeatureById(id) {
const featureCollection = this._getSourceFeatureCollection();
if (!featureCollection) {
- return;
+ return null;
}
return featureCollection.features.find((feature) => {
diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx
index ac69505a9bed5..7021859ee9827 100644
--- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx
@@ -15,7 +15,7 @@ import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/vi
interface Props {
layerId: string;
- onChange: (args: OnSourceChangeArgs) => void;
+ onChange: (...args: OnSourceChangeArgs[]) => void;
source: IEmsFileSource;
tooltipFields: IField[];
}
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap
new file mode 100644
index 0000000000000..f6d0129e85abf
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_field_config_editor.test.tsx.snap
@@ -0,0 +1,491 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render error for dupes 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ string
+
+ ,
+ "value": "String",
+ },
+ Object {
+ "inputDisplay":
+
+
+
+
+ number
+
+ ,
+ "value": "Number",
+ },
+ ]
+ }
+ valueOfSelected="String"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ string
+
+ ,
+ "value": "String",
+ },
+ Object {
+ "inputDisplay":
+
+
+
+
+ number
+
+ ,
+ "value": "Number",
+ },
+ ]
+ }
+ valueOfSelected="Number"
+ />
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
+
+`;
+
+exports[`should render error for empty name 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ string
+
+ ,
+ "value": "String",
+ },
+ Object {
+ "inputDisplay":
+
+
+
+
+ number
+
+ ,
+ "value": "Number",
+ },
+ ]
+ }
+ valueOfSelected="String"
+ />
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
+
+`;
+
+exports[`should render field editor 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ string
+
+ ,
+ "value": "String",
+ },
+ Object {
+ "inputDisplay":
+
+
+
+
+ number
+
+ ,
+ "value": "Number",
+ },
+ ]
+ }
+ valueOfSelected="String"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ string
+
+ ,
+ "value": "String",
+ },
+ Object {
+ "inputDisplay":
+
+
+
+
+ number
+
+ ,
+ "value": "Number",
+ },
+ ]
+ }
+ valueOfSelected="Number"
+ />
+
+
+
+
+
+
+
+
+
+
+ Add
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap
new file mode 100644
index 0000000000000..699173bd362fa
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_source_settings.test.tsx.snap
@@ -0,0 +1,211 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render fields-editor when there is no layername 1`] = `
+
+
+
+
+
+
+ Available levels
+
+
+
+
+ }
+ max={24}
+ min={0}
+ onChange={[Function]}
+ prepend="Zoom"
+ showInput="inputWithPopover"
+ showLabels={true}
+ value={
+ Array [
+ 4,
+ 14,
+ ]
+ }
+ />
+
+`;
+
+exports[`should render with fields 1`] = `
+
+
+
+
+
+
+ Available levels
+
+
+
+
+ }
+ max={24}
+ min={0}
+ onChange={[Function]}
+ prepend="Zoom"
+ showInput="inputWithPopover"
+ showLabels={true}
+ value={
+ Array [
+ 4,
+ 14,
+ ]
+ }
+ />
+
+ Fields which are available in
+
+
+ foobar
+
+ .
+
+
+ These can be used for tooltips and dynamic styling.
+
+ }
+ delay="regular"
+ position="top"
+ >
+
+ Fields
+
+
+
+
+ }
+ labelType="label"
+ >
+
+
+
+`;
+
+exports[`should render without fields 1`] = `
+
+
+
+
+
+
+ Available levels
+
+
+
+
+ }
+ max={24}
+ min={0}
+ onChange={[Function]}
+ prepend="Zoom"
+ showInput="inputWithPopover"
+ showLabels={true}
+ value={
+ Array [
+ 4,
+ 14,
+ ]
+ }
+ />
+
+`;
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap
new file mode 100644
index 0000000000000..ccd0e0064d075
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/mvt_single_layer_vector_source_editor.test.tsx.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render source creation editor (fields should _not_ be included) 1`] = `
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap
new file mode 100644
index 0000000000000..bccf2b17e2b5d
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/__snapshots__/update_source_editor.test.tsx.snap
@@ -0,0 +1,57 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render update source editor (fields _should_ be included) 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx
index 067c7f5a47ca3..32fa329be85df 100644
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx
@@ -6,23 +6,21 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
-import {
- MVTSingleLayerVectorSourceEditor,
- MVTSingleLayerVectorSourceConfig,
-} from './mvt_single_layer_vector_source_editor';
+import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor';
import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source';
import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer';
import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants';
+import { TiledSingleLayerVectorSourceSettings } from '../../../../common/descriptor_types';
export const mvtVectorSourceWizardConfig: LayerWizard = {
categories: [LAYER_WIZARD_CATEGORY.REFERENCE],
description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', {
- defaultMessage: 'Vector source wizard',
+ defaultMessage: 'Data service implementing the Mapbox vector tile specification',
}),
icon: 'grid',
renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
- const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => {
+ const onSourceConfigChange = (sourceConfig: TiledSingleLayerVectorSourceSettings) => {
const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig);
const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
previewLayers([layerDescriptor]);
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx
new file mode 100644
index 0000000000000..0121dc45cb9ee
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.test.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('../../../kibana_services', () => ({}));
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { MVTFieldConfigEditor } from './mvt_field_config_editor';
+import { MVT_FIELD_TYPE } from '../../../../common/constants';
+
+test('should render field editor', async () => {
+ const fields = [
+ {
+ name: 'foo',
+ type: MVT_FIELD_TYPE.STRING,
+ },
+ {
+ name: 'bar',
+ type: MVT_FIELD_TYPE.NUMBER,
+ },
+ ];
+ const component = shallow( {}} />);
+
+ expect(component).toMatchSnapshot();
+});
+
+test('should render error for empty name', async () => {
+ const fields = [
+ {
+ name: '',
+ type: MVT_FIELD_TYPE.STRING,
+ },
+ ];
+ const component = shallow( {}} />);
+
+ expect(component).toMatchSnapshot();
+});
+
+test('should render error for dupes', async () => {
+ const fields = [
+ {
+ name: 'foo',
+ type: MVT_FIELD_TYPE.STRING,
+ },
+ {
+ name: 'foo',
+ type: MVT_FIELD_TYPE.NUMBER,
+ },
+ ];
+ const component = shallow( {}} />);
+
+ expect(component).toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx
new file mode 100644
index 0000000000000..b2a93a4ef88ad
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_field_config_editor.tsx
@@ -0,0 +1,210 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* eslint-disable @typescript-eslint/consistent-type-definitions */
+
+import React, { ChangeEvent, Component, Fragment } from 'react';
+import {
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiSuperSelect,
+ EuiFieldText,
+ EuiSpacer,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import _ from 'lodash';
+import { MVTFieldDescriptor } from '../../../../common/descriptor_types';
+import { FieldIcon } from '../../../../../../../src/plugins/kibana_react/public';
+import { MVT_FIELD_TYPE } from '../../../../common/constants';
+
+function makeOption({
+ value,
+ icon,
+ message,
+}: {
+ value: MVT_FIELD_TYPE;
+ icon: string;
+ message: string;
+}) {
+ return {
+ value,
+ inputDisplay: (
+
+
+
+
+ {message}
+
+ ),
+ };
+}
+
+const FIELD_TYPE_OPTIONS = [
+ {
+ value: MVT_FIELD_TYPE.STRING,
+ icon: 'string',
+ message: i18n.translate('xpack.maps.mvtSource.stringFieldLabel', {
+ defaultMessage: 'string',
+ }),
+ },
+ {
+ value: MVT_FIELD_TYPE.NUMBER,
+ icon: 'number',
+ message: i18n.translate('xpack.maps.mvtSource.numberFieldLabel', {
+ defaultMessage: 'number',
+ }),
+ },
+].map(makeOption);
+
+interface Props {
+ fields: MVTFieldDescriptor[];
+ onChange: (fields: MVTFieldDescriptor[]) => void;
+}
+
+interface State {
+ currentFields: MVTFieldDescriptor[];
+}
+
+export class MVTFieldConfigEditor extends Component {
+ state: State = {
+ currentFields: _.cloneDeep(this.props.fields),
+ };
+
+ _notifyChange = _.debounce(() => {
+ const invalid = this.state.currentFields.some((field: MVTFieldDescriptor) => {
+ return field.name === '';
+ });
+
+ if (!invalid) {
+ this.props.onChange(this.state.currentFields);
+ }
+ });
+
+ _fieldChange(newFields: MVTFieldDescriptor[]) {
+ this.setState(
+ {
+ currentFields: newFields,
+ },
+ this._notifyChange
+ );
+ }
+
+ _removeField(index: number) {
+ const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice();
+ newFields.splice(index, 1);
+ this._fieldChange(newFields);
+ }
+
+ _addField = () => {
+ const newFields: MVTFieldDescriptor[] = this.state.currentFields.slice();
+ newFields.push({
+ type: MVT_FIELD_TYPE.STRING,
+ name: '',
+ });
+ this._fieldChange(newFields);
+ };
+
+ _renderFieldTypeDropDown(mvtFieldConfig: MVTFieldDescriptor, index: number) {
+ const onChange = (type: MVT_FIELD_TYPE) => {
+ const newFields = this.state.currentFields.slice();
+ newFields[index] = {
+ type,
+ name: newFields[index].name,
+ };
+ this._fieldChange(newFields);
+ };
+
+ return (
+ onChange(value)}
+ compressed
+ />
+ );
+ }
+
+ _renderFieldButtonDelete(index: number) {
+ return (
+ {
+ this._removeField(index);
+ }}
+ title={i18n.translate('xpack.maps.mvtSource.trashButtonTitle', {
+ defaultMessage: 'Remove field',
+ })}
+ aria-label={i18n.translate('xpack.maps.mvtSource.trashButtonAriaLabel', {
+ defaultMessage: 'Remove field',
+ })}
+ />
+ );
+ }
+
+ _renderFieldNameInput(mvtFieldConfig: MVTFieldDescriptor, index: number) {
+ const onChange = (e: ChangeEvent) => {
+ const name = e.target.value;
+ const newFields = this.state.currentFields.slice();
+ newFields[index] = {
+ name,
+ type: newFields[index].type,
+ };
+ this._fieldChange(newFields);
+ };
+
+ const emptyName = mvtFieldConfig.name === '';
+ const hasDupes =
+ this.state.currentFields.filter((field) => field.name === mvtFieldConfig.name).length > 1;
+
+ return (
+
+ );
+ }
+
+ _renderFieldConfig() {
+ return this.state.currentFields.map((mvtFieldConfig: MVTFieldDescriptor, index: number) => {
+ return (
+ <>
+
+ {this._renderFieldNameInput(mvtFieldConfig, index)}
+ {this._renderFieldTypeDropDown(mvtFieldConfig, index)}
+ {this._renderFieldButtonDelete(index)}
+
+
+ >
+ );
+ });
+ }
+
+ render() {
+ return (
+
+ {this._renderFieldConfig()}
+
+
+
+
+ {i18n.translate('xpack.maps.mvtSource.addFieldLabel', {
+ defaultMessage: 'Add',
+ })}
+
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx
new file mode 100644
index 0000000000000..b5c75b97e6cb2
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.test.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('../../../kibana_services', () => ({}));
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings';
+
+const defaultSettings = {
+ handleChange: () => {},
+ layerName: 'foobar',
+ fields: [],
+ minSourceZoom: 4,
+ maxSourceZoom: 14,
+ showFields: true,
+};
+
+test('should render with fields', async () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
+
+test('should render without fields', async () => {
+ const settings = { ...defaultSettings, showFields: false };
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
+
+test('should not render fields-editor when there is no layername', async () => {
+ const settings = { ...defaultSettings, layerName: '' };
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx
new file mode 100644
index 0000000000000..cd3fd97cf66a6
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_source_settings.tsx
@@ -0,0 +1,191 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* eslint-disable @typescript-eslint/consistent-type-definitions */
+
+import React, { Fragment, Component, ChangeEvent } from 'react';
+import { EuiFieldText, EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import _ from 'lodash';
+import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
+import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public';
+import { MVTFieldConfigEditor } from './mvt_field_config_editor';
+import { MVTFieldDescriptor } from '../../../../common/descriptor_types';
+
+export type MVTSettings = {
+ layerName: string;
+ fields: MVTFieldDescriptor[];
+ minSourceZoom: number;
+ maxSourceZoom: number;
+};
+
+interface State {
+ currentLayerName: string;
+ currentMinSourceZoom: number;
+ currentMaxSourceZoom: number;
+ currentFields: MVTFieldDescriptor[];
+}
+
+interface Props {
+ handleChange: (args: MVTSettings) => void;
+ layerName: string;
+ fields: MVTFieldDescriptor[];
+ minSourceZoom: number;
+ maxSourceZoom: number;
+ showFields: boolean;
+}
+
+export class MVTSingleLayerSourceSettings extends Component {
+ // Tracking in state to allow for debounce.
+ // Changes to layer-name and/or min/max zoom require heavy operation at map-level (removing and re-adding all sources/layers)
+ // To preserve snappyness of typing, debounce the dispatches.
+ state = {
+ currentLayerName: this.props.layerName,
+ currentMinSourceZoom: this.props.minSourceZoom,
+ currentMaxSourceZoom: this.props.maxSourceZoom,
+ currentFields: _.cloneDeep(this.props.fields),
+ };
+
+ _handleChange = _.debounce(() => {
+ this.props.handleChange({
+ layerName: this.state.currentLayerName,
+ minSourceZoom: this.state.currentMinSourceZoom,
+ maxSourceZoom: this.state.currentMaxSourceZoom,
+ fields: this.state.currentFields,
+ });
+ }, 200);
+
+ _handleLayerNameInputChange = (e: ChangeEvent) => {
+ this.setState({ currentLayerName: e.target.value }, this._handleChange);
+ };
+
+ _handleFieldChange = (fields: MVTFieldDescriptor[]) => {
+ this.setState({ currentFields: fields }, this._handleChange);
+ };
+
+ _handleZoomRangeChange = (e: Value) => {
+ this.setState(
+ {
+ currentMinSourceZoom: parseInt(e[0] as string, 10),
+ currentMaxSourceZoom: parseInt(e[1] as string, 10),
+ },
+ this._handleChange
+ );
+ };
+
+ render() {
+ const preMessage = i18n.translate(
+ 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPreHelpMessage',
+ {
+ defaultMessage: 'Fields which are available in ',
+ }
+ );
+ const message = (
+ <>
+ {this.state.currentLayerName}.{' '}
+ >
+ );
+ const postMessage = i18n.translate(
+ 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsPostHelpMessage',
+ {
+ defaultMessage: 'These can be used for tooltips and dynamic styling.',
+ }
+ );
+ const fieldEditor =
+ this.props.showFields && this.state.currentLayerName !== '' ? (
+
+ {preMessage}
+ {message}
+ {postMessage}
+ >
+ }
+ >
+
+ {i18n.translate(
+ 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.fieldsMessage',
+ {
+ defaultMessage: 'Fields',
+ }
+ )}{' '}
+
+
+
+ }
+ >
+
+
+ ) : null;
+
+ return (
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.zoomRangeTopMessage',
+ {
+ defaultMessage: 'Available levels',
+ }
+ )}{' '}
+
+
+
+ }
+ formRowDisplay="columnCompressed"
+ value={[this.state.currentMinSourceZoom, this.state.currentMaxSourceZoom]}
+ min={MIN_ZOOM}
+ max={MAX_ZOOM}
+ onChange={this._handleZoomRangeChange}
+ allowEmptyRange={false}
+ showInput="inputWithPopover"
+ compressed
+ showLabels
+ prepend={i18n.translate(
+ 'xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage',
+ {
+ defaultMessage: 'Zoom',
+ }
+ )}
+ />
+ {fieldEditor}
+
+ );
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx
new file mode 100644
index 0000000000000..bc08baad7a842
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.test.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source';
+import { MVT_FIELD_TYPE, SOURCE_TYPES } from '../../../../common/constants';
+import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types';
+
+const descriptor: TiledSingleLayerVectorSourceDescriptor = {
+ type: SOURCE_TYPES.MVT_SINGLE_LAYER,
+ urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
+ layerName: 'foobar',
+ minSourceZoom: 4,
+ maxSourceZoom: 14,
+ fields: [],
+ tooltipProperties: [],
+};
+
+describe('getUrlTemplateWithMeta', () => {
+ it('should echo configuration', async () => {
+ const source = new MVTSingleLayerVectorSource(descriptor);
+ const config = await source.getUrlTemplateWithMeta();
+ expect(config.urlTemplate).toEqual(descriptor.urlTemplate);
+ expect(config.layerName).toEqual(descriptor.layerName);
+ expect(config.minSourceZoom).toEqual(descriptor.minSourceZoom);
+ expect(config.maxSourceZoom).toEqual(descriptor.maxSourceZoom);
+ });
+});
+
+describe('canFormatFeatureProperties', () => {
+ it('false if no tooltips', async () => {
+ const source = new MVTSingleLayerVectorSource(descriptor);
+ expect(source.canFormatFeatureProperties()).toEqual(false);
+ });
+ it('true if tooltip', async () => {
+ const descriptorWithTooltips = {
+ ...descriptor,
+ fields: [{ name: 'foobar', type: MVT_FIELD_TYPE.STRING }],
+ tooltipProperties: ['foobar'],
+ };
+ const source = new MVTSingleLayerVectorSource(descriptorWithTooltips);
+ expect(source.canFormatFeatureProperties()).toEqual(true);
+ });
+});
+
+describe('filterAndFormatPropertiesToHtml', () => {
+ const descriptorWithFields = {
+ ...descriptor,
+ fields: [
+ {
+ name: 'foo',
+ type: MVT_FIELD_TYPE.STRING,
+ },
+ {
+ name: 'food',
+ type: MVT_FIELD_TYPE.STRING,
+ },
+ {
+ name: 'fooz',
+ type: MVT_FIELD_TYPE.NUMBER,
+ },
+ ],
+ tooltipProperties: ['foo', 'fooz'],
+ };
+
+ it('should get tooltipproperties', async () => {
+ const source = new MVTSingleLayerVectorSource(descriptorWithFields);
+ const tooltipProperties = await source.filterAndFormatPropertiesToHtml({
+ foo: 'bar',
+ fooz: 123,
+ });
+ expect(tooltipProperties.length).toEqual(2);
+ expect(tooltipProperties[0].getPropertyName()).toEqual('foo');
+ expect(tooltipProperties[0].getHtmlDisplayValue()).toEqual('bar');
+ expect(tooltipProperties[1].getPropertyName()).toEqual('fooz');
+ expect(tooltipProperties[1].getHtmlDisplayValue()).toEqual('123');
+ });
+});
+
+describe('getImmutableSourceProperties', () => {
+ it('should only show immutable props', async () => {
+ const source = new MVTSingleLayerVectorSource(descriptor);
+ const properties = await source.getImmutableProperties();
+ expect(properties).toEqual([
+ { label: 'Data source', value: '.pbf vector tiles' },
+ { label: 'Url', value: 'https://example.com/{x}/{y}/{z}.pbf' },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts
deleted file mode 100644
index 03b91df22d3ca..0000000000000
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import uuid from 'uuid/v4';
-import { AbstractSource, ImmutableSourceProperty } from '../source';
-import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source';
-import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
-import { IField } from '../../fields/field';
-import { registerSource } from '../source_registry';
-import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters';
-import {
- MapExtent,
- TiledSingleLayerVectorSourceDescriptor,
- VectorSourceSyncMeta,
-} from '../../../../common/descriptor_types';
-import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor';
-import { ITooltipProperty } from '../../tooltips/tooltip_property';
-
-export const sourceTitle = i18n.translate(
- 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle',
- {
- defaultMessage: 'Vector Tile Layer',
- }
-);
-
-export class MVTSingleLayerVectorSource extends AbstractSource
- implements ITiledSingleLayerVectorSource {
- static createDescriptor({
- urlTemplate,
- layerName,
- minSourceZoom,
- maxSourceZoom,
- }: MVTSingleLayerVectorSourceConfig) {
- return {
- type: SOURCE_TYPES.MVT_SINGLE_LAYER,
- id: uuid(),
- urlTemplate,
- layerName,
- minSourceZoom: Math.max(MIN_ZOOM, minSourceZoom),
- maxSourceZoom: Math.min(MAX_ZOOM, maxSourceZoom),
- };
- }
-
- readonly _descriptor: TiledSingleLayerVectorSourceDescriptor;
-
- constructor(
- sourceDescriptor: TiledSingleLayerVectorSourceDescriptor,
- inspectorAdapters?: object
- ) {
- super(sourceDescriptor, inspectorAdapters);
- this._descriptor = sourceDescriptor;
- }
-
- renderSourceSettingsEditor() {
- return null;
- }
-
- getFieldNames(): string[] {
- return [];
- }
-
- getGeoJsonWithMeta(
- layerName: 'string',
- searchFilters: unknown[],
- registerCancelCallback: (callback: () => void) => void
- ): Promise {
- // todo: remove this method
- // This is a consequence of ITiledSingleLayerVectorSource extending IVectorSource.
- throw new Error('Does not implement getGeoJsonWithMeta');
- }
-
- async getFields(): Promise {
- return [];
- }
-
- async getImmutableProperties(): Promise {
- return [
- { label: getDataSourceLabel(), value: sourceTitle },
- { label: getUrlLabel(), value: this._descriptor.urlTemplate },
- {
- label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage', {
- defaultMessage: 'Layer name',
- }),
- value: this._descriptor.layerName,
- },
- {
- label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage', {
- defaultMessage: 'Min zoom',
- }),
- value: this._descriptor.minSourceZoom.toString(),
- },
- {
- label: i18n.translate('xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage', {
- defaultMessage: 'Max zoom',
- }),
- value: this._descriptor.maxSourceZoom.toString(),
- },
- ];
- }
-
- async getDisplayName(): Promise {
- return this._descriptor.layerName;
- }
-
- async getUrlTemplateWithMeta() {
- return {
- urlTemplate: this._descriptor.urlTemplate,
- layerName: this._descriptor.layerName,
- minSourceZoom: this._descriptor.minSourceZoom,
- maxSourceZoom: this._descriptor.maxSourceZoom,
- };
- }
-
- async getSupportedShapeTypes(): Promise {
- return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
- }
-
- canFormatFeatureProperties() {
- return false;
- }
-
- getMinZoom() {
- return this._descriptor.minSourceZoom;
- }
-
- getMaxZoom() {
- return this._descriptor.maxSourceZoom;
- }
-
- getBoundsForFilters(
- boundsFilters: BoundsFilters,
- registerCancelCallback: (requestToken: symbol, callback: () => void) => void
- ): MapExtent | null {
- return null;
- }
-
- getFieldByName(fieldName: string): IField | null {
- return null;
- }
-
- getSyncMeta(): VectorSourceSyncMeta {
- return null;
- }
-
- getApplyGlobalQuery(): boolean {
- return false;
- }
-
- async filterAndFormatPropertiesToHtml(properties: unknown): Promise {
- return [];
- }
-}
-
-registerSource({
- ConstructorFunction: MVTSingleLayerVectorSource,
- type: SOURCE_TYPES.MVT_SINGLE_LAYER,
-});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
new file mode 100644
index 0000000000000..ae28828dec5a8
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx
@@ -0,0 +1,222 @@
+/*
+ * 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';
+import uuid from 'uuid/v4';
+import React from 'react';
+import { GeoJsonProperties } from 'geojson';
+import { AbstractSource, ImmutableSourceProperty, SourceEditorArgs } from '../source';
+import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source';
+import {
+ FIELD_ORIGIN,
+ MAX_ZOOM,
+ MIN_ZOOM,
+ SOURCE_TYPES,
+ VECTOR_SHAPE_TYPE,
+} from '../../../../common/constants';
+import { registerSource } from '../source_registry';
+import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters';
+import {
+ MapExtent,
+ MVTFieldDescriptor,
+ TiledSingleLayerVectorSourceDescriptor,
+ VectorSourceSyncMeta,
+} from '../../../../common/descriptor_types';
+import { MVTField } from '../../fields/mvt_field';
+import { UpdateSourceEditor } from './update_source_editor';
+import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property';
+
+export const sourceTitle = i18n.translate(
+ 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle',
+ {
+ defaultMessage: '.pbf vector tiles',
+ }
+);
+
+export class MVTSingleLayerVectorSource extends AbstractSource
+ implements ITiledSingleLayerVectorSource {
+ static createDescriptor({
+ urlTemplate,
+ layerName,
+ minSourceZoom,
+ maxSourceZoom,
+ fields,
+ tooltipProperties,
+ }: Partial) {
+ return {
+ type: SOURCE_TYPES.MVT_SINGLE_LAYER,
+ id: uuid(),
+ urlTemplate: urlTemplate ? urlTemplate : '',
+ layerName: layerName ? layerName : '',
+ minSourceZoom:
+ typeof minSourceZoom === 'number' ? Math.max(MIN_ZOOM, minSourceZoom) : MIN_ZOOM,
+ maxSourceZoom:
+ typeof maxSourceZoom === 'number' ? Math.min(MAX_ZOOM, maxSourceZoom) : MAX_ZOOM,
+ fields: fields ? fields : [],
+ tooltipProperties: tooltipProperties ? tooltipProperties : [],
+ };
+ }
+
+ readonly _descriptor: TiledSingleLayerVectorSourceDescriptor;
+ readonly _tooltipFields: MVTField[];
+
+ constructor(
+ sourceDescriptor: TiledSingleLayerVectorSourceDescriptor,
+ inspectorAdapters?: object
+ ) {
+ super(sourceDescriptor, inspectorAdapters);
+ this._descriptor = MVTSingleLayerVectorSource.createDescriptor(sourceDescriptor);
+
+ this._tooltipFields = this._descriptor.tooltipProperties
+ .map((fieldName) => {
+ return this.getFieldByName(fieldName);
+ })
+ .filter((f) => f !== null) as MVTField[];
+ }
+
+ async supportsFitToBounds() {
+ return false;
+ }
+
+ renderSourceSettingsEditor({ onChange }: SourceEditorArgs) {
+ return (
+
+ );
+ }
+
+ getFieldNames(): string[] {
+ return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
+ return field.name;
+ });
+ }
+
+ getMVTFields(): MVTField[] {
+ return this._descriptor.fields.map((field: MVTFieldDescriptor) => {
+ return new MVTField({
+ fieldName: field.name,
+ type: field.type,
+ source: this,
+ origin: FIELD_ORIGIN.SOURCE,
+ });
+ });
+ }
+
+ getFieldByName(fieldName: string): MVTField | null {
+ try {
+ return this.createField({ fieldName });
+ } catch (e) {
+ return null;
+ }
+ }
+
+ createField({ fieldName }: { fieldName: string }): MVTField {
+ const field = this._descriptor.fields.find((f: MVTFieldDescriptor) => {
+ return f.name === fieldName;
+ });
+ if (!field) {
+ throw new Error(`Cannot create field for fieldName ${fieldName}`);
+ }
+ return new MVTField({
+ fieldName: field.name,
+ type: field.type,
+ source: this,
+ origin: FIELD_ORIGIN.SOURCE,
+ });
+ }
+
+ getGeoJsonWithMeta(
+ layerName: 'string',
+ searchFilters: unknown[],
+ registerCancelCallback: (callback: () => void) => void
+ ): Promise {
+ // Having this method here is a consequence of ITiledSingleLayerVectorSource extending IVectorSource.
+ throw new Error('Does not implement getGeoJsonWithMeta');
+ }
+
+ async getFields(): Promise {
+ return this.getMVTFields();
+ }
+
+ getLayerName(): string {
+ return this._descriptor.layerName;
+ }
+
+ async getImmutableProperties(): Promise {
+ return [
+ { label: getDataSourceLabel(), value: sourceTitle },
+ { label: getUrlLabel(), value: this._descriptor.urlTemplate },
+ ];
+ }
+
+ async getDisplayName(): Promise {
+ return this.getLayerName();
+ }
+
+ async getUrlTemplateWithMeta() {
+ return {
+ urlTemplate: this._descriptor.urlTemplate,
+ layerName: this._descriptor.layerName,
+ minSourceZoom: this._descriptor.minSourceZoom,
+ maxSourceZoom: this._descriptor.maxSourceZoom,
+ };
+ }
+
+ async getSupportedShapeTypes(): Promise {
+ return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON];
+ }
+
+ canFormatFeatureProperties() {
+ return !!this._tooltipFields.length;
+ }
+
+ getMinZoom() {
+ return this._descriptor.minSourceZoom;
+ }
+
+ getMaxZoom() {
+ return this._descriptor.maxSourceZoom;
+ }
+
+ getBoundsForFilters(
+ boundsFilters: BoundsFilters,
+ registerCancelCallback: (requestToken: symbol, callback: () => void) => void
+ ): MapExtent | null {
+ return null;
+ }
+
+ getSyncMeta(): VectorSourceSyncMeta {
+ return null;
+ }
+
+ getApplyGlobalQuery(): boolean {
+ return false;
+ }
+
+ async filterAndFormatPropertiesToHtml(
+ properties: GeoJsonProperties,
+ featureId?: string | number
+ ): Promise {
+ const tooltips = [];
+ for (const key in properties) {
+ if (properties.hasOwnProperty(key)) {
+ for (let i = 0; i < this._tooltipFields.length; i++) {
+ const mvtField = this._tooltipFields[i];
+ if (mvtField.getName() === key) {
+ const tooltip = new TooltipProperty(key, key, properties[key]);
+ tooltips.push(tooltip);
+ break;
+ }
+ }
+ }
+ }
+ return tooltips;
+ }
+}
+
+registerSource({
+ ConstructorFunction: MVTSingleLayerVectorSource,
+ type: SOURCE_TYPES.MVT_SINGLE_LAYER,
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx
new file mode 100644
index 0000000000000..986756f840014
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.test.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('../../../kibana_services', () => ({}));
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { MVTSingleLayerVectorSourceEditor } from './mvt_single_layer_vector_source_editor';
+
+test('should render source creation editor (fields should _not_ be included)', async () => {
+ const component = shallow( {}} />);
+
+ expect(component).toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx
index 760b8c676cb37..49487e96a4544 100644
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx
@@ -10,17 +10,14 @@ import _ from 'lodash';
import { EuiFieldText, EuiFormRow, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
-import { ValidatedDualRange, Value } from '../../../../../../../src/plugins/kibana_react/public';
+import {
+ MVTFieldDescriptor,
+ TiledSingleLayerVectorSourceSettings,
+} from '../../../../common/descriptor_types';
+import { MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings';
-export type MVTSingleLayerVectorSourceConfig = {
- urlTemplate: string;
- layerName: string;
- minSourceZoom: number;
- maxSourceZoom: number;
-};
-
-export interface Props {
- onSourceConfigChange: (sourceConfig: MVTSingleLayerVectorSourceConfig) => void;
+interface Props {
+ onSourceConfigChange: (sourceConfig: TiledSingleLayerVectorSourceSettings) => void;
}
interface State {
@@ -28,6 +25,7 @@ interface State {
layerName: string;
minSourceZoom: number;
maxSourceZoom: number;
+ fields?: MVTFieldDescriptor[];
}
export class MVTSingleLayerVectorSourceEditor extends Component {
@@ -36,6 +34,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component {
layerName: '',
minSourceZoom: MIN_ZOOM,
maxSourceZoom: MAX_ZOOM,
+ fields: [],
};
_sourceConfigChange = _.debounce(() => {
@@ -50,6 +49,7 @@ export class MVTSingleLayerVectorSourceEditor extends Component {
layerName: this.state.layerName,
minSourceZoom: this.state.minSourceZoom,
maxSourceZoom: this.state.maxSourceZoom,
+ fields: this.state.fields,
});
}
}, 200);
@@ -64,23 +64,13 @@ export class MVTSingleLayerVectorSourceEditor extends Component {
);
};
- _handleLayerNameInputChange = (e: ChangeEvent) => {
- const layerName = e.target.value;
- this.setState(
- {
- layerName,
- },
- () => this._sourceConfigChange()
- );
- };
-
- _handleZoomRangeChange = (e: Value) => {
- const minSourceZoom = parseInt(e[0] as string, 10);
- const maxSourceZoom = parseInt(e[1] as string, 10);
-
- if (this.state.minSourceZoom !== minSourceZoom || this.state.maxSourceZoom !== maxSourceZoom) {
- this.setState({ minSourceZoom, maxSourceZoom }, () => this._sourceConfigChange());
- }
+ _handleChange = (state: {
+ layerName: string;
+ fields: MVTFieldDescriptor[];
+ minSourceZoom: number;
+ maxSourceZoom: number;
+ }) => {
+ this.setState(state, () => this._sourceConfigChange());
};
render() {
@@ -90,37 +80,30 @@ export class MVTSingleLayerVectorSourceEditor extends Component {
label={i18n.translate('xpack.maps.source.MVTSingleLayerVectorSourceEditor.urlMessage', {
defaultMessage: 'Url',
})}
- >
-
-
-
-
+
-
);
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts
new file mode 100644
index 0000000000000..599eaea73c9a0
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/types.ts
@@ -0,0 +1,16 @@
+/*
+ * 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 { MVTFieldDescriptor } from '../../../../common/descriptor_types';
+
+export interface MVTSingleLayerVectorSourceConfig {
+ urlTemplate: string;
+ layerName: string;
+ minSourceZoom: number;
+ maxSourceZoom: number;
+ fields?: MVTFieldDescriptor[];
+ tooltipProperties?: string[];
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.tsx
new file mode 100644
index 0000000000000..fd19379058e3b
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.test.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.
+ */
+
+jest.mock('../../../kibana_services', () => ({}));
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { UpdateSourceEditor } from './update_source_editor';
+import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source';
+import { TiledSingleLayerVectorSourceDescriptor } from '../../../../common/descriptor_types';
+import { SOURCE_TYPES } from '../../../../common/constants';
+
+const descriptor: TiledSingleLayerVectorSourceDescriptor = {
+ type: SOURCE_TYPES.MVT_SINGLE_LAYER,
+ urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf',
+ layerName: 'foobar',
+ minSourceZoom: 4,
+ maxSourceZoom: 14,
+ fields: [],
+ tooltipProperties: [],
+};
+
+test('should render update source editor (fields _should_ be included)', async () => {
+ const source = new MVTSingleLayerVectorSource(descriptor);
+
+ const component = shallow(
+ {}} />
+ );
+
+ expect(component).toMatchSnapshot();
+});
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx
new file mode 100644
index 0000000000000..a959912718197
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx
@@ -0,0 +1,136 @@
+/*
+ * 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, { Component, Fragment } from 'react';
+import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { TooltipSelector } from '../../../components/tooltip_selector';
+import { MVTField } from '../../fields/mvt_field';
+import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source';
+import { MVTSettings, MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings';
+import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view';
+import { MVTFieldDescriptor } from '../../../../common/descriptor_types';
+
+interface Props {
+ tooltipFields: MVTField[];
+ onChange: (...args: OnSourceChangeArgs[]) => void;
+ source: MVTSingleLayerVectorSource;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+interface State {}
+
+export class UpdateSourceEditor extends Component {
+ _onTooltipPropertiesSelect = (propertyNames: string[]) => {
+ this.props.onChange({ propName: 'tooltipProperties', value: propertyNames });
+ };
+
+ _handleChange = (settings: MVTSettings) => {
+ const changes: OnSourceChangeArgs[] = [];
+ if (settings.layerName !== this.props.source.getLayerName()) {
+ changes.push({ propName: 'layerName', value: settings.layerName });
+ }
+ if (settings.minSourceZoom !== this.props.source.getMinZoom()) {
+ changes.push({ propName: 'minSourceZoom', value: settings.minSourceZoom });
+ }
+ if (settings.maxSourceZoom !== this.props.source.getMaxZoom()) {
+ changes.push({ propName: 'maxSourceZoom', value: settings.maxSourceZoom });
+ }
+ if (!_.isEqual(settings.fields, this._getFieldDescriptors())) {
+ changes.push({ propName: 'fields', value: settings.fields });
+
+ // Remove dangling tooltips.
+ // This behaves similar to how stale styling properties are removed (e.g. on metric-change in agg sources)
+ const sanitizedTooltips = [];
+ for (let i = 0; i < this.props.tooltipFields.length; i++) {
+ const tooltipName = this.props.tooltipFields[i].getName();
+ for (let j = 0; j < settings.fields.length; j++) {
+ if (settings.fields[j].name === tooltipName) {
+ sanitizedTooltips.push(tooltipName);
+ break;
+ }
+ }
+ }
+
+ if (!_.isEqual(sanitizedTooltips, this.props.tooltipFields)) {
+ changes.push({ propName: 'tooltipProperties', value: sanitizedTooltips });
+ }
+ }
+ this.props.onChange(...changes);
+ };
+
+ _getFieldDescriptors(): MVTFieldDescriptor[] {
+ return this.props.source.getMVTFields().map((field: MVTField) => {
+ return field.getMVTFieldDescriptor();
+ });
+ }
+
+ _renderSourceSettingsCard() {
+ const fieldDescriptors: MVTFieldDescriptor[] = this._getFieldDescriptors();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ _renderTooltipSelectionCard() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+
+ {this._renderSourceSettingsCard()}
+ {this._renderTooltipSelectionCard()}
+
+ );
+ }
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts
index f937eac336532..c68e22ada8b0c 100644
--- a/x-pack/plugins/maps/public/classes/sources/source.ts
+++ b/x-pack/plugins/maps/public/classes/sources/source.ts
@@ -17,7 +17,7 @@ import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view';
export type SourceEditorArgs = {
- onChange: (args: OnSourceChangeArgs) => void;
+ onChange: (...args: OnSourceChangeArgs[]) => void;
};
export type ImmutableSourceProperty = {
diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts
index 99a7478cd8362..42993bf36f618 100644
--- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts
+++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts
@@ -5,7 +5,7 @@
*/
/* eslint-disable @typescript-eslint/consistent-type-definitions */
-import { FeatureCollection } from 'geojson';
+import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
import { Filter, TimeRange } from 'src/plugins/data/public';
import { AbstractSource, ISource } from '../source';
import { IField } from '../../fields/field';
@@ -35,7 +35,7 @@ export type BoundsFilters = {
};
export interface IVectorSource extends ISource {
- filterAndFormatPropertiesToHtml(properties: unknown): Promise;
+ filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
@@ -51,10 +51,12 @@ export interface IVectorSource extends ISource {
getSyncMeta(): VectorSourceSyncMeta;
getFieldNames(): string[];
getApplyGlobalQuery(): boolean;
+ createField({ fieldName }: { fieldName: string }): IField;
+ canFormatFeatureProperties(): boolean;
}
export class AbstractVectorSource extends AbstractSource implements IVectorSource {
- filterAndFormatPropertiesToHtml(properties: unknown): Promise;
+ filterAndFormatPropertiesToHtml(properties: GeoJsonProperties): Promise;
getBoundsForFilters(
boundsFilters: BoundsFilters,
registerCancelCallback: (requestToken: symbol, callback: () => void) => void
@@ -72,6 +74,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc
canFormatFeatureProperties(): boolean;
getApplyGlobalQuery(): boolean;
getFieldNames(): string[];
+ createField({ fieldName }: { fieldName: string }): IField;
}
export interface ITiledSingleLayerVectorSource extends IVectorSource {
@@ -83,4 +86,5 @@ export interface ITiledSingleLayerVectorSource extends IVectorSource {
}>;
getMinZoom(): number;
getMaxZoom(): number;
+ getLayerName(): string;
}
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js
index b7a80562f10ca..fe2f302504a15 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js
@@ -89,7 +89,7 @@ export class ColorMapSelect extends Component {
};
_renderColorStopsInput() {
- if (!this.props.useCustomColorMap) {
+ if (!this.props.isCustomOnly && !this.props.useCustomColorMap) {
return null;
}
@@ -102,7 +102,7 @@ export class ColorMapSelect extends Component {
swatches={this.props.swatches}
/>
);
- } else
+ } else {
colorStopEditor = (
);
+ }
return (
@@ -121,6 +122,10 @@ export class ColorMapSelect extends Component {
}
_renderColorMapSelections() {
+ if (this.props.isCustomOnly) {
+ return null;
+ }
+
const colorMapOptionsWithCustom = [
{
value: CUSTOM_COLOR_MAP,
@@ -146,19 +151,22 @@ export class ColorMapSelect extends Component {
) : null;
return (
-
- {toggle}
-
-
-
-
+
+
+ {toggle}
+
+
+
+
+
+
);
}
@@ -166,7 +174,6 @@ export class ColorMapSelect extends Component {
return (
{this._renderColorMapSelections()}
-
{this._renderColorStopsInput()}
);
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js
index fa13e1cf66664..90070343a1b48 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js
@@ -90,6 +90,7 @@ export function DynamicColorForm({
if (styleProperty.isOrdinal()) {
return (
{
+ const field = fields.find((field) => {
return field.name === selectedFieldName;
});
+ //Do not spread in all the other unused values (e.g. type, supportsAutoDomain etc...)
+ if (field) {
+ selectedOption = {
+ value: field.value,
+ label: field.label,
+ };
+ }
}
return (
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js
index e285d91dcd7a4..e4dc9d1b4d8f6 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js
@@ -46,19 +46,16 @@ export class StyleMapSelect extends Component {
};
_renderCustomStopsInput() {
- if (!this.props.useCustomMap) {
+ return !this.props.isCustomOnly && !this.props.useCustomMap
+ ? null
+ : this.props.renderCustomStopsInput(this._onCustomMapChange);
+ }
+
+ _renderMapSelect() {
+ if (this.props.isCustomOnly) {
return null;
}
- return (
-
-
- {this.props.renderCustomStopsInput(this._onCustomMapChange)}
-
- );
- }
-
- render() {
const mapOptionsWithCustom = [
{
value: CUSTOM_MAP,
@@ -87,6 +84,15 @@ export class StyleMapSelect extends Component {
hasDividers={true}
compressed
/>
+
+
+ );
+ }
+
+ render() {
+ return (
+
+ {this._renderMapSelect()}
{this._renderCustomStopsInput()}
);
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js
index f9f8a67846470..e3724d42a783b 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js
@@ -36,17 +36,20 @@ export function DynamicIconForm({
};
function renderIconMapSelect() {
- if (!styleOptions.field || !styleOptions.field.name) {
+ const field = styleProperty.getField();
+ if (!field) {
return null;
}
return (
);
}
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js
index 08f5dfe4f4ba0..6cfe656d65a1e 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js
@@ -8,8 +8,8 @@ import React from 'react';
import { StyleMapSelect } from '../style_map_select';
import { i18n } from '@kbn/i18n';
-import { getIconPaletteOptions } from '../../symbol_utils';
import { IconStops } from './icon_stops';
+import { getIconPaletteOptions } from '../../symbol_utils';
export function IconMapSelect({
customIconStops,
@@ -19,6 +19,7 @@ export function IconMapSelect({
styleProperty,
symbolOptions,
useCustomIconMap,
+ isCustomOnly,
}) {
function onMapSelectChange({ customMapStops, selectedMapId, useCustomMap }) {
onChange({
@@ -52,6 +53,7 @@ export function IconMapSelect({
useCustomMap={useCustomIconMap}
selectedMapId={iconPaletteId}
renderCustomStopsInput={renderCustomIconStopsInput}
+ isCustomOnly={isCustomOnly}
/>
);
}
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js
index 7856a4ddaff39..6528648eff552 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js
@@ -62,6 +62,7 @@ export class VectorStyleEditor extends Component {
name: field.getName(),
origin: field.getOrigin(),
type: await field.getDataType(),
+ supportsAutoDomain: field.supportsAutoDomain(),
};
};
@@ -109,7 +110,9 @@ export class VectorStyleEditor extends Component {
}
_getOrdinalFields() {
- return [...this.state.dateFields, ...this.state.numberFields];
+ return [...this.state.dateFields, ...this.state.numberFields].filter((field) => {
+ return field.supportsAutoDomain;
+ });
}
_handleSelectedFeatureChange = (selectedFeature) => {
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js
index ae4d935e2457b..763eb81ad0f98 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js
@@ -10,11 +10,10 @@ import { VECTOR_STYLES } from '../../../../../common/constants';
export class DynamicOrientationProperty extends DynamicStyleProperty {
syncIconRotationWithMb(symbolLayerId, mbMap) {
- if (this._options.field && this._options.field.name) {
- const targetName = getComputedFieldName(
- VECTOR_STYLES.ICON_ORIENTATION,
- this._options.field.name
- );
+ if (this._field && this._field.isValid()) {
+ const targetName = this._field.supportsAutoDomain()
+ ? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName())
+ : this._field.getName();
// Using property state instead of feature-state because layout properties do not support feature-state
mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]);
} else {
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js
index de868f3f92650..a7a3130875a95 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js
@@ -10,7 +10,11 @@ import { getComputedFieldName } from '../style_util';
export class DynamicTextProperty extends DynamicStyleProperty {
syncTextFieldWithMb(mbLayerId, mbMap) {
if (this._field && this._field.isValid()) {
- const targetName = getComputedFieldName(this._styleName, this._options.field.name);
+ // Fields that support auto-domain are normalized with a field-formatter and stored into a computed-field
+ // Otherwise, the raw value is just carried over and no computed field is created.
+ const targetName = this._field.supportsAutoDomain()
+ ? getComputedFieldName(this._styleName, this.getFieldName())
+ : this._field.getName();
mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']);
} else {
mbMap.setLayoutProperty(mbLayerId, 'text-field', null);
diff --git a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts
index 7149fe29f90ec..7bb79d8d341d3 100644
--- a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts
+++ b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts
@@ -19,7 +19,7 @@ export interface ITooltipProperty {
export interface LoadFeatureProps {
layerId: string;
- featureId: number;
+ featureId?: number | string;
}
export interface FeatureGeometry {
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js
index 14252dcfc067d..557fe5fd5f705 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js
@@ -43,16 +43,16 @@ export class LayerPanel extends React.Component {
componentDidMount() {
this._isMounted = true;
- this.loadDisplayName();
- this.loadImmutableSourceProperties();
- this.loadLeftJoinFields();
+ this._loadDisplayName();
+ this._loadImmutableSourceProperties();
+ this._loadLeftJoinFields();
}
componentWillUnmount() {
this._isMounted = false;
}
- loadDisplayName = async () => {
+ _loadDisplayName = async () => {
if (!this.props.selectedLayer) {
return;
}
@@ -63,7 +63,7 @@ export class LayerPanel extends React.Component {
}
};
- loadImmutableSourceProperties = async () => {
+ _loadImmutableSourceProperties = async () => {
if (!this.props.selectedLayer) {
return;
}
@@ -74,7 +74,7 @@ export class LayerPanel extends React.Component {
}
};
- async loadLeftJoinFields() {
+ async _loadLeftJoinFields() {
if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) {
return;
}
@@ -97,8 +97,11 @@ export class LayerPanel extends React.Component {
}
}
- _onSourceChange = ({ propName, value, newLayerType }) => {
- this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType);
+ _onSourceChange = (...args) => {
+ for (let i = 0; i < args.length; i++) {
+ const { propName, value, newLayerType } = args[i];
+ this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType);
+ }
};
_renderFilterSection() {
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js
index 362186a8f5549..5e2a153b2ccbf 100644
--- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js
+++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js
@@ -31,14 +31,15 @@ export class FeatureProperties extends React.Component {
this._isMounted = false;
}
- _loadProperties = () => {
+ _loadProperties = async () => {
this._fetchProperties({
nextFeatureId: this.props.featureId,
nextLayerId: this.props.layerId,
+ mbProperties: this.props.mbProperties,
});
};
- _fetchProperties = async ({ nextLayerId, nextFeatureId }) => {
+ _fetchProperties = async ({ nextLayerId, nextFeatureId, mbProperties }) => {
if (this.prevLayerId === nextLayerId && this.prevFeatureId === nextFeatureId) {
// do not reload same feature properties
return;
@@ -64,6 +65,7 @@ export class FeatureProperties extends React.Component {
properties = await this.props.loadFeatureProperties({
layerId: nextLayerId,
featureId: nextFeatureId,
+ mbProperties: mbProperties,
});
} catch (error) {
if (this._isMounted) {
diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js
index e5b97947602b0..d91bc8e803ab9 100644
--- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js
+++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js
@@ -132,6 +132,7 @@ export class FeaturesTooltip extends React.Component {
{
sinon.assert.notCalled(closeOnClickTooltipStub);
sinon.assert.calledWith(openOnClickTooltipStub, {
- features: [{ id: 1, layerId: 'tfi3f' }],
+ features: [{ id: 1, layerId: 'tfi3f', mbProperties: { __kbn__feature_id__: 1 } }],
location: [100, 30],
});
});
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
index 03c2aeb2edd0a..6c42057680408 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
+++ b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js
@@ -58,7 +58,7 @@ export class TooltipPopover extends Component {
// Mapbox feature geometry is from vector tile and is not the same as the original geometry.
_loadFeatureGeometry = ({ layerId, featureId }) => {
const tooltipLayer = this._findLayerById(layerId);
- if (!tooltipLayer) {
+ if (!tooltipLayer || typeof featureId === 'undefined') {
return null;
}
@@ -70,22 +70,24 @@ export class TooltipPopover extends Component {
return targetFeature.geometry;
};
- _loadFeatureProperties = async ({ layerId, featureId }) => {
+ _loadFeatureProperties = async ({ layerId, featureId, mbProperties }) => {
const tooltipLayer = this._findLayerById(layerId);
if (!tooltipLayer) {
return [];
}
- const targetFeature = tooltipLayer.getFeatureById(featureId);
- if (!targetFeature) {
- return [];
+ let targetFeature;
+ if (typeof featureId !== 'undefined') {
+ targetFeature = tooltipLayer.getFeatureById(featureId);
}
- return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties);
+
+ const properties = targetFeature ? targetFeature.properties : mbProperties;
+ return await tooltipLayer.getPropertiesForTooltip(properties);
};
_loadPreIndexedShape = async ({ layerId, featureId }) => {
const tooltipLayer = this._findLayerById(layerId);
- if (!tooltipLayer) {
+ if (!tooltipLayer || typeof featureId === 'undefined') {
return null;
}
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index 9539d530bab04..9d5125532e5b8 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -7,7 +7,7 @@
import React, { FC } from 'react';
import ReactDOM from 'react-dom';
-import { AppMountParameters, CoreStart } from 'kibana/public';
+import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
@@ -17,6 +17,8 @@ import { setLicenseCache } from './license';
import { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { MlRouter } from './routing';
+import { mlApiServicesProvider } from './services/ml_api_service';
+import { HttpService } from './services/http_service';
type MlDependencies = MlSetupDependencies & MlStartDependencies;
@@ -27,6 +29,23 @@ interface AppProps {
const localStorage = new Storage(window.localStorage);
+/**
+ * Provides global services available across the entire ML app.
+ */
+export function getMlGlobalServices(httpStart: HttpStart) {
+ const httpService = new HttpService(httpStart);
+ return {
+ httpService,
+ mlApiServices: mlApiServicesProvider(httpService),
+ };
+}
+
+export interface MlServicesContext {
+ mlServices: MlGlobalServices;
+}
+
+export type MlGlobalServices = ReturnType;
+
const App: FC = ({ coreStart, deps }) => {
const pageDeps = {
indexPatterns: deps.data.indexPatterns,
@@ -47,7 +66,9 @@ const App: FC = ({ coreStart, deps }) => {
const I18nContext = coreStart.i18n.Context;
return (
-
+
diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
index 3fb654f35be4d..803281bcd0ce9 100644
--- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
@@ -27,7 +27,6 @@ import {
normalizeTimes,
} from './job_select_service_utils';
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
-import { ml } from '../../services/ml_api_service';
import { useMlKibana } from '../../contexts/kibana';
import { JobSelectionMaps } from './job_selector';
@@ -66,7 +65,10 @@ export const JobSelectorFlyout: FC = ({
withTimeRangeSelector = true,
}) => {
const {
- services: { notifications },
+ services: {
+ notifications,
+ mlServices: { mlApiServices },
+ },
} = useMlKibana();
const [newSelection, setNewSelection] = useState(selectedIds);
@@ -151,7 +153,7 @@ export const JobSelectorFlyout: FC = ({
async function fetchJobs() {
try {
- const resp = await ml.jobs.jobsWithTimerange(dateFormatTz);
+ const resp = await mlApiServices.jobs.jobsWithTimerange(dateFormatTz);
const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH);
const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs);
setJobs(normalizedJobs);
diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx
index 27f8c822d68e3..beafae1ecd2f6 100644
--- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx
+++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx
@@ -9,10 +9,7 @@ import { Subscription } from 'rxjs';
import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui';
import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public';
-import {
- mlTimefilterRefresh$,
- mlTimefilterTimeChange$,
-} from '../../../services/timefilter_refresh_service';
+import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
import { useUrlState } from '../../../util/url_state';
import { useMlKibana } from '../../../contexts/kibana';
@@ -108,7 +105,6 @@ export const DatePickerWrapper: FC = () => {
timefilter.setTime(newTime);
setTime(newTime);
setRecentlyUsedRanges(getRecentlyUsedRanges());
- mlTimefilterTimeChange$.next({ lastRefresh: Date.now(), timeRange: { start, end } });
}
function updateInterval({
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
index 2a156b5716ad4..3bc3b8c2c6dfd 100644
--- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
@@ -13,6 +13,7 @@ import {
import { SecurityPluginSetup } from '../../../../../security/public';
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
+import { MlServicesContext } from '../../app';
interface StartPlugins {
data: DataPublicPluginStart;
@@ -20,7 +21,8 @@ interface StartPlugins {
licenseManagement?: LicenseManagementUIPluginSetup;
share: SharePluginStart;
}
-export type StartServices = CoreStart & StartPlugins & { kibanaVersion: string };
+export type StartServices = CoreStart &
+ StartPlugins & { kibanaVersion: string } & MlServicesContext;
// eslint-disable-next-line react-hooks/rules-of-hooks
export const useMlKibana = () => useKibana();
export type MlKibanaReactContextValue = KibanaReactContextValue;
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
index 07d5a153664b7..95ef5e5b2938c 100644
--- a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
@@ -7,6 +7,7 @@
import React from 'react';
import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
import { SavedSearchSavedObject } from '../../../../common/types/kibana';
+import { MlServicesContext } from '../../app';
export interface MlContextValue {
combinedQuery: any;
@@ -34,4 +35,4 @@ export type SavedSearchQuery = object;
// Multiple custom hooks can be created to access subsets of
// the overall context value if necessary too,
// see useCurrentIndexPattern() for example.
-export const MlContext = React.createContext>({});
+export const MlContext = React.createContext>({});
diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss
index 7e5f354bbb402..63c471e66c49a 100644
--- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss
+++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss
@@ -1,3 +1,5 @@
+$borderRadius: $euiBorderRadius / 2;
+
.ml-swimlane-selector {
visibility: hidden;
}
@@ -104,10 +106,9 @@
// SASSTODO: This entire selector needs to be rewritten.
// It looks extremely brittle with very specific sizing units
- .ml-explorer-swimlane {
+ .mlExplorerSwimlane {
user-select: none;
padding: 0;
- margin-bottom: $euiSizeS;
line.gridLine {
stroke: $euiBorderColor;
@@ -218,17 +219,20 @@
div.lane {
height: 30px;
border-bottom: 0px;
- border-radius: 2px;
- margin-top: -1px;
+ border-radius: $borderRadius;
white-space: nowrap;
+ &:not(:first-child) {
+ margin-top: -1px;
+ }
+
div.lane-label {
display: inline-block;
- font-size: 13px;
+ font-size: $euiFontSizeXS;
height: 30px;
text-align: right;
vertical-align: middle;
- border-radius: 2px;
+ border-radius: $borderRadius;
padding-right: 5px;
margin-right: 5px;
border: 1px solid transparent;
@@ -261,7 +265,7 @@
.sl-cell-inner-dragselect {
height: 26px;
margin: 1px;
- border-radius: 2px;
+ border-radius: $borderRadius;
text-align: center;
}
@@ -293,7 +297,7 @@
.sl-cell-inner,
.sl-cell-inner-dragselect {
border: 2px solid $euiColorDarkShade;
- border-radius: 2px;
+ border-radius: $borderRadius;
opacity: 1;
}
}
diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts
index 590a69283a819..095b42ffac5b7 100644
--- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts
+++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts
@@ -11,6 +11,7 @@ import useObservable from 'react-use/lib/useObservable';
import { forkJoin, of, Observable, Subject } from 'rxjs';
import { mergeMap, switchMap, tap } from 'rxjs/operators';
+import { useCallback, useMemo } from 'react';
import { anomalyDataChange } from '../explorer_charts/explorer_charts_container_service';
import { explorerService } from '../explorer_dashboard_service';
import {
@@ -22,15 +23,17 @@ import {
loadAnomaliesTableData,
loadDataForCharts,
loadFilteredTopInfluencers,
- loadOverallData,
loadTopInfluencers,
- loadViewBySwimlane,
- loadViewByTopFieldValuesForSelectedTime,
AppStateSelectedCells,
ExplorerJob,
TimeRangeBounds,
} from '../explorer_utils';
import { ExplorerState } from '../reducers';
+import { useMlKibana, useTimefilter } from '../../contexts/kibana';
+import { AnomalyTimelineService } from '../../services/anomaly_timeline_service';
+import { mlResultsServiceProvider } from '../../services/results_service';
+import { isViewBySwimLaneData } from '../swimlane_container';
+import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants';
// Memoize the data fetching methods.
// wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument
@@ -39,13 +42,13 @@ import { ExplorerState } from '../reducers';
// about this parameter. The generic type T retains and returns the type information of
// the original function.
const memoizeIsEqual = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs);
-const wrapWithLastRefreshArg = any>(func: T) => {
+const wrapWithLastRefreshArg = any>(func: T, context: any = null) => {
return function (lastRefresh: number, ...args: Parameters): ReturnType {
- return func.apply(null, args);
+ return func.apply(context, args);
};
};
-const memoize = any>(func: T) => {
- return memoizeOne(wrapWithLastRefreshArg(func), memoizeIsEqual);
+const memoize = any>(func: T, context?: any) => {
+ return memoizeOne(wrapWithLastRefreshArg(func, context), memoizeIsEqual);
};
const memoizedAnomalyDataChange = memoize(anomalyDataChange);
@@ -56,9 +59,7 @@ const memoizedLoadDataForCharts = memoize(loadDataForC
const memoizedLoadFilteredTopInfluencers = memoize(
loadFilteredTopInfluencers
);
-const memoizedLoadOverallData = memoize(loadOverallData);
const memoizedLoadTopInfluencers = memoize(loadTopInfluencers);
-const memoizedLoadViewBySwimlane = memoize(loadViewBySwimlane);
const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData);
export interface LoadExplorerDataConfig {
@@ -73,6 +74,9 @@ export interface LoadExplorerDataConfig {
tableInterval: string;
tableSeverity: number;
viewBySwimlaneFieldName: string;
+ viewByFromPage: number;
+ viewByPerPage: number;
+ swimlaneContainerWidth: number;
}
export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => {
@@ -87,183 +91,213 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi
/**
* Fetches the data necessary for the Anomaly Explorer using observables.
- *
- * @param config LoadExplorerDataConfig
- *
- * @return Partial
*/
-function loadExplorerData(config: LoadExplorerDataConfig): Observable> {
- if (!isLoadExplorerDataConfig(config)) {
- return of({});
- }
-
- const {
- bounds,
- lastRefresh,
- influencersFilterQuery,
- noInfluencersConfigured,
- selectedCells,
- selectedJobs,
- swimlaneBucketInterval,
- swimlaneLimit,
- tableInterval,
- tableSeverity,
- viewBySwimlaneFieldName,
- } = config;
-
- const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
- const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
- const timerange = getSelectionTimeRange(
- selectedCells,
- swimlaneBucketInterval.asSeconds(),
- bounds
+const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService) => {
+ const memoizedLoadOverallData = memoize(
+ anomalyTimelineService.loadOverallData,
+ anomalyTimelineService
);
+ const memoizedLoadViewBySwimlane = memoize(
+ anomalyTimelineService.loadViewBySwimlane,
+ anomalyTimelineService
+ );
+ return (config: LoadExplorerDataConfig): Observable> => {
+ if (!isLoadExplorerDataConfig(config)) {
+ return of({});
+ }
- const dateFormatTz = getDateFormatTz();
-
- // First get the data where we have all necessary args at hand using forkJoin:
- // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues
- return forkJoin({
- annotationsData: memoizedLoadAnnotationsTableData(
+ const {
+ bounds,
lastRefresh,
+ influencersFilterQuery,
+ noInfluencersConfigured,
selectedCells,
selectedJobs,
+ swimlaneBucketInterval,
+ swimlaneLimit,
+ tableInterval,
+ tableSeverity,
+ viewBySwimlaneFieldName,
+ swimlaneContainerWidth,
+ viewByFromPage,
+ viewByPerPage,
+ } = config;
+
+ const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
+ const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
+ const timerange = getSelectionTimeRange(
+ selectedCells,
swimlaneBucketInterval.asSeconds(),
bounds
- ),
- anomalyChartRecords: memoizedLoadDataForCharts(
- lastRefresh,
- jobIds,
- timerange.earliestMs,
- timerange.latestMs,
- selectionInfluencers,
- selectedCells,
- influencersFilterQuery
- ),
- influencers:
- selectionInfluencers.length === 0
- ? memoizedLoadTopInfluencers(
+ );
+
+ const dateFormatTz = getDateFormatTz();
+
+ // First get the data where we have all necessary args at hand using forkJoin:
+ // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues
+ return forkJoin({
+ annotationsData: memoizedLoadAnnotationsTableData(
+ lastRefresh,
+ selectedCells,
+ selectedJobs,
+ swimlaneBucketInterval.asSeconds(),
+ bounds
+ ),
+ anomalyChartRecords: memoizedLoadDataForCharts(
+ lastRefresh,
+ jobIds,
+ timerange.earliestMs,
+ timerange.latestMs,
+ selectionInfluencers,
+ selectedCells,
+ influencersFilterQuery
+ ),
+ influencers:
+ selectionInfluencers.length === 0
+ ? memoizedLoadTopInfluencers(
+ lastRefresh,
+ jobIds,
+ timerange.earliestMs,
+ timerange.latestMs,
+ [],
+ noInfluencersConfigured,
+ influencersFilterQuery
+ )
+ : Promise.resolve({}),
+ overallState: memoizedLoadOverallData(lastRefresh, selectedJobs, swimlaneContainerWidth),
+ tableData: memoizedLoadAnomaliesTableData(
+ lastRefresh,
+ selectedCells,
+ selectedJobs,
+ dateFormatTz,
+ swimlaneBucketInterval.asSeconds(),
+ bounds,
+ viewBySwimlaneFieldName,
+ tableInterval,
+ tableSeverity,
+ influencersFilterQuery
+ ),
+ topFieldValues:
+ selectedCells !== undefined && selectedCells.showTopFieldValues === true
+ ? anomalyTimelineService.loadViewByTopFieldValuesForSelectedTime(
+ timerange.earliestMs,
+ timerange.latestMs,
+ selectedJobs,
+ viewBySwimlaneFieldName,
+ swimlaneLimit,
+ viewByPerPage,
+ viewByFromPage,
+ swimlaneContainerWidth
+ )
+ : Promise.resolve([]),
+ }).pipe(
+ // Trigger a side-effect action to reset view-by swimlane,
+ // show the view-by loading indicator
+ // and pass on the data we already fetched.
+ tap(explorerService.setViewBySwimlaneLoading),
+ // Trigger a side-effect to update the charts.
+ tap(({ anomalyChartRecords }) => {
+ if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
+ memoizedAnomalyDataChange(
lastRefresh,
- jobIds,
+ anomalyChartRecords,
timerange.earliestMs,
timerange.latestMs,
+ tableSeverity
+ );
+ } else {
+ memoizedAnomalyDataChange(
+ lastRefresh,
[],
- noInfluencersConfigured,
- influencersFilterQuery
- )
- : Promise.resolve({}),
- overallState: memoizedLoadOverallData(
- lastRefresh,
- selectedJobs,
- swimlaneBucketInterval,
- bounds
- ),
- tableData: memoizedLoadAnomaliesTableData(
- lastRefresh,
- selectedCells,
- selectedJobs,
- dateFormatTz,
- swimlaneBucketInterval.asSeconds(),
- bounds,
- viewBySwimlaneFieldName,
- tableInterval,
- tableSeverity,
- influencersFilterQuery
- ),
- topFieldValues:
- selectedCells !== undefined && selectedCells.showTopFieldValues === true
- ? loadViewByTopFieldValuesForSelectedTime(
timerange.earliestMs,
timerange.latestMs,
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- noInfluencersConfigured
- )
- : Promise.resolve([]),
- }).pipe(
- // Trigger a side-effect action to reset view-by swimlane,
- // show the view-by loading indicator
- // and pass on the data we already fetched.
- tap(explorerService.setViewBySwimlaneLoading),
- // Trigger a side-effect to update the charts.
- tap(({ anomalyChartRecords }) => {
- if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
- memoizedAnomalyDataChange(
- lastRefresh,
- anomalyChartRecords,
- timerange.earliestMs,
- timerange.latestMs,
- tableSeverity
- );
- } else {
- memoizedAnomalyDataChange(
- lastRefresh,
- [],
- timerange.earliestMs,
- timerange.latestMs,
- tableSeverity
- );
- }
- }),
- // Load view-by swimlane data and filtered top influencers.
- // mergeMap is used to have access to the already fetched data and act on it in arg #1.
- // In arg #2 of mergeMap we combine the data and pass it on in the action format
- // which can be consumed by explorerReducer() later on.
- mergeMap(
- ({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
- forkJoin({
- influencers:
- (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
- anomalyChartRecords !== undefined &&
- anomalyChartRecords.length > 0
- ? memoizedLoadFilteredTopInfluencers(
- lastRefresh,
- jobIds,
- timerange.earliestMs,
- timerange.latestMs,
- anomalyChartRecords,
- selectionInfluencers,
- noInfluencersConfigured,
- influencersFilterQuery
- )
- : Promise.resolve(influencers),
- viewBySwimlaneState: memoizedLoadViewBySwimlane(
- lastRefresh,
- topFieldValues,
- {
- earliest: overallState.overallSwimlaneData.earliest,
- latest: overallState.overallSwimlaneData.latest,
- },
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- influencersFilterQuery,
- noInfluencersConfigured
- ),
- }),
- (
- { annotationsData, overallState, tableData },
- { influencers, viewBySwimlaneState }
- ): Partial => {
- return {
- annotationsData,
- influencers,
- ...overallState,
- ...viewBySwimlaneState,
- tableData,
- };
- }
- )
- );
-}
-
-const loadExplorerData$ = new Subject();
-const explorerData$ = loadExplorerData$.pipe(
- switchMap((config: LoadExplorerDataConfig) => loadExplorerData(config))
-);
-
+ tableSeverity
+ );
+ }
+ }),
+ // Load view-by swimlane data and filtered top influencers.
+ // mergeMap is used to have access to the already fetched data and act on it in arg #1.
+ // In arg #2 of mergeMap we combine the data and pass it on in the action format
+ // which can be consumed by explorerReducer() later on.
+ mergeMap(
+ ({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
+ forkJoin({
+ influencers:
+ (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
+ anomalyChartRecords !== undefined &&
+ anomalyChartRecords.length > 0
+ ? memoizedLoadFilteredTopInfluencers(
+ lastRefresh,
+ jobIds,
+ timerange.earliestMs,
+ timerange.latestMs,
+ anomalyChartRecords,
+ selectionInfluencers,
+ noInfluencersConfigured,
+ influencersFilterQuery
+ )
+ : Promise.resolve(influencers),
+ viewBySwimlaneState: memoizedLoadViewBySwimlane(
+ lastRefresh,
+ topFieldValues,
+ {
+ earliest: overallState.earliest,
+ latest: overallState.latest,
+ },
+ selectedJobs,
+ viewBySwimlaneFieldName,
+ ANOMALY_SWIM_LANE_HARD_LIMIT,
+ viewByPerPage,
+ viewByFromPage,
+ swimlaneContainerWidth,
+ influencersFilterQuery
+ ),
+ }),
+ (
+ { annotationsData, overallState, tableData },
+ { influencers, viewBySwimlaneState }
+ ): Partial => {
+ return {
+ annotationsData,
+ influencers,
+ loading: false,
+ viewBySwimlaneDataLoading: false,
+ overallSwimlaneData: overallState,
+ viewBySwimlaneData: viewBySwimlaneState,
+ tableData,
+ swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState)
+ ? viewBySwimlaneState.cardinality
+ : undefined,
+ };
+ }
+ )
+ );
+ };
+};
export const useExplorerData = (): [Partial | undefined, (d: any) => void] => {
+ const timefilter = useTimefilter();
+
+ const {
+ services: {
+ mlServices: { mlApiServices },
+ uiSettings,
+ },
+ } = useMlKibana();
+ const loadExplorerData = useMemo(() => {
+ const service = new AnomalyTimelineService(
+ timefilter,
+ uiSettings,
+ mlResultsServiceProvider(mlApiServices)
+ );
+ return loadExplorerDataProvider(service);
+ }, []);
+ const loadExplorerData$ = useMemo(() => new Subject(), []);
+ const explorerData$ = useMemo(() => loadExplorerData$.pipe(switchMap(loadExplorerData)), []);
const explorerData = useObservable(explorerData$);
- return [explorerData, (c) => loadExplorerData$.next(c)];
+
+ const update = useCallback((c) => {
+ loadExplorerData$.next(c);
+ }, []);
+
+ return [explorerData, update];
};
diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
index 16e2fb47a209d..3ad749c9d0631 100644
--- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
@@ -52,7 +52,6 @@ function getDefaultEmbeddablepaPanelConfig(jobIds: JobId[]) {
interface AddToDashboardControlProps {
jobIds: JobId[];
viewBy: string;
- limit: number;
onClose: (callback?: () => Promise) => void;
}
@@ -63,7 +62,6 @@ export const AddToDashboardControl: FC = ({
onClose,
jobIds,
viewBy,
- limit,
}) => {
const {
notifications: { toasts },
@@ -141,7 +139,6 @@ export const AddToDashboardControl: FC = ({
jobIds,
swimlaneType,
viewBy,
- limit,
},
};
}
@@ -206,8 +203,8 @@ export const AddToDashboardControl: FC = ({
{
id: SWIMLANE_TYPE.VIEW_BY,
label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', {
- defaultMessage: 'View by {viewByField}, up to {limit} rows',
- values: { viewByField: viewBy, limit },
+ defaultMessage: 'View by {viewByField}',
+ values: { viewByField: viewBy },
}),
},
];
diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
index b4d32e2af64b8..e00e2e1e1e2eb 100644
--- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
@@ -22,12 +22,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { DRAG_SELECT_ACTION, VIEW_BY_JOB_LABEL } from './explorer_constants';
+import { DRAG_SELECT_ACTION, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants';
import { AddToDashboardControl } from './add_to_dashboard_control';
import { useMlKibana } from '../contexts/kibana';
import { TimeBuckets } from '../util/time_buckets';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
-import { SelectLimit } from './select_limit';
import {
ALLOW_CELL_RANGE_SELECTION,
dragSelect$,
@@ -36,9 +35,9 @@ import {
import { ExplorerState } from './reducers/explorer_reducer';
import { hasMatchingPoints } from './has_matching_points';
import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers_found/explorer_no_influencers_found';
-import { LoadingIndicator } from '../components/loading_indicator';
import { SwimlaneContainer } from './swimlane_container';
-import { OverallSwimlaneData } from './explorer_utils';
+import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
+import { NoOverallData } from './components/no_overall_data';
function mapSwimlaneOptionsToEuiOptions(options: string[]) {
return options.map((option) => ({
@@ -132,8 +131,11 @@ export const AnomalyTimeline: FC = React.memo(
viewBySwimlaneDataLoading,
viewBySwimlaneFieldName,
viewBySwimlaneOptions,
- swimlaneLimit,
selectedJobs,
+ viewByFromPage,
+ viewByPerPage,
+ swimlaneLimit,
+ loading,
} = explorerState;
const setSwimlaneSelectActive = useCallback((active: boolean) => {
@@ -159,25 +161,18 @@ export const AnomalyTimeline: FC = React.memo(
}, []);
// Listener for click events in the swimlane to load corresponding anomaly data.
- const swimlaneCellClick = useCallback((selectedCellsUpdate: any) => {
- // If selectedCells is an empty object we clear any existing selection,
- // otherwise we save the new selection in AppState and update the Explorer.
- if (Object.keys(selectedCellsUpdate).length === 0) {
- setSelectedCells();
- } else {
- setSelectedCells(selectedCellsUpdate);
- }
- }, []);
-
- const showOverallSwimlane =
- overallSwimlaneData !== null &&
- overallSwimlaneData.laneLabels &&
- overallSwimlaneData.laneLabels.length > 0;
-
- const showViewBySwimlane =
- viewBySwimlaneData !== null &&
- viewBySwimlaneData.laneLabels &&
- viewBySwimlaneData.laneLabels.length > 0;
+ const swimlaneCellClick = useCallback(
+ (selectedCellsUpdate: any) => {
+ // If selectedCells is an empty object we clear any existing selection,
+ // otherwise we save the new selection in AppState and update the Explorer.
+ if (Object.keys(selectedCellsUpdate).length === 0) {
+ setSelectedCells();
+ } else {
+ setSelectedCells(selectedCellsUpdate);
+ }
+ },
+ [setSelectedCells]
+ );
const menuItems = useMemo(() => {
const items = [];
@@ -235,21 +230,6 @@ export const AnomalyTimeline: FC = React.memo(
/>
-
-
-
-
- }
- display={'columnCompressed'}
- >
-
-
-
{viewByLoadedForTimeFormatted && (
@@ -305,68 +285,84 @@ export const AnomalyTimeline: FC
= React.memo(
- {showOverallSwimlane && (
- explorerService.setSwimlaneContainerWidth(width)}
- />
- )}
+ explorerService.setSwimlaneContainerWidth(width)}
+ isLoading={loading}
+ noDataWarning={}
+ />
+
+
{viewBySwimlaneOptions.length > 0 && (
<>
- {showViewBySwimlane && (
- <>
-
-
-
+
+ explorerService.setSwimlaneContainerWidth(width)}
+ fromPage={viewByFromPage}
+ perPage={viewByPerPage}
+ swimlaneLimit={swimlaneLimit}
+ onPaginationChange={({ perPage: perPageUpdate, fromPage: fromPageUpdate }) => {
+ if (perPageUpdate) {
+ explorerService.setViewByPerPage(perPageUpdate);
}
- timeBuckets={timeBuckets}
- swimlaneCellClick={swimlaneCellClick}
- swimlaneData={viewBySwimlaneData as OverallSwimlaneData}
- swimlaneType={'viewBy'}
- selection={selectedCells}
- swimlaneRenderDoneListener={swimlaneRenderDoneListener}
- onResize={(width) => explorerService.setSwimlaneContainerWidth(width)}
- />
-
- >
- )}
-
- {viewBySwimlaneDataLoading && }
-
- {!showViewBySwimlane &&
- !viewBySwimlaneDataLoading &&
- typeof viewBySwimlaneFieldName === 'string' && (
-
+ ) : (
+
+ )
+ ) : null
+ }
/>
- )}
+
+ >
>
)}
@@ -380,7 +376,6 @@ export const AnomalyTimeline: FC = React.memo(
}}
jobIds={selectedJobs.map(({ id }) => id)}
viewBy={viewBySwimlaneFieldName!}
- limit={swimlaneLimit}
/>
)}
>
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap
index 3ba4ebb2acdea..d3190d2ac1dad 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap
@@ -1,20 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExplorerNoInfluencersFound snapshot 1`] = `
-
-
-
+
`;
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx
index 639c0f7b78504..24def01108584 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx
@@ -7,7 +7,6 @@
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt } from '@elastic/eui';
/*
* React component for rendering EuiEmptyPrompt when no influencers were found.
@@ -15,26 +14,17 @@ import { EuiEmptyPrompt } from '@elastic/eui';
export const ExplorerNoInfluencersFound: FC<{
viewBySwimlaneFieldName: string;
showFilterMessage?: boolean;
-}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) => (
-
- {showFilterMessage === false && (
-
- )}
- {showFilterMessage === true && (
-
- )}
-
- }
- />
-);
+}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) =>
+ showFilterMessage === false ? (
+
+ ) : (
+
+ );
diff --git a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx
new file mode 100644
index 0000000000000..e73aac66a0d9f
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.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, { FC } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const NoOverallData: FC = () => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js
index 71c96840d1b57..df4cea0c07987 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer.js
@@ -12,8 +12,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
import {
EuiFlexGroup,
@@ -27,6 +25,7 @@ import {
EuiPageHeaderSection,
EuiSpacer,
EuiTitle,
+ EuiLoadingContent,
} from '@elastic/eui';
import { AnnotationFlyout } from '../components/annotations/annotation_flyout';
@@ -36,12 +35,10 @@ import { DatePickerWrapper } from '../components/navigation_menu/date_picker_wra
import { InfluencersList } from '../components/influencers_list';
import { explorerService } from './explorer_dashboard_service';
import { AnomalyResultsViewSelector } from '../components/anomaly_results_view_selector';
-import { LoadingIndicator } from '../components/loading_indicator/loading_indicator';
import { NavigationMenu } from '../components/navigation_menu';
import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts';
import { JobSelector } from '../components/job_selector';
import { SelectInterval } from '../components/controls/select_interval/select_interval';
-import { limit$ } from './select_limit/select_limit';
import { SelectSeverity } from '../components/controls/select_severity/select_severity';
import {
ExplorerQueryBar,
@@ -142,19 +139,6 @@ export class Explorer extends React.Component {
state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG };
- _unsubscribeAll = new Subject();
-
- componentDidMount() {
- limit$.pipe(takeUntil(this._unsubscribeAll)).subscribe(explorerService.setSwimlaneLimit);
- }
-
- componentWillUnmount() {
- this._unsubscribeAll.next();
- this._unsubscribeAll.complete();
- }
-
- viewByChangeHandler = (e) => explorerService.setViewBySwimlaneFieldName(e.target.value);
-
// Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes
// and will cause a syntax error when called with getKqlQueryValues
applyFilter = (fieldName, fieldValue, action) => {
@@ -240,29 +224,7 @@ export class Explorer extends React.Component {
const noJobsFound = selectedJobs === null || selectedJobs.length === 0;
const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0;
- if (loading === true) {
- return (
-
-
-
- );
- }
-
- if (noJobsFound) {
+ if (noJobsFound && !loading) {
return (
@@ -270,7 +232,7 @@ export class Explorer extends React.Component {
);
}
- if (noJobsFound && hasResults === false) {
+ if (noJobsFound && hasResults === false && !loading) {
return (
@@ -320,7 +282,11 @@ export class Explorer extends React.Component {
/>
-
+ {loading ? (
+
+ ) : (
+
+ )}
)}
@@ -352,59 +318,59 @@ export class Explorer extends React.Component {
>
)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
-
-
-
-
-
- )}
-
-
-
-
-
- {showCharts && }
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
+
+
+
+
+
+ )}
+
+
+
+ {showCharts && }
+
+
+ >
+ )}
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
index d1adf8c7ad744..21e13cb029d69 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
@@ -27,9 +27,10 @@ export const EXPLORER_ACTION = {
SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings',
SET_SELECTED_CELLS: 'setSelectedCells',
SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth',
- SET_SWIMLANE_LIMIT: 'setSwimlaneLimit',
SET_VIEW_BY_SWIMLANE_FIELD_NAME: 'setViewBySwimlaneFieldName',
SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading',
+ SET_VIEW_BY_PER_PAGE: 'setViewByPerPage',
+ SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage',
};
export const FILTER_ACTION = {
@@ -51,9 +52,23 @@ export const CHART_TYPE = {
};
export const MAX_CATEGORY_EXAMPLES = 10;
+
+/**
+ * Maximum amount of top influencer to fetch.
+ */
export const MAX_INFLUENCER_FIELD_VALUES = 10;
export const MAX_INFLUENCER_FIELD_NAMES = 50;
export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', {
defaultMessage: 'job ID',
});
+/**
+ * Hard limitation for the size of terms
+ * aggregations on influencers values.
+ */
+export const ANOMALY_SWIM_LANE_HARD_LIMIT = 1000;
+
+/**
+ * Default page size fot the anomaly swim lane.
+ */
+export const SWIM_LANE_DEFAULT_PAGE_SIZE = 10;
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts
index 30ab918983a77..1429bf0858361 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts
@@ -12,7 +12,7 @@
import { isEqual } from 'lodash';
import { from, isObservable, Observable, Subject } from 'rxjs';
-import { distinctUntilChanged, flatMap, map, scan } from 'rxjs/operators';
+import { distinctUntilChanged, flatMap, map, scan, shareReplay } from 'rxjs/operators';
import { DeepPartial } from '../../../common/types/common';
@@ -49,7 +49,9 @@ const explorerFilteredAction$ = explorerAction$.pipe(
// applies action and returns state
const explorerState$: Observable = explorerFilteredAction$.pipe(
- scan(explorerReducer, getExplorerDefaultState())
+ scan(explorerReducer, getExplorerDefaultState()),
+ // share the last emitted value among new subscribers
+ shareReplay(1)
);
interface ExplorerAppState {
@@ -59,6 +61,8 @@ interface ExplorerAppState {
selectedTimes?: number[];
showTopFieldValues?: boolean;
viewByFieldName?: string;
+ viewByPerPage?: number;
+ viewByFromPage?: number;
};
mlExplorerFilter: {
influencersFilterQuery?: unknown;
@@ -88,6 +92,14 @@ const explorerAppState$: Observable = explorerState$.pipe(
appState.mlExplorerSwimlane.viewByFieldName = state.viewBySwimlaneFieldName;
}
+ if (state.viewByFromPage !== undefined) {
+ appState.mlExplorerSwimlane.viewByFromPage = state.viewByFromPage;
+ }
+
+ if (state.viewByPerPage !== undefined) {
+ appState.mlExplorerSwimlane.viewByPerPage = state.viewByPerPage;
+ }
+
if (state.filterActive) {
appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery;
appState.mlExplorerFilter.filterActive = state.filterActive;
@@ -153,13 +165,16 @@ export const explorerService = {
payload,
});
},
- setSwimlaneLimit: (payload: number) => {
- explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_LIMIT, payload });
- },
setViewBySwimlaneFieldName: (payload: string) => {
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME, payload });
},
setViewBySwimlaneLoading: (payload: any) => {
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING, payload });
},
+ setViewByFromPage: (payload: number) => {
+ explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE, payload });
+ },
+ setViewByPerPage: (payload: number) => {
+ explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE, payload });
+ },
};
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
index 4e6dcdcc5129c..aa386288ac7e0 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
@@ -29,7 +29,7 @@ import {
ChartTooltipService,
ChartTooltipValue,
} from '../components/chart_tooltip/chart_tooltip_service';
-import { OverallSwimlaneData } from './explorer_utils';
+import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
const SCSS = {
mlDragselectDragging: 'mlDragselectDragging',
@@ -57,7 +57,7 @@ export interface ExplorerSwimlaneProps {
maskAll?: boolean;
timeBuckets: InstanceType;
swimlaneCellClick?: Function;
- swimlaneData: OverallSwimlaneData;
+ swimlaneData: OverallSwimlaneData | ViewBySwimLaneData;
swimlaneType: SwimlaneType;
selection?: {
lanes: any[];
@@ -211,7 +211,7 @@ export class ExplorerSwimlane extends React.Component {
const { swimlaneType } = this.props;
// This selects both overall and viewby swimlane
- const wrapper = d3.selectAll('.ml-explorer-swimlane');
+ const wrapper = d3.selectAll('.mlExplorerSwimlane');
wrapper.selectAll('.lane-label').classed('lane-label-masked', true);
wrapper
@@ -242,7 +242,7 @@ export class ExplorerSwimlane extends React.Component {
maskIrrelevantSwimlanes(maskAll: boolean) {
if (maskAll === true) {
// This selects both overall and viewby swimlane
- const allSwimlanes = d3.selectAll('.ml-explorer-swimlane');
+ const allSwimlanes = d3.selectAll('.mlExplorerSwimlane');
allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true);
allSwimlanes
.selectAll('.sl-cell-inner,.sl-cell-inner-dragselect')
@@ -258,7 +258,7 @@ export class ExplorerSwimlane extends React.Component {
clearSelection() {
// This selects both overall and viewby swimlane
- const wrapper = d3.selectAll('.ml-explorer-swimlane');
+ const wrapper = d3.selectAll('.mlExplorerSwimlane');
wrapper.selectAll('.lane-label').classed('lane-label-masked', false);
wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false);
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts
index 2d49fa737cef6..05fdb52e1ccb2 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts
@@ -8,8 +8,6 @@ import { Moment } from 'moment';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
-import { TimeBucketsInterval } from '../util/time_buckets';
-
interface ClearedSelectedAnomaliesState {
selectedCells: undefined;
viewByLoadedForTimeFormatted: null;
@@ -35,6 +33,10 @@ export declare interface OverallSwimlaneData extends SwimlaneData {
latest: number;
}
+export interface ViewBySwimLaneData extends OverallSwimlaneData {
+ cardinality: number;
+}
+
export declare const getDateFormatTz: () => any;
export declare const getDefaultSwimlaneData: () => SwimlaneData;
@@ -163,22 +165,6 @@ declare interface LoadOverallDataResponse {
overallSwimlaneData: OverallSwimlaneData;
}
-export declare const loadOverallData: (
- selectedJobs: ExplorerJob[],
- interval: TimeBucketsInterval,
- bounds: TimeRangeBounds
-) => Promise;
-
-export declare const loadViewBySwimlane: (
- fieldValues: string[],
- bounds: SwimlaneBounds,
- selectedJobs: ExplorerJob[],
- viewBySwimlaneFieldName: string,
- swimlaneLimit: number,
- influencersFilterQuery: any,
- noInfluencersConfigured: boolean
-) => Promise;
-
export declare const loadViewByTopFieldValuesForSelectedTime: (
earliestMs: number,
latestMs: number,
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js
index bd6a7ee59c942..23da9669ee9a5 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js
@@ -8,11 +8,9 @@
* utils for Anomaly Explorer.
*/
-import { chain, each, get, union, uniq } from 'lodash';
+import { chain, get, union, uniq } from 'lodash';
import moment from 'moment-timezone';
-import { i18n } from '@kbn/i18n';
-
import {
ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE,
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
@@ -27,7 +25,7 @@ import { parseInterval } from '../../../common/util/parse_interval';
import { ml } from '../services/ml_api_service';
import { mlJobService } from '../services/job_service';
import { mlResultsService } from '../services/results_service';
-import { getBoundsRoundedToInterval, getTimeBucketsFromCache } from '../util/time_buckets';
+import { getTimeBucketsFromCache } from '../util/time_buckets';
import { getTimefilter, getUiSettings } from '../util/dependency_cache';
import {
@@ -36,7 +34,6 @@ import {
SWIMLANE_TYPE,
VIEW_BY_JOB_LABEL,
} from './explorer_constants';
-import { getSwimlaneContainerWidth } from './legacy_utils';
// create new job objects based on standard job config objects
// new job objects just contain job id, bucket span in seconds and a selected flag.
@@ -51,6 +48,7 @@ export function getClearedSelectedAnomaliesState() {
return {
selectedCells: undefined,
viewByLoadedForTimeFormatted: null,
+ swimlaneLimit: undefined,
};
}
@@ -267,58 +265,6 @@ export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth)
return buckets.getInterval();
}
-export function loadViewByTopFieldValuesForSelectedTime(
- earliestMs,
- latestMs,
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- noInfluencersConfigured
-) {
- const selectedJobIds = selectedJobs.map((d) => d.id);
-
- // Find the top field values for the selected time, and then load the 'view by'
- // swimlane over the full time range for those specific field values.
- return new Promise((resolve) => {
- if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
- mlResultsService
- .getTopInfluencers(selectedJobIds, earliestMs, latestMs, swimlaneLimit)
- .then((resp) => {
- if (resp.influencers[viewBySwimlaneFieldName] === undefined) {
- resolve([]);
- }
-
- const topFieldValues = [];
- const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
- if (Array.isArray(topInfluencers)) {
- topInfluencers.forEach((influencerData) => {
- if (influencerData.maxAnomalyScore > 0) {
- topFieldValues.push(influencerData.influencerFieldValue);
- }
- });
- }
- resolve(topFieldValues);
- });
- } else {
- mlResultsService
- .getScoresByBucket(
- selectedJobIds,
- earliestMs,
- latestMs,
- getSwimlaneBucketInterval(
- selectedJobs,
- getSwimlaneContainerWidth(noInfluencersConfigured)
- ).asSeconds() + 's',
- swimlaneLimit
- )
- .then((resp) => {
- const topFieldValues = Object.keys(resp.results);
- resolve(topFieldValues);
- });
- }
- });
-}
-
// Obtain the list of 'View by' fields per job and viewBySwimlaneFieldName
export function getViewBySwimlaneOptions({
currentViewBySwimlaneFieldName,
@@ -435,105 +381,6 @@ export function getViewBySwimlaneOptions({
};
}
-export function processOverallResults(scoresByTime, searchBounds, interval) {
- const overallLabel = i18n.translate('xpack.ml.explorer.overallLabel', {
- defaultMessage: 'Overall',
- });
- const dataset = {
- laneLabels: [overallLabel],
- points: [],
- interval,
- earliest: searchBounds.min.valueOf() / 1000,
- latest: searchBounds.max.valueOf() / 1000,
- };
-
- if (Object.keys(scoresByTime).length > 0) {
- // Store the earliest and latest times of the data returned by the ES aggregations,
- // These will be used for calculating the earliest and latest times for the swimlane charts.
- each(scoresByTime, (score, timeMs) => {
- const time = timeMs / 1000;
- dataset.points.push({
- laneLabel: overallLabel,
- time,
- value: score,
- });
-
- dataset.earliest = Math.min(time, dataset.earliest);
- dataset.latest = Math.max(time + dataset.interval, dataset.latest);
- });
- }
-
- return dataset;
-}
-
-export function processViewByResults(
- scoresByInfluencerAndTime,
- sortedLaneValues,
- bounds,
- viewBySwimlaneFieldName,
- interval
-) {
- // Processes the scores for the 'view by' swimlane.
- // Sorts the lanes according to the supplied array of lane
- // values in the order in which they should be displayed,
- // or pass an empty array to sort lanes according to max score over all time.
- const dataset = {
- fieldName: viewBySwimlaneFieldName,
- points: [],
- interval,
- };
-
- // Set the earliest and latest to be the same as the overall swimlane.
- dataset.earliest = bounds.earliest;
- dataset.latest = bounds.latest;
-
- const laneLabels = [];
- const maxScoreByLaneLabel = {};
-
- each(scoresByInfluencerAndTime, (influencerData, influencerFieldValue) => {
- laneLabels.push(influencerFieldValue);
- maxScoreByLaneLabel[influencerFieldValue] = 0;
-
- each(influencerData, (anomalyScore, timeMs) => {
- const time = timeMs / 1000;
- dataset.points.push({
- laneLabel: influencerFieldValue,
- time,
- value: anomalyScore,
- });
- maxScoreByLaneLabel[influencerFieldValue] = Math.max(
- maxScoreByLaneLabel[influencerFieldValue],
- anomalyScore
- );
- });
- });
-
- const sortValuesLength = sortedLaneValues.length;
- if (sortValuesLength === 0) {
- // Sort lanes in descending order of max score.
- // Note the keys in scoresByInfluencerAndTime received from the ES request
- // are not guaranteed to be sorted by score if they can be parsed as numbers
- // (e.g. if viewing by HTTP response code).
- dataset.laneLabels = laneLabels.sort((a, b) => {
- return maxScoreByLaneLabel[b] - maxScoreByLaneLabel[a];
- });
- } else {
- // Sort lanes according to supplied order
- // e.g. when a cell in the overall swimlane has been selected.
- // Find the index of each lane label from the actual data set,
- // rather than using sortedLaneValues as-is, just in case they differ.
- dataset.laneLabels = laneLabels.sort((a, b) => {
- let aIndex = sortedLaneValues.indexOf(a);
- let bIndex = sortedLaneValues.indexOf(b);
- aIndex = aIndex > -1 ? aIndex : sortValuesLength;
- bIndex = bIndex > -1 ? bIndex : sortValuesLength;
- return aIndex - bIndex;
- });
- }
-
- return dataset;
-}
-
export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) {
const jobIds =
selectedCells !== undefined && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL
@@ -723,138 +570,6 @@ export async function loadDataForCharts(
});
}
-export function loadOverallData(selectedJobs, interval, bounds) {
- return new Promise((resolve) => {
- // Loads the overall data components i.e. the overall swimlane and influencers list.
- if (selectedJobs === null) {
- resolve({
- loading: false,
- hasResuts: false,
- });
- return;
- }
-
- // Ensure the search bounds align to the bucketing interval used in the swimlane so
- // that the first and last buckets are complete.
- const searchBounds = getBoundsRoundedToInterval(bounds, interval, false);
- const selectedJobIds = selectedJobs.map((d) => d.id);
-
- // Load the overall bucket scores by time.
- // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
- // which wouldn't be the case if e.g. '1M' was used.
- // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works
- // to ensure the search is inclusive of end time.
- const overallBucketsBounds = getBoundsRoundedToInterval(bounds, interval, true);
- mlResultsService
- .getOverallBucketScores(
- selectedJobIds,
- // Note there is an optimization for when top_n == 1.
- // If top_n > 1, we should test what happens when the request takes long
- // and refactor the loading calls, if necessary, to avoid delays in loading other components.
- 1,
- overallBucketsBounds.min.valueOf(),
- overallBucketsBounds.max.valueOf(),
- interval.asSeconds() + 's'
- )
- .then((resp) => {
- const overallSwimlaneData = processOverallResults(
- resp.results,
- searchBounds,
- interval.asSeconds()
- );
-
- resolve({
- loading: false,
- overallSwimlaneData,
- });
- });
- });
-}
-
-export function loadViewBySwimlane(
- fieldValues,
- bounds,
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- influencersFilterQuery,
- noInfluencersConfigured
-) {
- return new Promise((resolve) => {
- const finish = (resp) => {
- if (resp !== undefined) {
- const viewBySwimlaneData = processViewByResults(
- resp.results,
- fieldValues,
- bounds,
- viewBySwimlaneFieldName,
- getSwimlaneBucketInterval(
- selectedJobs,
- getSwimlaneContainerWidth(noInfluencersConfigured)
- ).asSeconds()
- );
-
- resolve({
- viewBySwimlaneData,
- viewBySwimlaneDataLoading: false,
- });
- } else {
- resolve({ viewBySwimlaneDataLoading: false });
- }
- };
-
- if (selectedJobs === undefined || viewBySwimlaneFieldName === undefined) {
- finish();
- return;
- } else {
- // Ensure the search bounds align to the bucketing interval used in the swimlane so
- // that the first and last buckets are complete.
- const timefilter = getTimefilter();
- const timefilterBounds = timefilter.getActiveBounds();
- const searchBounds = getBoundsRoundedToInterval(
- timefilterBounds,
- getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)),
- false
- );
- const selectedJobIds = selectedJobs.map((d) => d.id);
-
- // load scores by influencer/jobId value and time.
- // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
- // which wouldn't be the case if e.g. '1M' was used.
- const interval = `${getSwimlaneBucketInterval(
- selectedJobs,
- getSwimlaneContainerWidth(noInfluencersConfigured)
- ).asSeconds()}s`;
- if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
- mlResultsService
- .getInfluencerValueMaxScoreByTime(
- selectedJobIds,
- viewBySwimlaneFieldName,
- fieldValues,
- searchBounds.min.valueOf(),
- searchBounds.max.valueOf(),
- interval,
- swimlaneLimit,
- influencersFilterQuery
- )
- .then(finish);
- } else {
- const jobIds =
- fieldValues !== undefined && fieldValues.length > 0 ? fieldValues : selectedJobIds;
- mlResultsService
- .getScoresByBucket(
- jobIds,
- searchBounds.min.valueOf(),
- searchBounds.max.valueOf(),
- interval,
- swimlaneLimit
- )
- .then(finish);
- }
- }
- });
-}
-
export async function loadTopInfluencers(
selectedJobIds,
earliestMs,
@@ -871,6 +586,8 @@ export async function loadTopInfluencers(
earliestMs,
latestMs,
MAX_INFLUENCER_FIELD_VALUES,
+ 10,
+ 1,
influencers,
influencersFilterQuery
)
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
index a19750494afdc..068f43a140c90 100644
--- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useCallback, useMemo } from 'react';
import { useUrlState } from '../../util/url_state';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
@@ -14,55 +15,55 @@ export const useSelectedCells = (): [
] => {
const [appState, setAppState] = useUrlState('_a');
- let selectedCells: AppStateSelectedCells | undefined;
-
// keep swimlane selection, restore selectedCells from AppState
- if (
- appState &&
- appState.mlExplorerSwimlane &&
- appState.mlExplorerSwimlane.selectedType !== undefined
- ) {
- selectedCells = {
- type: appState.mlExplorerSwimlane.selectedType,
- lanes: appState.mlExplorerSwimlane.selectedLanes,
- times: appState.mlExplorerSwimlane.selectedTimes,
- showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues,
- viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName,
- };
- }
+ const selectedCells = useMemo(() => {
+ return appState?.mlExplorerSwimlane?.selectedType !== undefined
+ ? {
+ type: appState.mlExplorerSwimlane.selectedType,
+ lanes: appState.mlExplorerSwimlane.selectedLanes,
+ times: appState.mlExplorerSwimlane.selectedTimes,
+ showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues,
+ viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName,
+ }
+ : undefined;
+ // TODO fix appState to use memoization
+ }, [JSON.stringify(appState?.mlExplorerSwimlane)]);
- const setSelectedCells = (swimlaneSelectedCells: AppStateSelectedCells) => {
- const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane };
+ const setSelectedCells = useCallback(
+ (swimlaneSelectedCells: AppStateSelectedCells) => {
+ const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane };
- if (swimlaneSelectedCells !== undefined) {
- swimlaneSelectedCells.showTopFieldValues = false;
+ if (swimlaneSelectedCells !== undefined) {
+ swimlaneSelectedCells.showTopFieldValues = false;
- const currentSwimlaneType = selectedCells?.type;
- const currentShowTopFieldValues = selectedCells?.showTopFieldValues;
- const newSwimlaneType = swimlaneSelectedCells?.type;
+ const currentSwimlaneType = selectedCells?.type;
+ const currentShowTopFieldValues = selectedCells?.showTopFieldValues;
+ const newSwimlaneType = swimlaneSelectedCells?.type;
- if (
- (currentSwimlaneType === SWIMLANE_TYPE.OVERALL &&
- newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) ||
- newSwimlaneType === SWIMLANE_TYPE.OVERALL ||
- currentShowTopFieldValues === true
- ) {
- swimlaneSelectedCells.showTopFieldValues = true;
- }
+ if (
+ (currentSwimlaneType === SWIMLANE_TYPE.OVERALL &&
+ newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) ||
+ newSwimlaneType === SWIMLANE_TYPE.OVERALL ||
+ currentShowTopFieldValues === true
+ ) {
+ swimlaneSelectedCells.showTopFieldValues = true;
+ }
- mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type;
- mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes;
- mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times;
- mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues;
- setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
- } else {
- delete mlExplorerSwimlane.selectedType;
- delete mlExplorerSwimlane.selectedLanes;
- delete mlExplorerSwimlane.selectedTimes;
- delete mlExplorerSwimlane.showTopFieldValues;
- setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
- }
- };
+ mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type;
+ mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes;
+ mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times;
+ mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues;
+ setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
+ } else {
+ delete mlExplorerSwimlane.selectedType;
+ delete mlExplorerSwimlane.selectedLanes;
+ delete mlExplorerSwimlane.selectedTimes;
+ delete mlExplorerSwimlane.showTopFieldValues;
+ setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
+ }
+ },
+ [appState?.mlExplorerSwimlane, selectedCells]
+ );
return [selectedCells, setSelectedCells];
};
diff --git a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts
index 3b92ee3fa37f6..b85b0401c45ca 100644
--- a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts
+++ b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts
@@ -11,8 +11,3 @@ export function getChartContainerWidth() {
const chartContainer = document.querySelector('.explorer-charts');
return Math.floor((chartContainer && chartContainer.clientWidth) || 0);
}
-
-export function getSwimlaneContainerWidth() {
- const explorerContainer = document.querySelector('.ml-explorer');
- return (explorerContainer && explorerContainer.clientWidth) || 0;
-}
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts
index 1614da14e355a..dd1d0516b6173 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts
@@ -19,5 +19,6 @@ export function clearInfluencerFilterSettings(state: ExplorerState): ExplorerSta
queryString: '',
tableQueryString: '',
...getClearedSelectedAnomaliesState(),
+ viewByFromPage: 1,
};
}
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts
index a26c0564c6b16..49f5794273a04 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts
@@ -17,6 +17,7 @@ export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload)
noInfluencersConfigured: getInfluencers(selectedJobs).length === 0,
overallSwimlaneData: getDefaultSwimlaneData(),
selectedJobs,
+ viewByFromPage: 1,
};
// clear filter if selected jobs have no influencers
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts
index c31b26b7adb7b..c55c06c80ab81 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts
@@ -27,7 +27,7 @@ import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder';
export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => {
const { type, payload } = nextAction;
- let nextState;
+ let nextState: ExplorerState;
switch (type) {
case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS:
@@ -39,6 +39,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
...state,
...getClearedSelectedAnomaliesState(),
loading: false,
+ viewByFromPage: 1,
selectedJobs: [],
};
break;
@@ -82,22 +83,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
break;
case EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH:
- if (state.noInfluencersConfigured === true) {
- // swimlane is full width, minus 30 for the 'no influencers' info icon,
- // minus 170 for the lane labels, minus 50 padding
- nextState = { ...state, swimlaneContainerWidth: payload - 250 };
- } else {
- // swimlane width is 5 sixths of the window,
- // minus 170 for the lane labels, minus 50 padding
- nextState = { ...state, swimlaneContainerWidth: (payload / 6) * 5 - 220 };
- }
- break;
-
- case EXPLORER_ACTION.SET_SWIMLANE_LIMIT:
- nextState = {
- ...state,
- swimlaneLimit: payload,
- };
+ nextState = { ...state, swimlaneContainerWidth: payload };
break;
case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME:
@@ -117,6 +103,9 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
...getClearedSelectedAnomaliesState(),
maskAll,
viewBySwimlaneFieldName,
+ viewBySwimlaneData: getDefaultSwimlaneData(),
+ viewByFromPage: 1,
+ viewBySwimlaneDataLoading: true,
};
break;
@@ -125,7 +114,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
nextState = {
...state,
annotationsData,
- ...overallState,
+ overallSwimlaneData: overallState,
tableData,
viewBySwimlaneData: {
...getDefaultSwimlaneData(),
@@ -134,6 +123,22 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
};
break;
+ case EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE:
+ nextState = {
+ ...state,
+ viewByFromPage: payload,
+ };
+ break;
+
+ case EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE:
+ nextState = {
+ ...state,
+ // reset current page on the page size change
+ viewByFromPage: 1,
+ viewByPerPage: payload,
+ };
+ break;
+
default:
nextState = state;
}
@@ -155,7 +160,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
filteredFields: nextState.filteredFields,
isAndOperator: nextState.isAndOperator,
selectedJobs: nextState.selectedJobs,
- selectedCells: nextState.selectedCells,
+ selectedCells: nextState.selectedCells!,
});
const { bounds, selectedCells } = nextState;
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts
index 819f6ca1cac92..be87de7da8c88 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts
@@ -57,5 +57,6 @@ export function setInfluencerFilterSettings(
filteredFields.includes(selectedViewByFieldName) === false,
viewBySwimlaneFieldName: selectedViewByFieldName,
viewBySwimlaneOptions: filteredViewBySwimlaneOptions,
+ viewByFromPage: 1,
};
}
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
index 4e1a2af9b13a6..892b46467345b 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
@@ -19,7 +19,9 @@ import {
TimeRangeBounds,
OverallSwimlaneData,
SwimlaneData,
+ ViewBySwimLaneData,
} from '../../explorer_utils';
+import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../explorer_constants';
export interface ExplorerState {
annotationsData: any[];
@@ -42,14 +44,16 @@ export interface ExplorerState {
selectedJobs: ExplorerJob[] | null;
swimlaneBucketInterval: any;
swimlaneContainerWidth: number;
- swimlaneLimit: number;
tableData: AnomaliesTableData;
tableQueryString: string;
viewByLoadedForTimeFormatted: string | null;
- viewBySwimlaneData: SwimlaneData | OverallSwimlaneData;
+ viewBySwimlaneData: SwimlaneData | ViewBySwimLaneData;
viewBySwimlaneDataLoading: boolean;
viewBySwimlaneFieldName?: string;
+ viewByPerPage: number;
+ viewByFromPage: number;
viewBySwimlaneOptions: string[];
+ swimlaneLimit?: number;
}
function getDefaultIndexPattern() {
@@ -78,7 +82,6 @@ export function getExplorerDefaultState(): ExplorerState {
selectedJobs: null,
swimlaneBucketInterval: undefined,
swimlaneContainerWidth: 0,
- swimlaneLimit: 10,
tableData: {
anomalies: [],
examplesByJobId: [''],
@@ -92,5 +95,8 @@ export function getExplorerDefaultState(): ExplorerState {
viewBySwimlaneDataLoading: false,
viewBySwimlaneFieldName: undefined,
viewBySwimlaneOptions: [],
+ viewByPerPage: SWIM_LANE_DEFAULT_PAGE_SIZE,
+ viewByFromPage: 1,
+ swimlaneLimit: undefined,
};
}
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx
deleted file mode 100644
index cf65419e4bd80..0000000000000
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx
+++ /dev/null
@@ -1,29 +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 { act } from 'react-dom/test-utils';
-import { shallow } from 'enzyme';
-import { SelectLimit } from './select_limit';
-
-describe('SelectLimit', () => {
- test('creates correct initial selected value', () => {
- const wrapper = shallow();
- expect(wrapper.props().value).toEqual(10);
- });
-
- test('state for currently selected value is updated correctly on click', () => {
- const wrapper = shallow();
- expect(wrapper.props().value).toEqual(10);
-
- act(() => {
- wrapper.simulate('change', { target: { value: 25 } });
- });
- wrapper.update();
-
- expect(wrapper.props().value).toEqual(10);
- });
-});
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
deleted file mode 100644
index 7a2df1a0f0535..0000000000000
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
+++ /dev/null
@@ -1,40 +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.
- */
-
-/*
- * React component for rendering a select element with limit options.
- */
-import React from 'react';
-import useObservable from 'react-use/lib/useObservable';
-import { BehaviorSubject } from 'rxjs';
-
-import { EuiSelect } from '@elastic/eui';
-
-const limitOptions = [5, 10, 25, 50];
-
-const euiOptions = limitOptions.map((limit) => ({
- value: limit,
- text: `${limit}`,
-}));
-
-export const defaultLimit = limitOptions[1];
-export const limit$ = new BehaviorSubject(defaultLimit);
-
-export const useSwimlaneLimit = (): [number, (newLimit: number) => void] => {
- const limit = useObservable(limit$, defaultLimit);
-
- return [limit!, (newLimit: number) => limit$.next(newLimit)];
-};
-
-export const SelectLimit = () => {
- const [limit, setLimit] = useSwimlaneLimit();
-
- function onChange(e: React.ChangeEvent) {
- setLimit(parseInt(e.target.value, 10));
- }
-
- return ;
-};
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
index 57d1fd81000b7..e34e1d26c9cab 100644
--- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
@@ -5,7 +5,14 @@
*/
import React, { FC, useCallback, useState } from 'react';
-import { EuiResizeObserver, EuiText } from '@elastic/eui';
+import {
+ EuiText,
+ EuiLoadingChart,
+ EuiResizeObserver,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
import { throttle } from 'lodash';
import {
@@ -14,48 +21,139 @@ import {
} from '../../application/explorer/explorer_swimlane';
import { MlTooltipComponent } from '../../application/components/chart_tooltip';
+import { SwimLanePagination } from './swimlane_pagination';
+import { SWIMLANE_TYPE } from './explorer_constants';
+import { ViewBySwimLaneData } from './explorer_utils';
+/**
+ * Ignore insignificant resize, e.g. browser scrollbar appearance.
+ */
+const RESIZE_IGNORED_DIFF_PX = 20;
const RESIZE_THROTTLE_TIME_MS = 500;
+export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData {
+ return arg && arg.hasOwnProperty('cardinality');
+}
+
+/**
+ * Anomaly swim lane container responsible for handling resizing, pagination and injecting
+ * tooltip service.
+ *
+ * @param children
+ * @param onResize
+ * @param perPage
+ * @param fromPage
+ * @param swimlaneLimit
+ * @param onPaginationChange
+ * @param props
+ * @constructor
+ */
export const SwimlaneContainer: FC<
Omit & {
onResize: (width: number) => void;
+ fromPage?: number;
+ perPage?: number;
+ swimlaneLimit?: number;
+ onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void;
+ isLoading: boolean;
+ noDataWarning: string | JSX.Element | null;
}
-> = ({ children, onResize, ...props }) => {
+> = ({
+ children,
+ onResize,
+ perPage,
+ fromPage,
+ swimlaneLimit,
+ onPaginationChange,
+ isLoading,
+ noDataWarning,
+ ...props
+}) => {
const [chartWidth, setChartWidth] = useState(0);
const resizeHandler = useCallback(
throttle((e: { width: number; height: number }) => {
const labelWidth = 200;
- setChartWidth(e.width - labelWidth);
- onResize(e.width);
+ const resultNewWidth = e.width - labelWidth;
+ if (Math.abs(resultNewWidth - chartWidth) > RESIZE_IGNORED_DIFF_PX) {
+ setChartWidth(resultNewWidth);
+ onResize(resultNewWidth);
+ }
}, RESIZE_THROTTLE_TIME_MS),
- []
+ [chartWidth]
);
+ const showSwimlane =
+ props.swimlaneData &&
+ props.swimlaneData.laneLabels &&
+ props.swimlaneData.laneLabels.length > 0 &&
+ props.swimlaneData.points.length > 0;
+
+ const isPaginationVisible =
+ (showSwimlane || isLoading) &&
+ swimlaneLimit !== undefined &&
+ onPaginationChange &&
+ props.swimlaneType === SWIMLANE_TYPE.VIEW_BY &&
+ fromPage &&
+ perPage;
+
return (
-
- {(resizeRef) => (
- {
- resizeRef(el);
- }}
- >
-
-
-
- {(tooltipService) => (
-
+
+ {(resizeRef) => (
+ {
+ resizeRef(el);
+ }}
+ data-test-subj="mlSwimLaneContainer"
+ >
+
+
+ {showSwimlane && !isLoading && (
+
+ {(tooltipService) => (
+
+ )}
+
+ )}
+ {isLoading && (
+
+
+
+ )}
+ {!isLoading && !showSwimlane && (
+ {noDataWarning}}
/>
)}
-
-
-
-
- )}
-
+
+
+ {isPaginationVisible && (
+
+
+
+ )}
+
+ )}
+
+ >
);
};
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx
new file mode 100644
index 0000000000000..0607f7fd35fad
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx
@@ -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 React, { FC, useCallback, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPopover,
+ EuiContextMenuPanel,
+ EuiPagination,
+ EuiContextMenuItem,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+interface SwimLanePaginationProps {
+ fromPage: number;
+ perPage: number;
+ cardinality: number;
+ onPaginationChange: (arg: { perPage?: number; fromPage?: number }) => void;
+}
+
+export const SwimLanePagination: FC = ({
+ cardinality,
+ fromPage,
+ perPage,
+ onPaginationChange,
+}) => {
+ const componentFromPage = fromPage - 1;
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const onButtonClick = () => setIsPopoverOpen(() => !isPopoverOpen);
+ const closePopover = () => setIsPopoverOpen(false);
+
+ const goToPage = useCallback((pageNumber: number) => {
+ onPaginationChange({ fromPage: pageNumber + 1 });
+ }, []);
+
+ const setPerPage = useCallback((perPageUpdate: number) => {
+ onPaginationChange({ perPage: perPageUpdate });
+ }, []);
+
+ const pageCount = Math.ceil(cardinality / perPage);
+
+ const items = [5, 10, 20, 50, 100].map((v) => {
+ return (
+ {
+ closePopover();
+ setPerPage(v);
+ }}
+ >
+
+
+ );
+ });
+
+ return (
+
+
+
+
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ panelPaddingSize="none"
+ >
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 2e355c6073abd..52b4408d1ac5b 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -22,7 +22,6 @@ import { ml } from '../../services/ml_api_service';
import { useExplorerData } from '../../explorer/actions';
import { explorerService } from '../../explorer/explorer_dashboard_service';
import { getDateFormatTz } from '../../explorer/explorer_utils';
-import { useSwimlaneLimit } from '../../explorer/select_limit';
import { useJobSelection } from '../../components/job_selector/use_job_selection';
import { useShowCharts } from '../../components/controls/checkbox_showcharts';
import { useTableInterval } from '../../components/controls/select_interval';
@@ -30,6 +29,7 @@ import { useTableSeverity } from '../../components/controls/select_severity';
import { useUrlState } from '../../util/url_state';
import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
+import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
const breadcrumbs = [
ML_BREADCRUMB,
@@ -151,10 +151,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
const [showCharts] = useShowCharts();
const [tableInterval] = useTableInterval();
const [tableSeverity] = useTableSeverity();
- const [swimlaneLimit] = useSwimlaneLimit();
- useEffect(() => {
- explorerService.setSwimlaneLimit(swimlaneLimit);
- }, [swimlaneLimit]);
const [selectedCells, setSelectedCells] = useSelectedCells();
useEffect(() => {
@@ -170,14 +166,26 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
selectedCells,
selectedJobs: explorerState.selectedJobs,
swimlaneBucketInterval: explorerState.swimlaneBucketInterval,
- swimlaneLimit: explorerState.swimlaneLimit,
tableInterval: tableInterval.val,
tableSeverity: tableSeverity.val,
viewBySwimlaneFieldName: explorerState.viewBySwimlaneFieldName,
+ swimlaneContainerWidth: explorerState.swimlaneContainerWidth,
+ viewByPerPage: explorerState.viewByPerPage,
+ viewByFromPage: explorerState.viewByFromPage,
}) ||
undefined;
+
useEffect(() => {
- loadExplorerData(loadExplorerDataConfig);
+ if (explorerState && explorerState.swimlaneContainerWidth > 0) {
+ loadExplorerData({
+ ...loadExplorerDataConfig,
+ swimlaneLimit:
+ explorerState?.viewBySwimlaneData &&
+ isViewBySwimLaneData(explorerState?.viewBySwimlaneData)
+ ? explorerState?.viewBySwimlaneData.cardinality
+ : undefined,
+ });
+ }
}, [JSON.stringify(loadExplorerDataConfig)]);
if (explorerState === undefined || refresh === undefined || showCharts === undefined) {
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
index ac4882b0055ae..11ec074bac1db 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
@@ -12,41 +12,47 @@ import { I18nProvider } from '@kbn/i18n/react';
import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer';
-jest.mock('../../contexts/kibana/kibana_context', () => ({
- useMlKibana: () => {
- return {
- services: {
- uiSettings: { get: jest.fn() },
- data: {
- query: {
- timefilter: {
+jest.mock('../../contexts/kibana/kibana_context', () => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { of } = require('rxjs');
+ return {
+ useMlKibana: () => {
+ return {
+ services: {
+ uiSettings: { get: jest.fn() },
+ data: {
+ query: {
timefilter: {
- disableTimeRangeSelector: jest.fn(),
- disableAutoRefreshSelector: jest.fn(),
- enableTimeRangeSelector: jest.fn(),
- enableAutoRefreshSelector: jest.fn(),
- getRefreshInterval: jest.fn(),
- setRefreshInterval: jest.fn(),
- getTime: jest.fn(),
- isAutoRefreshSelectorEnabled: jest.fn(),
- isTimeRangeSelectorEnabled: jest.fn(),
- getRefreshIntervalUpdate$: jest.fn(),
- getTimeUpdate$: jest.fn(),
- getEnabledUpdated$: jest.fn(),
+ timefilter: {
+ disableTimeRangeSelector: jest.fn(),
+ disableAutoRefreshSelector: jest.fn(),
+ enableTimeRangeSelector: jest.fn(),
+ enableAutoRefreshSelector: jest.fn(),
+ getRefreshInterval: jest.fn(),
+ setRefreshInterval: jest.fn(),
+ getTime: jest.fn(),
+ isAutoRefreshSelectorEnabled: jest.fn(),
+ isTimeRangeSelectorEnabled: jest.fn(),
+ getRefreshIntervalUpdate$: jest.fn(),
+ getTimeUpdate$: jest.fn(() => {
+ return of();
+ }),
+ getEnabledUpdated$: jest.fn(),
+ },
+ history: { get: jest.fn() },
},
- history: { get: jest.fn() },
},
},
- },
- notifications: {
- toasts: {
- addDanger: () => {},
+ notifications: {
+ toasts: {
+ addDanger: () => {},
+ },
},
},
- },
- };
- },
-}));
+ };
+ },
+ };
+});
jest.mock('../../util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
diff --git a/x-pack/plugins/ml/public/application/routing/use_refresh.ts b/x-pack/plugins/ml/public/application/routing/use_refresh.ts
index f0b93c876526b..c247fd9765e96 100644
--- a/x-pack/plugins/ml/public/application/routing/use_refresh.ts
+++ b/x-pack/plugins/ml/public/application/routing/use_refresh.ts
@@ -5,26 +5,40 @@
*/
import { useObservable } from 'react-use';
-import { merge, Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { merge } from 'rxjs';
+import { map, skip } from 'rxjs/operators';
+import { useMemo } from 'react';
import { annotationsRefresh$ } from '../services/annotations_service';
-import {
- mlTimefilterRefresh$,
- mlTimefilterTimeChange$,
-} from '../services/timefilter_refresh_service';
+import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
+import { useTimefilter } from '../contexts/kibana';
export interface Refresh {
lastRefresh: number;
timeRange?: { start: string; end: string };
}
-const refresh$: Observable = merge(
- mlTimefilterRefresh$,
- mlTimefilterTimeChange$,
- annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d })))
-);
-
+/**
+ * Hook that provides the latest refresh timestamp
+ * and the most recent applied time range.
+ */
export const useRefresh = () => {
+ const timefilter = useTimefilter();
+
+ const refresh$ = useMemo(() => {
+ return merge(
+ mlTimefilterRefresh$,
+ timefilter.getTimeUpdate$().pipe(
+ // skip initially emitted value
+ skip(1),
+ map((_) => {
+ const { from, to } = timefilter.getTime();
+ return { lastRefresh: Date.now(), timeRange: { start: from, end: to } };
+ })
+ ),
+ annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d })))
+ );
+ }, []);
+
return useObservable(refresh$);
};
diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
similarity index 82%
rename from x-pack/plugins/ml/public/application/services/explorer_service.ts
rename to x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
index 0944328db0052..f2e362f754f2b 100644
--- a/x-pack/plugins/ml/public/application/services/explorer_service.ts
+++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
@@ -12,14 +12,19 @@ import {
UI_SETTINGS,
} from '../../../../../../src/plugins/data/public';
import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets';
-import { ExplorerJob, OverallSwimlaneData, SwimlaneData } from '../explorer/explorer_utils';
+import {
+ ExplorerJob,
+ OverallSwimlaneData,
+ SwimlaneData,
+ ViewBySwimLaneData,
+} from '../explorer/explorer_utils';
import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants';
import { MlResultsService } from './results_service';
/**
- * Anomaly Explorer Service
+ * Service for retrieving anomaly swim lanes data.
*/
-export class ExplorerService {
+export class AnomalyTimelineService {
private timeBuckets: TimeBuckets;
private _customTimeRange: TimeRange | undefined;
@@ -130,12 +135,27 @@ export class ExplorerService {
return overallSwimlaneData;
}
+ /**
+ * Fetches view by swim lane data.
+ *
+ * @param fieldValues
+ * @param bounds
+ * @param selectedJobs
+ * @param viewBySwimlaneFieldName
+ * @param swimlaneLimit
+ * @param perPage
+ * @param fromPage
+ * @param swimlaneContainerWidth
+ * @param influencersFilterQuery
+ */
public async loadViewBySwimlane(
fieldValues: string[],
bounds: { earliest: number; latest: number },
selectedJobs: ExplorerJob[],
viewBySwimlaneFieldName: string,
swimlaneLimit: number,
+ perPage: number,
+ fromPage: number,
swimlaneContainerWidth: number,
influencersFilterQuery?: any
): Promise {
@@ -172,7 +192,8 @@ export class ExplorerService {
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
interval,
- swimlaneLimit
+ perPage,
+ fromPage
);
} else {
response = await this.mlResultsService.getInfluencerValueMaxScoreByTime(
@@ -183,6 +204,8 @@ export class ExplorerService {
searchBounds.max.valueOf(),
interval,
swimlaneLimit,
+ perPage,
+ fromPage,
influencersFilterQuery
);
}
@@ -193,6 +216,7 @@ export class ExplorerService {
const viewBySwimlaneData = this.processViewByResults(
response.results,
+ response.cardinality,
fieldValues,
bounds,
viewBySwimlaneFieldName,
@@ -204,6 +228,55 @@ export class ExplorerService {
return viewBySwimlaneData;
}
+ public async loadViewByTopFieldValuesForSelectedTime(
+ earliestMs: number,
+ latestMs: number,
+ selectedJobs: ExplorerJob[],
+ viewBySwimlaneFieldName: string,
+ swimlaneLimit: number,
+ perPage: number,
+ fromPage: number,
+ swimlaneContainerWidth: number
+ ) {
+ const selectedJobIds = selectedJobs.map((d) => d.id);
+
+ // Find the top field values for the selected time, and then load the 'view by'
+ // swimlane over the full time range for those specific field values.
+ if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
+ const resp = await this.mlResultsService.getTopInfluencers(
+ selectedJobIds,
+ earliestMs,
+ latestMs,
+ swimlaneLimit,
+ perPage,
+ fromPage
+ );
+ if (resp.influencers[viewBySwimlaneFieldName] === undefined) {
+ return [];
+ }
+
+ const topFieldValues: any[] = [];
+ const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
+ if (Array.isArray(topInfluencers)) {
+ topInfluencers.forEach((influencerData) => {
+ if (influencerData.maxAnomalyScore > 0) {
+ topFieldValues.push(influencerData.influencerFieldValue);
+ }
+ });
+ }
+ return topFieldValues;
+ } else {
+ const resp = await this.mlResultsService.getScoresByBucket(
+ selectedJobIds,
+ earliestMs,
+ latestMs,
+ this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's',
+ swimlaneLimit
+ );
+ return Object.keys(resp.results);
+ }
+ }
+
private getTimeBounds(): TimeRangeBounds {
return this._customTimeRange !== undefined
? this.timeFilter.calculateBounds(this._customTimeRange)
@@ -245,6 +318,7 @@ export class ExplorerService {
private processViewByResults(
scoresByInfluencerAndTime: Record,
+ cardinality: number,
sortedLaneValues: string[],
bounds: any,
viewBySwimlaneFieldName: string,
@@ -254,7 +328,7 @@ export class ExplorerService {
// Sorts the lanes according to the supplied array of lane
// values in the order in which they should be displayed,
// or pass an empty array to sort lanes according to max score over all time.
- const dataset: OverallSwimlaneData = {
+ const dataset: ViewBySwimLaneData = {
fieldName: viewBySwimlaneFieldName,
points: [],
laneLabels: [],
@@ -262,6 +336,7 @@ export class ExplorerService {
// Set the earliest and latest to be the same as the overall swim lane.
earliest: bounds.earliest,
latest: bounds.latest,
+ cardinality,
};
const maxScoreByLaneLabel: Record = {};
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
index a618534d7ae00..00adb2d325833 100644
--- a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
@@ -37,7 +37,7 @@ describe('DashboardService', () => {
// assert
expect(mockSavedObjectClient.find).toHaveBeenCalledWith({
type: 'dashboard',
- perPage: 10,
+ perPage: 1000,
search: `test*`,
searchFields: ['title^3', 'description'],
});
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
index 7f2bb71d18eb9..d6ccfc2f203e9 100644
--- a/x-pack/plugins/ml/public/application/services/dashboard_service.ts
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
@@ -34,7 +34,7 @@ export function dashboardServiceProvider(
async fetchDashboards(query?: string) {
return await savedObjectClient.find({
type: 'dashboard',
- perPage: 10,
+ perPage: 1000,
search: query ? `${query}*` : '',
searchFields: ['title^3', 'description'],
});
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index af6944d7ae2d2..d1b6f95f32bed 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -12,7 +12,7 @@ import { annotations } from './annotations';
import { dataFrameAnalytics } from './data_frame_analytics';
import { filters } from './filters';
import { resultsApiProvider } from './results';
-import { jobs } from './jobs';
+import { jobsApiProvider } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info';
@@ -726,7 +726,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
dataFrameAnalytics,
filters,
results: resultsApiProvider(httpService),
- jobs,
+ jobs: jobsApiProvider(httpService),
fileDatavisualizer,
};
}
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
index 6aa62da3f0768..d356fc0ef339b 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { http } from '../http_service';
+import { HttpService } from '../http_service';
import { basePath } from './index';
import { Dictionary } from '../../../../common/types/common';
@@ -24,10 +24,10 @@ import {
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
import { Category } from '../../../../common/types/categories';
-export const jobs = {
+export const jobsApiProvider = (httpService: HttpService) => ({
jobsSummary(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/jobs_summary`,
method: 'POST',
body,
@@ -36,7 +36,10 @@ export const jobs = {
jobsWithTimerange(dateFormatTz: string) {
const body = JSON.stringify({ dateFormatTz });
- return http<{ jobs: MlJobWithTimeRange[]; jobsMap: Dictionary }>({
+ return httpService.http<{
+ jobs: MlJobWithTimeRange[];
+ jobsMap: Dictionary;
+ }>({
path: `${basePath()}/jobs/jobs_with_time_range`,
method: 'POST',
body,
@@ -45,7 +48,7 @@ export const jobs = {
jobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/jobs`,
method: 'POST',
body,
@@ -53,7 +56,7 @@ export const jobs = {
},
groups() {
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/groups`,
method: 'GET',
});
@@ -61,7 +64,7 @@ export const jobs = {
updateGroups(updatedJobs: string[]) {
const body = JSON.stringify({ jobs: updatedJobs });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/update_groups`,
method: 'POST',
body,
@@ -75,7 +78,7 @@ export const jobs = {
end,
});
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/force_start_datafeeds`,
method: 'POST',
body,
@@ -84,7 +87,7 @@ export const jobs = {
stopDatafeeds(datafeedIds: string[]) {
const body = JSON.stringify({ datafeedIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/stop_datafeeds`,
method: 'POST',
body,
@@ -93,7 +96,7 @@ export const jobs = {
deleteJobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/delete_jobs`,
method: 'POST',
body,
@@ -102,7 +105,7 @@ export const jobs = {
closeJobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/close_jobs`,
method: 'POST',
body,
@@ -111,7 +114,7 @@ export const jobs = {
forceStopAndCloseJob(jobId: string) {
const body = JSON.stringify({ jobId });
- return http<{ success: boolean }>({
+ return httpService.http<{ success: boolean }>({
path: `${basePath()}/jobs/force_stop_and_close_job`,
method: 'POST',
body,
@@ -121,7 +124,7 @@ export const jobs = {
jobAuditMessages(jobId: string, from?: number) {
const jobIdString = jobId !== undefined ? `/${jobId}` : '';
const query = from !== undefined ? { from } : {};
- return http({
+ return httpService.http({
path: `${basePath()}/job_audit_messages/messages${jobIdString}`,
method: 'GET',
query,
@@ -129,7 +132,7 @@ export const jobs = {
},
deletingJobTasks() {
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/deleting_jobs_tasks`,
method: 'GET',
});
@@ -137,7 +140,7 @@ export const jobs = {
jobsExist(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/jobs_exist`,
method: 'POST',
body,
@@ -146,7 +149,7 @@ export const jobs = {
newJobCaps(indexPatternTitle: string, isRollup: boolean = false) {
const query = isRollup === true ? { rollup: true } : {};
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}`,
method: 'GET',
query,
@@ -175,7 +178,7 @@ export const jobs = {
splitFieldName,
splitFieldValue,
});
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/new_job_line_chart`,
method: 'POST',
body,
@@ -202,7 +205,7 @@ export const jobs = {
aggFieldNamePairs,
splitFieldName,
});
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/new_job_population_chart`,
method: 'POST',
body,
@@ -210,7 +213,7 @@ export const jobs = {
},
getAllJobAndGroupIds() {
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/all_jobs_and_group_ids`,
method: 'GET',
});
@@ -222,7 +225,7 @@ export const jobs = {
start,
end,
});
- return http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({
+ return httpService.http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({
path: `${basePath()}/jobs/look_back_progress`,
method: 'POST',
body,
@@ -249,7 +252,7 @@ export const jobs = {
end,
analyzer,
});
- return http<{
+ return httpService.http<{
examples: CategoryFieldExample[];
sampleSize: number;
overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS;
@@ -263,7 +266,10 @@ export const jobs = {
topCategories(jobId: string, count: number) {
const body = JSON.stringify({ jobId, count });
- return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({
+ return httpService.http<{
+ total: number;
+ categories: Array<{ count?: number; category: Category }>;
+ }>({
path: `${basePath()}/jobs/top_categories`,
method: 'POST',
body,
@@ -278,10 +284,13 @@ export const jobs = {
calendarEvents?: Array<{ start: number; end: number; description: string }>
) {
const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents });
- return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({
+ return httpService.http<{
+ total: number;
+ categories: Array<{ count?: number; category: Category }>;
+ }>({
path: `${basePath()}/jobs/revert_model_snapshot`,
method: 'POST',
body,
});
},
-};
+});
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
index 1b2c01ab73fce..b26528b76037b 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
@@ -14,9 +14,19 @@ export function resultsServiceProvider(
earliestMs: number,
latestMs: number,
interval: string | number,
- maxResults: number
+ perPage?: number,
+ fromPage?: number
+ ): Promise;
+ getTopInfluencers(
+ selectedJobIds: string[],
+ earliestMs: number,
+ latestMs: number,
+ maxFieldValues: number,
+ perPage?: number,
+ fromPage?: number,
+ influencers?: any[],
+ influencersFilterQuery?: any
): Promise;
- getTopInfluencers(): Promise;
getTopInfluencerValues(): Promise;
getOverallBucketScores(
jobIds: any,
@@ -33,6 +43,8 @@ export function resultsServiceProvider(
latestMs: number,
interval: string,
maxResults: number,
+ perPage: number,
+ fromPage: number,
influencersFilterQuery: any
): Promise;
getRecordInfluencers(): Promise;
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index 9e3fed189b6f4..55ddb1de3529e 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -9,6 +9,10 @@ import _ from 'lodash';
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
import { escapeForElasticsearchQuery } from '../../util/string_utils';
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
+import {
+ ANOMALY_SWIM_LANE_HARD_LIMIT,
+ SWIM_LANE_DEFAULT_PAGE_SIZE,
+} from '../../explorer/explorer_constants';
/**
* Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
@@ -24,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) {
// Pass an empty array or ['*'] to search over all job IDs.
// Returned response contains a results property, with a key for job
// which has results for the specified time range.
- getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) {
+ getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@@ -88,7 +92,7 @@ export function resultsServiceProvider(mlApiServices) {
jobId: {
terms: {
field: 'job_id',
- size: maxResults !== undefined ? maxResults : 5,
+ size: jobIds?.length ?? 1,
order: {
anomalyScore: 'desc',
},
@@ -99,6 +103,12 @@ export function resultsServiceProvider(mlApiServices) {
field: 'anomaly_score',
},
},
+ bucketTruncate: {
+ bucket_sort: {
+ from: (fromPage - 1) * perPage,
+ size: perPage === 0 ? 1 : perPage,
+ },
+ },
byTime: {
date_histogram: {
field: 'timestamp',
@@ -158,7 +168,9 @@ export function resultsServiceProvider(mlApiServices) {
jobIds,
earliestMs,
latestMs,
- maxFieldValues = 10,
+ maxFieldValues = ANOMALY_SWIM_LANE_HARD_LIMIT,
+ perPage = 10,
+ fromPage = 1,
influencers = [],
influencersFilterQuery
) {
@@ -272,6 +284,12 @@ export function resultsServiceProvider(mlApiServices) {
},
},
aggs: {
+ bucketTruncate: {
+ bucket_sort: {
+ from: (fromPage - 1) * perPage,
+ size: perPage,
+ },
+ },
maxAnomalyScore: {
max: {
field: 'influencer_score',
@@ -472,7 +490,9 @@ export function resultsServiceProvider(mlApiServices) {
earliestMs,
latestMs,
interval,
- maxResults,
+ maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT,
+ perPage = SWIM_LANE_DEFAULT_PAGE_SIZE,
+ fromPage = 1,
influencersFilterQuery
) {
return new Promise((resolve, reject) => {
@@ -565,10 +585,15 @@ export function resultsServiceProvider(mlApiServices) {
},
},
aggs: {
+ influencerValuesCardinality: {
+ cardinality: {
+ field: 'influencer_field_value',
+ },
+ },
influencerFieldValues: {
terms: {
field: 'influencer_field_value',
- size: maxResults !== undefined ? maxResults : 10,
+ size: !!maxResults ? maxResults : ANOMALY_SWIM_LANE_HARD_LIMIT,
order: {
maxAnomalyScore: 'desc',
},
@@ -579,6 +604,12 @@ export function resultsServiceProvider(mlApiServices) {
field: 'influencer_score',
},
},
+ bucketTruncate: {
+ bucket_sort: {
+ from: (fromPage - 1) * perPage,
+ size: perPage,
+ },
+ },
byTime: {
date_histogram: {
field: 'timestamp',
@@ -618,6 +649,8 @@ export function resultsServiceProvider(mlApiServices) {
obj.results[fieldValue] = fieldValues;
});
+ obj.cardinality = resp.aggregations?.influencerValuesCardinality?.value ?? 0;
+
resolve(obj);
})
.catch((resp) => {
diff --git a/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx b/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx
index 86c07a3577f7b..4f5d0723d65a4 100644
--- a/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx
+++ b/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx
@@ -9,4 +9,3 @@ import { Subject } from 'rxjs';
import { Refresh } from '../routing/use_refresh';
export const mlTimefilterRefresh$ = new Subject>();
-export const mlTimefilterTimeChange$ = new Subject>();
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
index 3b4562628051e..83070a5d94ba0 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
@@ -16,10 +16,10 @@ import {
IContainer,
} from '../../../../../../src/plugins/embeddable/public';
import { MlStartDependencies } from '../../plugin';
-import { ExplorerSwimlaneContainer } from './explorer_swimlane_container';
+import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
import { JobId } from '../../../common/types/anomaly_detection_jobs';
-import { ExplorerService } from '../../application/services/explorer_service';
+import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service';
import {
Filter,
Query,
@@ -40,7 +40,7 @@ export interface AnomalySwimlaneEmbeddableCustomInput {
jobIds: JobId[];
swimlaneType: SwimlaneType;
viewBy?: string;
- limit?: number;
+ perPage?: number;
// Embeddable inputs which are not included in the default interface
filters: Filter[];
@@ -58,12 +58,12 @@ export interface AnomalySwimlaneEmbeddableCustomOutput {
jobIds: JobId[];
swimlaneType: SwimlaneType;
viewBy?: string;
- limit?: number;
+ perPage?: number;
}
export interface AnomalySwimlaneServices {
anomalyDetectorService: AnomalyDetectorService;
- explorerService: ExplorerService;
+ anomalyTimelineService: AnomalyTimelineService;
}
export type AnomalySwimlaneEmbeddableServices = [
@@ -101,14 +101,20 @@ export class AnomalySwimlaneEmbeddable extends Embeddable<
super.render(node);
this.node = node;
+ const I18nContext = this.services[0].i18n.Context;
+
ReactDOM.render(
- this.updateOutput(output)}
- />,
+
+ {
+ this.updateInput(input);
+ }}
+ />
+ ,
node
);
}
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx
index 6b2ab89de8a5d..243369982ac1f 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx
@@ -46,6 +46,9 @@ describe('AnomalySwimlaneEmbeddableFactory', () => {
});
expect(Object.keys(createServices[0])).toEqual(Object.keys(coreStart));
expect(createServices[1]).toMatchObject(pluginsStart);
- expect(Object.keys(createServices[2])).toEqual(['anomalyDetectorService', 'explorerService']);
+ expect(Object.keys(createServices[2])).toEqual([
+ 'anomalyDetectorService',
+ 'anomalyTimelineService',
+ ]);
});
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
index 37c2cfb3e029b..0d587b428d89b 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
@@ -22,7 +22,7 @@ import {
import { MlStartDependencies } from '../../plugin';
import { HttpService } from '../../application/services/http_service';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
-import { ExplorerService } from '../../application/services/explorer_service';
+import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service';
import { mlResultsServiceProvider } from '../../application/services/results_service';
import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout';
import { mlApiServicesProvider } from '../../application/services/ml_api_service';
@@ -44,14 +44,10 @@ export class AnomalySwimlaneEmbeddableFactory
}
public async getExplicitInput(): Promise> {
- const [{ overlays, uiSettings }, , { anomalyDetectorService }] = await this.getServices();
+ const [coreStart] = await this.getServices();
try {
- return await resolveAnomalySwimlaneUserInput({
- anomalyDetectorService,
- overlays,
- uiSettings,
- });
+ return await resolveAnomalySwimlaneUserInput(coreStart);
} catch (e) {
return Promise.reject();
}
@@ -62,13 +58,13 @@ export class AnomalySwimlaneEmbeddableFactory
const httpService = new HttpService(coreStart.http);
const anomalyDetectorService = new AnomalyDetectorService(httpService);
- const explorerService = new ExplorerService(
+ const anomalyTimelineService = new AnomalyTimelineService(
pluginsStart.data.query.timefilter.timefilter,
coreStart.uiSettings,
mlResultsServiceProvider(mlApiServicesProvider(httpService))
);
- return [coreStart, pluginsStart, { anomalyDetectorService, explorerService }];
+ return [coreStart, pluginsStart, { anomalyDetectorService, anomalyTimelineService }];
}
public async create(
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
index 4977ece54bb57..be9a332e51dbc 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
@@ -27,7 +27,7 @@ export interface AnomalySwimlaneInitializerProps {
defaultTitle: string;
influencers: string[];
initialInput?: Partial<
- Pick
+ Pick
>;
onCreate: (swimlaneProps: {
panelTitle: string;
@@ -38,11 +38,6 @@ export interface AnomalySwimlaneInitializerProps {
onCancel: () => void;
}
-const limitOptions = [5, 10, 25, 50].map((limit) => ({
- value: limit,
- text: `${limit}`,
-}));
-
export const AnomalySwimlaneInitializer: FC = ({
defaultTitle,
influencers,
@@ -55,7 +50,6 @@ export const AnomalySwimlaneInitializer: FC = (
initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL
);
const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy);
- const [limit, setLimit] = useState(initialInput?.limit ?? 5);
const swimlaneTypeOptions = [
{
@@ -154,19 +148,6 @@ export const AnomalySwimlaneInitializer: FC = (
onChange={(e) => setViewBySwimlaneFieldName(e.target.value)}
/>
-
- }
- >
- setLimit(Number(e.target.value))}
- />
-
>
)}
@@ -186,7 +167,6 @@ export const AnomalySwimlaneInitializer: FC = (
panelTitle,
swimlaneType,
viewBy: viewBySwimlaneFieldName,
- limit,
})}
fill
>
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
index 54f50d2d3da32..1ffdadb60aaa3 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
@@ -5,10 +5,13 @@
*/
import React from 'react';
-import { IUiSettingsClient, OverlayStart } from 'kibana/public';
+import { CoreStart } from 'kibana/public';
import moment from 'moment';
import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants';
-import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
+import {
+ KibanaContextProvider,
+ toMountPoint,
+} from '../../../../../../src/plugins/kibana_react/public';
import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer';
import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
@@ -17,19 +20,17 @@ import {
AnomalySwimlaneEmbeddableInput,
getDefaultPanelTitle,
} from './anomaly_swimlane_embeddable';
+import { getMlGlobalServices } from '../../application/app';
+import { HttpService } from '../../application/services/http_service';
export async function resolveAnomalySwimlaneUserInput(
- {
- overlays,
- anomalyDetectorService,
- uiSettings,
- }: {
- anomalyDetectorService: AnomalyDetectorService;
- overlays: OverlayStart;
- uiSettings: IUiSettingsClient;
- },
+ coreStart: CoreStart,
input?: AnomalySwimlaneEmbeddableInput
): Promise> {
+ const { http, uiSettings, overlays } = coreStart;
+
+ const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
+
return new Promise(async (resolve, reject) => {
const maps = {
groupsMap: getInitialGroupsMap([]),
@@ -41,48 +42,50 @@ export async function resolveAnomalySwimlaneUserInput(
const selectedIds = input?.jobIds;
- const flyoutSession = overlays.openFlyout(
+ const flyoutSession = coreStart.overlays.openFlyout(
toMountPoint(
- {
- flyoutSession.close();
- reject();
- }}
- onSelectionConfirmed={async ({ jobIds, groups }) => {
- const title = input?.title ?? getDefaultPanelTitle(jobIds);
+
+ {
+ flyoutSession.close();
+ reject();
+ }}
+ onSelectionConfirmed={async ({ jobIds, groups }) => {
+ const title = input?.title ?? getDefaultPanelTitle(jobIds);
- const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
+ const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
- const influencers = anomalyDetectorService.extractInfluencers(jobs);
- influencers.push(VIEW_BY_JOB_LABEL);
+ const influencers = anomalyDetectorService.extractInfluencers(jobs);
+ influencers.push(VIEW_BY_JOB_LABEL);
- await flyoutSession.close();
+ await flyoutSession.close();
- const modalSession = overlays.openModal(
- toMountPoint(
- {
- modalSession.close();
- resolve({ jobIds, title: panelTitle, swimlaneType, viewBy, limit });
- }}
- onCancel={() => {
- modalSession.close();
- reject();
- }}
- />
- )
- );
- }}
- maps={maps}
- />
+ const modalSession = overlays.openModal(
+ toMountPoint(
+ {
+ modalSession.close();
+ resolve({ jobIds, title: panelTitle, swimlaneType, viewBy });
+ }}
+ onCancel={() => {
+ modalSession.close();
+ reject();
+ }}
+ />
+ )
+ );
+ }}
+ maps={maps}
+ />
+
),
{
'data-test-subj': 'mlAnomalySwimlaneEmbeddable',
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx
similarity index 73%
rename from x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx
rename to x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx
index 63ae89b5acdd1..846a3f543c2d4 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { ExplorerSwimlaneContainer } from './explorer_swimlane_container';
+import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container';
import { BehaviorSubject, Observable } from 'rxjs';
import { I18nProvider } from '@kbn/i18n/react';
import {
@@ -17,6 +17,7 @@ import { CoreStart } from 'kibana/public';
import { MlStartDependencies } from '../../plugin';
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
+import { SwimlaneContainer } from '../../application/explorer/swimlane_container';
jest.mock('./swimlane_input_resolver', () => ({
useSwimlaneInputResolver: jest.fn(() => {
@@ -24,12 +25,11 @@ jest.mock('./swimlane_input_resolver', () => ({
}),
}));
-jest.mock('../../application/explorer/explorer_swimlane', () => ({
- ExplorerSwimlane: jest.fn(),
-}));
-
-jest.mock('../../application/components/chart_tooltip', () => ({
- MlTooltipComponent: jest.fn(),
+jest.mock('../../application/explorer/swimlane_container', () => ({
+ SwimlaneContainer: jest.fn(() => {
+ return null;
+ }),
+ isViewBySwimLaneData: jest.fn(),
}));
const defaultOptions = { wrapper: I18nProvider };
@@ -38,6 +38,7 @@ describe('ExplorerSwimlaneContainer', () => {
let embeddableInput: BehaviorSubject>;
let refresh: BehaviorSubject;
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
+ const onInputChange = jest.fn();
beforeEach(() => {
embeddableInput = new BehaviorSubject({
@@ -61,25 +62,39 @@ describe('ExplorerSwimlaneContainer', () => {
};
(useSwimlaneInputResolver as jest.Mock).mockReturnValueOnce([
- mockOverallData,
SWIMLANE_TYPE.OVERALL,
- undefined,
+ mockOverallData,
+ 10,
+ jest.fn(),
+ {},
+ false,
+ null,
]);
- const { findByTestId } = render(
-
}
services={services}
refresh={refresh}
+ onInputChange={onInputChange}
/>,
defaultOptions
);
- expect(
- await findByTestId('mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable')
- ).toBeDefined();
+
+ const calledWith = ((SwimlaneContainer as unknown) as jest.Mock).mock
+ .calls[0][0];
+
+ expect(calledWith).toMatchObject({
+ perPage: 10,
+ swimlaneType: SWIMLANE_TYPE.OVERALL,
+ swimlaneData: mockOverallData,
+ isLoading: false,
+ swimlaneLimit: undefined,
+ fromPage: 1,
+ });
});
test('should render an error in case it could not fetch the ML swimlane data', async () => {
@@ -87,38 +102,25 @@ describe('ExplorerSwimlaneContainer', () => {
undefined,
undefined,
undefined,
+ undefined,
+ undefined,
+ false,
{ message: 'Something went wrong' },
]);
const { findByText } = render(
-
}
services={services}
refresh={refresh}
+ onInputChange={onInputChange}
/>,
defaultOptions
);
const errorMessage = await findByText('Something went wrong');
expect(errorMessage).toBeDefined();
});
-
- test('should render a loading indicator during the data fetching', async () => {
- const { findByTestId } = render(
-
- }
- services={services}
- refresh={refresh}
- />,
- defaultOptions
- );
- expect(
- await findByTestId('loading_mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable')
- ).toBeDefined();
- });
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx
new file mode 100644
index 0000000000000..5d91bdb41df6a
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useState } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { Observable } from 'rxjs';
+
+import { CoreStart } from 'kibana/public';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MlStartDependencies } from '../../plugin';
+import {
+ AnomalySwimlaneEmbeddableInput,
+ AnomalySwimlaneEmbeddableOutput,
+ AnomalySwimlaneServices,
+} from './anomaly_swimlane_embeddable';
+import { useSwimlaneInputResolver } from './swimlane_input_resolver';
+import { SwimlaneType } from '../../application/explorer/explorer_constants';
+import {
+ isViewBySwimLaneData,
+ SwimlaneContainer,
+} from '../../application/explorer/swimlane_container';
+
+export interface ExplorerSwimlaneContainerProps {
+ id: string;
+ embeddableInput: Observable;
+ services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
+ refresh: Observable;
+ onInputChange: (output: Partial) => void;
+}
+
+export const EmbeddableSwimLaneContainer: FC = ({
+ id,
+ embeddableInput,
+ services,
+ refresh,
+ onInputChange,
+}) => {
+ const [chartWidth, setChartWidth] = useState(0);
+ const [fromPage, setFromPage] = useState(1);
+
+ const [
+ swimlaneType,
+ swimlaneData,
+ perPage,
+ setPerPage,
+ timeBuckets,
+ isLoading,
+ error,
+ ] = useSwimlaneInputResolver(
+ embeddableInput,
+ onInputChange,
+ refresh,
+ services,
+ chartWidth,
+ fromPage
+ );
+
+ if (error) {
+ return (
+
+ }
+ color="danger"
+ iconType="alert"
+ style={{ width: '100%' }}
+ >
+ {error.message}
+
+ );
+ }
+
+ return (
+
+ {
+ setChartWidth(width);
+ }}
+ onPaginationChange={(update) => {
+ if (update.fromPage) {
+ setFromPage(update.fromPage);
+ }
+ if (update.perPage) {
+ setFromPage(1);
+ setPerPage(update.perPage);
+ }
+ }}
+ isLoading={isLoading}
+ noDataWarning={
+
+ }
+ />
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
deleted file mode 100644
index db2b9d55cfabb..0000000000000
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
+++ /dev/null
@@ -1,126 +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, { FC, useCallback, useState } from 'react';
-import {
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingChart,
- EuiResizeObserver,
- EuiSpacer,
- EuiText,
-} from '@elastic/eui';
-import { Observable } from 'rxjs';
-
-import { throttle } from 'lodash';
-import { CoreStart } from 'kibana/public';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { ExplorerSwimlane } from '../../application/explorer/explorer_swimlane';
-import { MlStartDependencies } from '../../plugin';
-import {
- AnomalySwimlaneEmbeddableInput,
- AnomalySwimlaneEmbeddableOutput,
- AnomalySwimlaneServices,
-} from './anomaly_swimlane_embeddable';
-import { MlTooltipComponent } from '../../application/components/chart_tooltip';
-import { useSwimlaneInputResolver } from './swimlane_input_resolver';
-import { SwimlaneType } from '../../application/explorer/explorer_constants';
-
-const RESIZE_THROTTLE_TIME_MS = 500;
-
-export interface ExplorerSwimlaneContainerProps {
- id: string;
- embeddableInput: Observable;
- services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
- refresh: Observable;
- onOutputChange?: (output: Partial) => void;
-}
-
-export const ExplorerSwimlaneContainer: FC = ({
- id,
- embeddableInput,
- services,
- refresh,
-}) => {
- const [chartWidth, setChartWidth] = useState(0);
-
- const [swimlaneType, swimlaneData, timeBuckets, error] = useSwimlaneInputResolver(
- embeddableInput,
- refresh,
- services,
- chartWidth
- );
-
- const onResize = useCallback(
- throttle((e: { width: number; height: number }) => {
- const labelWidth = 200;
- setChartWidth(e.width - labelWidth);
- }, RESIZE_THROTTLE_TIME_MS),
- []
- );
-
- if (error) {
- return (
-
- }
- color="danger"
- iconType="alert"
- style={{ width: '100%' }}
- >
- {error.message}
-
- );
- }
-
- return (
-
- {(resizeRef) => (
- {
- resizeRef(el);
- }}
- >
-
-
-
- {chartWidth > 0 && swimlaneData && swimlaneType ? (
-
-
- {(tooltipService) => (
-
- )}
-
-
- ) : (
-
-
-
-
-
- )}
-
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts
index 890c2bde6305d..a34955adebf62 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts
@@ -19,6 +19,7 @@ describe('useSwimlaneInputResolver', () => {
let embeddableInput: BehaviorSubject>;
let refresh: Subject;
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
+ let onInputChange: jest.Mock;
beforeEach(() => {
jest.useFakeTimers();
@@ -41,7 +42,7 @@ describe('useSwimlaneInputResolver', () => {
} as CoreStart,
(null as unknown) as MlStartDependencies,
({
- explorerService: {
+ anomalyTimelineService: {
setTimeRange: jest.fn(),
loadOverallData: jest.fn(() =>
Promise.resolve({
@@ -69,6 +70,7 @@ describe('useSwimlaneInputResolver', () => {
},
} as unknown) as AnomalySwimlaneServices,
];
+ onInputChange = jest.fn();
});
afterEach(() => {
jest.useRealTimers();
@@ -79,9 +81,11 @@ describe('useSwimlaneInputResolver', () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSwimlaneInputResolver(
embeddableInput as Observable,
+ onInputChange,
refresh,
services,
- 1000
+ 1000,
+ 1
)
);
@@ -94,7 +98,7 @@ describe('useSwimlaneInputResolver', () => {
});
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1);
- expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(1);
+ expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(1);
await act(async () => {
embeddableInput.next({
@@ -109,7 +113,7 @@ describe('useSwimlaneInputResolver', () => {
});
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
- expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(2);
+ expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(2);
await act(async () => {
embeddableInput.next({
@@ -124,7 +128,7 @@ describe('useSwimlaneInputResolver', () => {
});
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
- expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(3);
+ expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3);
});
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
index 3829bbce5e5c9..9ed6f88150f68 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
@@ -16,23 +16,31 @@ import {
skipWhile,
startWith,
switchMap,
+ tap,
} from 'rxjs/operators';
import { CoreStart } from 'kibana/public';
import { TimeBuckets } from '../../application/util/time_buckets';
import {
AnomalySwimlaneEmbeddableInput,
+ AnomalySwimlaneEmbeddableOutput,
AnomalySwimlaneServices,
} from './anomaly_swimlane_embeddable';
import { MlStartDependencies } from '../../plugin';
-import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants';
+import {
+ ANOMALY_SWIM_LANE_HARD_LIMIT,
+ SWIM_LANE_DEFAULT_PAGE_SIZE,
+ SWIMLANE_TYPE,
+ SwimlaneType,
+} from '../../application/explorer/explorer_constants';
import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters';
import { Query } from '../../../../../../src/plugins/data/common/query';
import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public';
import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils';
import { parseInterval } from '../../../common/util/parse_interval';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
+import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container';
+import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
-const RESIZE_IGNORED_DIFF_PX = 20;
const FETCH_RESULTS_DEBOUNCE_MS = 500;
function getJobsObservable(
@@ -48,17 +56,31 @@ function getJobsObservable(
export function useSwimlaneInputResolver(
embeddableInput: Observable,
+ onInputChange: (output: Partial) => void,
refresh: Observable,
services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices],
- chartWidth: number
-): [string | undefined, OverallSwimlaneData | undefined, TimeBuckets, Error | null | undefined] {
- const [{ uiSettings }, , { explorerService, anomalyDetectorService }] = services;
+ chartWidth: number,
+ fromPage: number
+): [
+ string | undefined,
+ OverallSwimlaneData | undefined,
+ number,
+ (perPage: number) => void,
+ TimeBuckets,
+ boolean,
+ Error | null | undefined
+] {
+ const [{ uiSettings }, , { anomalyTimelineService, anomalyDetectorService }] = services;
const [swimlaneData, setSwimlaneData] = useState();
const [swimlaneType, setSwimlaneType] = useState();
const [error, setError] = useState();
+ const [perPage, setPerPage] = useState();
+ const [isLoading, setIsLoading] = useState(false);
const chartWidth$ = useMemo(() => new Subject(), []);
+ const fromPage$ = useMemo(() => new Subject(), []);
+ const perPage$ = useMemo(() => new Subject(), []);
const timeBuckets = useMemo(() => {
return new TimeBuckets({
@@ -73,28 +95,32 @@ export function useSwimlaneInputResolver(
const subscription = combineLatest([
getJobsObservable(embeddableInput, anomalyDetectorService),
embeddableInput,
- chartWidth$.pipe(
- skipWhile((v) => !v),
- distinctUntilChanged((prev, curr) => {
- // emit only if the width has been changed significantly
- return Math.abs(curr - prev) < RESIZE_IGNORED_DIFF_PX;
- })
+ chartWidth$.pipe(skipWhile((v) => !v)),
+ fromPage$,
+ perPage$.pipe(
+ startWith(undefined),
+ // no need to emit when the initial value has been set
+ distinctUntilChanged(
+ (prev, curr) => prev === undefined && curr === SWIM_LANE_DEFAULT_PAGE_SIZE
+ )
),
refresh.pipe(startWith(null)),
])
.pipe(
+ tap(setIsLoading.bind(null, true)),
debounceTime(FETCH_RESULTS_DEBOUNCE_MS),
- switchMap(([jobs, input, swimlaneContainerWidth]) => {
+ switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => {
const {
viewBy,
swimlaneType: swimlaneTypeInput,
- limit,
+ perPage: perPageInput,
timeRange,
filters,
query,
+ viewMode,
} = input;
- explorerService.setTimeRange(timeRange);
+ anomalyTimelineService.setTimeRange(timeRange);
if (!swimlaneType) {
setSwimlaneType(swimlaneTypeInput);
@@ -118,18 +144,34 @@ export function useSwimlaneInputResolver(
return of(undefined);
}
- return from(explorerService.loadOverallData(explorerJobs, swimlaneContainerWidth)).pipe(
+ return from(
+ anomalyTimelineService.loadOverallData(explorerJobs, swimlaneContainerWidth)
+ ).pipe(
switchMap((overallSwimlaneData) => {
const { earliest, latest } = overallSwimlaneData;
if (overallSwimlaneData && swimlaneTypeInput === SWIMLANE_TYPE.VIEW_BY) {
+ if (perPageFromState === undefined) {
+ // set initial pagination from the input or default one
+ setPerPage(perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE);
+ }
+
+ if (viewMode === ViewMode.EDIT && perPageFromState !== perPageInput) {
+ // store per page value when the dashboard is in the edit mode
+ onInputChange({ perPage: perPageFromState });
+ }
+
return from(
- explorerService.loadViewBySwimlane(
+ anomalyTimelineService.loadViewBySwimlane(
[],
{ earliest, latest },
explorerJobs,
viewBy!,
- limit!,
+ isViewBySwimLaneData(swimlaneData)
+ ? swimlaneData.cardinality
+ : ANOMALY_SWIM_LANE_HARD_LIMIT,
+ perPageFromState ?? perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE,
+ fromPageInput,
swimlaneContainerWidth,
appliedFilters
)
@@ -156,6 +198,7 @@ export function useSwimlaneInputResolver(
if (data !== undefined) {
setError(null);
setSwimlaneData(data);
+ setIsLoading(false);
}
});
@@ -164,11 +207,28 @@ export function useSwimlaneInputResolver(
};
}, []);
+ useEffect(() => {
+ fromPage$.next(fromPage);
+ }, [fromPage]);
+
+ useEffect(() => {
+ if (perPage === undefined) return;
+ perPage$.next(perPage);
+ }, [perPage]);
+
useEffect(() => {
chartWidth$.next(chartWidth);
}, [chartWidth]);
- return [swimlaneType, swimlaneData, timeBuckets, error];
+ return [
+ swimlaneType,
+ swimlaneData,
+ perPage ?? SWIM_LANE_DEFAULT_PAGE_SIZE,
+ setPerPage,
+ timeBuckets,
+ isLoading,
+ error,
+ ];
}
export function processFilters(filters: Filter[], query: Query) {
diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx
index 312b9f31124b1..0db41c1ed104e 100644
--- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx
+++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx
@@ -14,8 +14,6 @@ import {
AnomalySwimlaneEmbeddableOutput,
} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable';
import { resolveAnomalySwimlaneUserInput } from '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout';
-import { HttpService } from '../application/services/http_service';
-import { AnomalyDetectorService } from '../application/services/anomaly_detector_service';
export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction';
@@ -39,18 +37,10 @@ export function createEditSwimlanePanelAction(getStartServices: CoreSetup['getSt
throw new Error('Not possible to execute an action without the embeddable context');
}
- const [{ overlays, uiSettings, http }] = await getStartServices();
- const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
+ const [coreStart] = await getStartServices();
try {
- const result = await resolveAnomalySwimlaneUserInput(
- {
- anomalyDetectorService,
- overlays,
- uiSettings,
- },
- embeddable.getInput()
- );
+ const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput());
embeddable.updateInput(result);
} catch (e) {
return Promise.reject();
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
index 98f4b4336a1c8..86cccff957211 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
@@ -60,6 +60,24 @@ export function ancestryArray(event: ResolverEvent): string[] | undefined {
return event.process.Ext.ancestry;
}
+export function getAncestryAsArray(event: ResolverEvent | undefined): string[] {
+ if (!event) {
+ return [];
+ }
+
+ const ancestors = ancestryArray(event);
+ if (ancestors) {
+ return ancestors;
+ }
+
+ const parentID = parentEntityId(event);
+ if (parentID) {
+ return [parentID];
+ }
+
+ return [];
+}
+
/**
* @param event The event to get the category for
*/
diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
index 398e2710b3253..42cbc2327fc28 100644
--- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
@@ -13,7 +13,6 @@ export const validateTree = {
params: schema.object({ id: schema.string() }),
query: schema.object({
children: schema.number({ defaultValue: 10, min: 0, max: 100 }),
- generations: schema.number({ defaultValue: 3, min: 0, max: 3 }),
ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }),
events: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
alerts: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
@@ -66,7 +65,6 @@ export const validateChildren = {
params: schema.object({ id: schema.string() }),
query: schema.object({
children: schema.number({ defaultValue: 10, min: 1, max: 100 }),
- generations: schema.number({ defaultValue: 3, min: 1, max: 3 }),
afterChild: schema.maybe(schema.string()),
legacyEndpointID: schema.maybe(schema.string()),
}),
diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts
index 4efe89b2429ad..42b1337a91464 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types.ts
@@ -77,12 +77,18 @@ export interface ResolverNodeStats {
*/
export interface ResolverChildNode extends ResolverLifecycleNode {
/**
- * A child node's pagination cursor can be null for a couple reasons:
- * 1. At the time of querying it could have no children in ES, in which case it will be marked as
- * null because we know it does not have children during this query.
- * 2. If the max level was reached we do not know if this node has children or not so we'll mark it as null
+ * nextChild can have 3 different states:
+ *
+ * undefined: This indicates that you should not use this node for additional queries. It does not mean that node does
+ * not have any more direct children. The node could have more direct children but to determine that, use the
+ * ResolverChildren node's nextChild.
+ *
+ * null: Indicates that we have received all the children of the node. There may be more descendants though.
+ *
+ * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants
+ * using this node's entity_id
*/
- nextChild: string | null;
+ nextChild?: string | null;
}
/**
@@ -92,7 +98,14 @@ export interface ResolverChildNode extends ResolverLifecycleNode {
export interface ResolverChildren {
childNodes: ResolverChildNode[];
/**
- * This is the children cursor for the origin of a tree.
+ * nextChild can have 2 different states:
+ *
+ * null: Indicates that we have received all the descendants that can be retrieved using this node. To retrieve more
+ * nodes in the tree use a cursor provided in one of the returned children. If no other cursor exists then the tree
+ * is complete.
+ *
+ * string: Indicates this node has more descendants that can be retrieved, pass this cursor in while using this node's
+ * entity_id for the request.
*/
nextChild: string | null;
}
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
index 9e9732a403f8f..2a1a2d2c8e194 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
@@ -64,7 +64,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { ALERTS_URL } from '../urls/navigation';
-describe('Detection rules, custom', () => {
+// // Skipped as was causing failures on master
+describe.skip('Detection rules, custom', () => {
before(() => {
esArchiverLoad('custom_rule_with_timeline');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
index 25fc1fc3a7c11..06e9228de4f49 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
@@ -17,7 +17,8 @@ import { ALERTS_URL } from '../urls/navigation';
const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson';
-describe('Export rules', () => {
+// Skipped as was causing failures on master
+describe.skip('Export rules', () => {
before(() => {
esArchiverLoad('custom_rules');
cy.server();
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx
index ff6e8859be049..98d4d3bd8faba 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx
@@ -7,7 +7,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import { MapToolTipComponent } from './map_tool_tip';
-import { MapFeature } from '../types';
+import { TooltipFeature } from '../../../../../../maps/common/descriptor_types';
describe('MapToolTip', () => {
test('placeholder component renders correctly against snapshot', () => {
@@ -18,10 +18,11 @@ describe('MapToolTip', () => {
test('full component renders correctly against snapshot', () => {
const addFilters = jest.fn();
const closeTooltip = jest.fn();
- const features: MapFeature[] = [
+ const features: TooltipFeature[] = [
{
id: 1,
layerId: 'layerId',
+ mbProperties: {},
},
];
const getLayerName = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts
index f91fd677ba7fe..e3ca3c5b84289 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts
@@ -36,11 +36,6 @@ export type SetQuery = (params: {
refetch: inputsModel.Refetch;
}) => void;
-export interface MapFeature {
- id: number;
- layerId: string;
-}
-
export interface FeatureGeometry {
coordinates: [number];
type: string;
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts
index 74448a324a4ec..9b8cd9fd3edab 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts
@@ -18,14 +18,14 @@ export function handleChildren(
return async (context, req, res) => {
const {
params: { id },
- query: { children, generations, afterChild, legacyEndpointID: endpointID },
+ query: { children, afterChild, legacyEndpointID: endpointID },
} = req;
try {
const client = context.core.elasticsearch.legacy.client;
const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID);
return res.ok({
- body: await fetcher.children(children, generations, afterChild),
+ body: await fetcher.children(children, afterChild),
});
} catch (err) {
log.warn(err);
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts
index 95bc612c58a1b..feb4a404b2359 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts
@@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
-import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
+import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
/**
* Builds a query for retrieving alerts for a node.
*/
-export class AlertsQuery extends ResolverQuery {
+export class AlertsQuery extends ResolverQuery {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
@@ -38,11 +38,7 @@ export class AlertsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(
- uniquePIDs.length,
- 'endgame.serial_event_id',
- 'endgame.unique_pid'
- ),
+ ...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}
@@ -60,14 +56,11 @@ export class AlertsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'),
+ ...this.pagination.buildQueryFields('event.id'),
};
}
- formatResponse(response: SearchResponse): PaginatedResults {
- return {
- results: ResolverQuery.getResults(response),
- totals: PaginationBuilder.getTotals(response.aggregations),
- };
+ formatResponse(response: SearchResponse): ResolverEvent[] {
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts
index 35f8cad01e672..1b6a8f2f83387 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts
@@ -14,10 +14,12 @@ import { MSearchQuery } from './multi_searcher';
/**
* ResolverQuery provides the base structure for queries to retrieve events when building a resolver graph.
*
- * @param T the structured return type of a resolver query. This represents the type that is returned when translating
- * Elasticsearch's SearchResponse response.
+ * @param T the structured return type of a resolver query. This represents the final return type of the query after handling
+ * any aggregations.
+ * @param R the is the type after transforming ES's response. Making this definable let's us set whether it is a resolver event
+ * or something else.
*/
-export abstract class ResolverQuery implements MSearchQuery {
+export abstract class ResolverQuery implements MSearchQuery {
/**
*
* @param indexPattern the index pattern to use in the query for finding indices with documents in ES.
@@ -50,7 +52,7 @@ export abstract class ResolverQuery implements MSearchQuery {
};
}
- protected static getResults(response: SearchResponse): ResolverEvent[] {
+ protected getResults(response: SearchResponse): R[] {
return response.hits.hits.map((hit) => hit._source);
}
@@ -68,19 +70,26 @@ export abstract class ResolverQuery implements MSearchQuery {
}
/**
- * Searches ES for the specified ids.
+ * Searches ES for the specified ids and format the response.
*
* @param client a client for searching ES
* @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid)
*/
- async search(client: ILegacyScopedClusterClient, ids: string | string[]): Promise {
- const res: SearchResponse = await client.callAsCurrentUser(
- 'search',
- this.buildSearch(ids)
- );
+ async searchAndFormat(client: ILegacyScopedClusterClient, ids: string | string[]): Promise {
+ const res: SearchResponse = await this.search(client, ids);
return this.formatResponse(res);
}
+ /**
+ * Searches ES for the specified ids but do not format the response.
+ *
+ * @param client a client for searching ES
+ * @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid)
+ */
+ async search(client: ILegacyScopedClusterClient, ids: string | string[]) {
+ return client.callAsCurrentUser('search', this.buildSearch(ids));
+ }
+
/**
* Builds a query to search the legacy data format.
*
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts
index a4d4cd546ef60..8175764b3a0a2 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts
@@ -25,7 +25,7 @@ describe('Children query', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const msearch: any = query.buildMSearch(['1234', '5678']);
expect(msearch[0].index).toBe('index-pattern');
- expect(msearch[1].query.bool.filter[0]).toStrictEqual({
+ expect(msearch[1].query.bool.filter[0].bool.should[0]).toStrictEqual({
terms: { 'process.parent.entity_id': ['1234', '5678'] },
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts
index b7b1a16926a15..7fd3808662baa 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts
@@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
-import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
+import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
/**
* Builds a query for retrieving descendants of a node.
*/
-export class ChildrenQuery extends ResolverQuery {
+export class ChildrenQuery extends ResolverQuery {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
@@ -53,11 +53,7 @@ export class ChildrenQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(
- uniquePIDs.length,
- 'endgame.serial_event_id',
- 'endgame.unique_ppid'
- ),
+ ...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}
@@ -67,7 +63,16 @@ export class ChildrenQuery extends ResolverQuery {
bool: {
filter: [
{
- terms: { 'process.parent.entity_id': entityIDs },
+ bool: {
+ should: [
+ {
+ terms: { 'process.parent.entity_id': entityIDs },
+ },
+ {
+ terms: { 'process.Ext.ancestry': entityIDs },
+ },
+ ],
+ },
},
{
term: { 'event.category': 'process' },
@@ -81,14 +86,11 @@ export class ChildrenQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.parent.entity_id'),
+ ...this.pagination.buildQueryFields('event.id'),
};
}
- formatResponse(response: SearchResponse): PaginatedResults {
- return {
- results: ResolverQuery.getResults(response),
- totals: PaginationBuilder.getTotals(response.aggregations),
- };
+ formatResponse(response: SearchResponse): ResolverEvent[] {
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
index ec65e30d1d5d4..abc86826e77dd 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
@@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
-import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
+import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
/**
* Builds a query for retrieving related events for a node.
*/
-export class EventsQuery extends ResolverQuery {
+export class EventsQuery extends ResolverQuery {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
@@ -45,11 +45,7 @@ export class EventsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(
- uniquePIDs.length,
- 'endgame.serial_event_id',
- 'endgame.unique_pid'
- ),
+ ...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}
@@ -74,14 +70,11 @@ export class EventsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'),
+ ...this.pagination.buildQueryFields('event.id'),
};
}
- formatResponse(response: SearchResponse): PaginatedResults {
- return {
- results: ResolverQuery.getResults(response),
- totals: PaginationBuilder.getTotals(response.aggregations),
- };
+ formatResponse(response: SearchResponse): ResolverEvent[] {
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts
index 93910293b00af..0b5728958e91f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts
@@ -60,6 +60,6 @@ export class LifecycleQuery extends ResolverQuery {
}
formatResponse(response: SearchResponse): ResolverEvent[] {
- return ResolverQuery.getResults(response);
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts
index f873ab3019f64..02dbd92d9252b 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts
@@ -5,7 +5,7 @@
*/
import { ILegacyScopedClusterClient } from 'kibana/server';
-import { MSearchResponse } from 'elasticsearch';
+import { MSearchResponse, SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
@@ -34,6 +34,10 @@ export interface QueryInfo {
* one or many unique identifiers to be searched for in this query
*/
ids: string | string[];
+ /**
+ * a function to handle the response
+ */
+ handler: (response: SearchResponse) => void;
}
/**
@@ -57,10 +61,10 @@ export class MultiSearcher {
throw new Error('No queries provided to MultiSearcher');
}
- let searchQuery: JsonObject[] = [];
- queries.forEach(
- (info) => (searchQuery = [...searchQuery, ...info.query.buildMSearch(info.ids)])
- );
+ const searchQuery: JsonObject[] = [];
+ for (const info of queries) {
+ searchQuery.push(...info.query.buildMSearch(info.ids));
+ }
const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', {
body: searchQuery,
});
@@ -72,6 +76,8 @@ export class MultiSearcher {
if (res.responses.length !== queries.length) {
throw new Error(`Responses length was: ${res.responses.length} expected ${queries.length}`);
}
- return res.responses;
+ for (let i = 0; i < queries.length; i++) {
+ queries[i].handler(res.responses[i]);
+ }
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts
index a728054bef219..b8fa409e2ca21 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts
@@ -7,13 +7,17 @@ import { SearchResponse } from 'elasticsearch';
import { ResolverQuery } from './base';
import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
-import { AggBucket } from '../utils/pagination';
export interface StatsResult {
alerts: Record;
events: Record;
}
+interface AggBucket {
+ key: string;
+ doc_count: number;
+}
+
interface CategoriesAgg extends AggBucket {
/**
* The reason categories is optional here is because if no data was returned in the query the categories aggregation
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts
index 181fb8c3df3f9..33011078ee823 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts
@@ -21,7 +21,6 @@ export function handleTree(
params: { id },
query: {
children,
- generations,
ancestors,
events,
alerts,
@@ -37,7 +36,7 @@ export function handleTree(
const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID);
const [childrenNodes, ancestry, relatedEvents, relatedAlerts] = await Promise.all([
- fetcher.children(children, generations, afterChild),
+ fetcher.children(children, afterChild),
fetcher.ancestors(ancestors),
fetcher.events(events, afterEvent),
fetcher.alerts(alerts, afterAlert),
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts
new file mode 100644
index 0000000000000..ae17cf4c3a562
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { SearchResponse } from 'elasticsearch';
+import { ILegacyScopedClusterClient } from 'kibana/server';
+import { ResolverRelatedAlerts, ResolverEvent } from '../../../../../common/endpoint/types';
+import { createRelatedAlerts } from './node';
+import { AlertsQuery } from '../queries/alerts';
+import { PaginationBuilder } from './pagination';
+import { QueryInfo } from '../queries/multi_searcher';
+import { SingleQueryHandler } from './fetch';
+
+/**
+ * Requests related alerts for the given node.
+ */
+export class RelatedAlertsQueryHandler implements SingleQueryHandler {
+ private relatedAlerts: ResolverRelatedAlerts | undefined;
+ private readonly query: AlertsQuery;
+ constructor(
+ private readonly limit: number,
+ private readonly entityID: string,
+ after: string | undefined,
+ indexPattern: string,
+ legacyEndpointID: string | undefined
+ ) {
+ this.query = new AlertsQuery(
+ PaginationBuilder.createBuilder(limit, after),
+ indexPattern,
+ legacyEndpointID
+ );
+ }
+
+ private handleResponse = (response: SearchResponse) => {
+ const results = this.query.formatResponse(response);
+ this.relatedAlerts = createRelatedAlerts(
+ this.entityID,
+ results,
+ PaginationBuilder.buildCursorRequestLimit(this.limit, results)
+ );
+ };
+
+ /**
+ * Builds a QueryInfo object that defines the related alerts to search for and how to handle the response.
+ *
+ * This will return undefined onces the results have been retrieved from ES.
+ */
+ nextQuery(): QueryInfo | undefined {
+ if (this.getResults()) {
+ return;
+ }
+
+ return {
+ query: this.query,
+ ids: this.entityID,
+ handler: this.handleResponse,
+ };
+ }
+
+ /**
+ * Get the results after an msearch.
+ */
+ getResults() {
+ return this.relatedAlerts;
+ }
+
+ /**
+ * Perform a regular search and return the results.
+ *
+ * @param client the elasticsearch client
+ */
+ async search(client: ILegacyScopedClusterClient) {
+ const results = this.getResults();
+ if (results) {
+ return results;
+ }
+
+ this.handleResponse(await this.query.search(client, this.entityID));
+ return this.getResults() ?? createRelatedAlerts(this.entityID);
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
new file mode 100644
index 0000000000000..9bf16dac791d7
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 { SearchResponse } from 'elasticsearch';
+import { ILegacyScopedClusterClient } from 'kibana/server';
+import {
+ parentEntityId,
+ entityId,
+ getAncestryAsArray,
+} from '../../../../../common/endpoint/models/event';
+import {
+ ResolverAncestry,
+ ResolverEvent,
+ ResolverLifecycleNode,
+} from '../../../../../common/endpoint/types';
+import { createAncestry, createLifecycle } from './node';
+import { LifecycleQuery } from '../queries/lifecycle';
+import { QueryInfo } from '../queries/multi_searcher';
+import { QueryHandler } from './fetch';
+
+/**
+ * Retrieve the ancestry portion of a resolver tree.
+ */
+export class AncestryQueryHandler implements QueryHandler {
+ private readonly ancestry: ResolverAncestry = createAncestry();
+ private ancestorsToFind: string[];
+ private readonly query: LifecycleQuery;
+
+ constructor(
+ private levels: number,
+ indexPattern: string,
+ legacyEndpointID: string | undefined,
+ originNode: ResolverLifecycleNode | undefined
+ ) {
+ this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels);
+ this.query = new LifecycleQuery(indexPattern, legacyEndpointID);
+
+ // add the origin node to the response if it exists
+ if (originNode) {
+ this.ancestry.ancestors.push(originNode);
+ this.ancestry.nextAncestor = parentEntityId(originNode.lifecycle[0]) || null;
+ }
+ }
+
+ private toMapOfNodes(results: ResolverEvent[]) {
+ return results.reduce((nodes: Map, event: ResolverEvent) => {
+ const nodeId = entityId(event);
+ let node = nodes.get(nodeId);
+ if (!node) {
+ node = createLifecycle(nodeId, []);
+ }
+
+ node.lifecycle.push(event);
+ return nodes.set(nodeId, node);
+ }, new Map());
+ }
+
+ private setNoMore() {
+ this.ancestry.nextAncestor = null;
+ this.ancestorsToFind = [];
+ this.levels = 0;
+ }
+
+ private handleResponse = (searchResp: SearchResponse) => {
+ const results = this.query.formatResponse(searchResp);
+ if (results.length === 0) {
+ this.setNoMore();
+ return;
+ }
+
+ // bucket the start and end events together for a single node
+ const ancestryNodes = this.toMapOfNodes(results);
+
+ // the order of this array is going to be weird, it will look like this
+ // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent]
+ this.ancestry.ancestors.push(...ancestryNodes.values());
+ this.ancestry.nextAncestor = parentEntityId(results[0]) || null;
+ this.levels = this.levels - ancestryNodes.size;
+ // the results come back in ascending order on timestamp so the first entry in the
+ // results should be the further ancestor (most distant grandparent)
+ this.ancestorsToFind = getAncestryAsArray(results[0]).slice(0, this.levels);
+ };
+
+ /**
+ * Returns whether there are more results to retrieve based on the limit that is passed in and the results that
+ * have already been received from ES.
+ */
+ hasMore(): boolean {
+ return this.levels > 0 && this.ancestorsToFind.length > 0;
+ }
+
+ /**
+ * Get a query info for retrieving the next set of results.
+ */
+ nextQuery(): QueryInfo | undefined {
+ if (this.hasMore()) {
+ return {
+ query: this.query,
+ ids: this.ancestorsToFind,
+ handler: this.handleResponse,
+ };
+ }
+ }
+
+ /**
+ * Return the results after using msearch to find them.
+ */
+ getResults() {
+ return this.ancestry;
+ }
+
+ /**
+ * Perform a regular search and return the results.
+ *
+ * @param client the elasticsearch client.
+ */
+ async search(client: ILegacyScopedClusterClient) {
+ while (this.hasMore()) {
+ const info = this.nextQuery();
+ if (!info) {
+ break;
+ }
+ this.handleResponse(await this.query.search(client, info.ids));
+ }
+ return this.getResults();
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts
index 1d55cb7cfd735..ca5b5aef0f651 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts
@@ -3,95 +3,195 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
-
-import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
+import {
+ EndpointDocGenerator,
+ Tree,
+ Event,
+ TreeNode,
+} from '../../../../../common/endpoint/generate_data';
import { ChildrenNodesHelper } from './children_helper';
-import { eventId, entityId, parentEntityId } from '../../../../../common/endpoint/models/event';
-import { ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types';
-
-function findParents(events: ResolverEvent[]): ResolverEvent[] {
- const cache = _.groupBy(events, entityId);
+import { eventId, isProcessStart } from '../../../../../common/endpoint/models/event';
- const parents: ResolverEvent[] = [];
- Object.values(cache).forEach((lifecycle) => {
- const parentNode = cache[parentEntityId(lifecycle[0])!];
- if (parentNode) {
- parents.push(parentNode[0]);
+function getStartEvents(events: Event[]): Event[] {
+ const startEvents: Event[] = [];
+ for (const event of events) {
+ if (isProcessStart(event)) {
+ startEvents.push(event);
}
- });
- return parents;
+ }
+ return startEvents;
}
-function findNode(tree: ResolverChildren, id: string) {
- return tree.childNodes.find((node) => {
- return node.entityID === id;
- });
+function getAllChildrenEvents(tree: Tree) {
+ const children: Event[] = [];
+ for (const child of tree.children.values()) {
+ children.push(...child.lifecycle);
+ }
+ return children;
+}
+
+function getStartEventsFromLevels(levels: Array
- }
- >
- {indicesSwitch}
-
- ) : (
- indicesSwitch
- )}
- {isAllIndices ? null : (
-
-
-
-
-
-
-
- {
- setSelectIndicesMode('custom');
- updatePolicyConfig({ indices: indexPatterns.join(',') });
- }}
- >
-
-
-
-
- ) : (
-
-
-
-
-
- {
- setSelectIndicesMode('list');
- updatePolicyConfig({ indices: indicesSelection });
- }}
- >
-
-
-
-
- )
- }
- helpText={
- selectIndicesMode === 'list' ? (
- 0 ? (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = undefined;
- });
- updatePolicyConfig({ indices: [] });
- setIndicesSelection([]);
- }}
- >
-
-
- ) : (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = 'on';
- });
- updatePolicyConfig({ indices: [...indices] });
- setIndicesSelection([...indices]);
- }}
- >
-
-
- ),
- }}
- />
- ) : null
- }
- isInvalid={Boolean(errors.indices)}
- error={errors.indices}
- >
- {selectIndicesMode === 'list' ? (
- {
- const newSelectedIndices: string[] = [];
- options.forEach(({ label, checked }) => {
- if (checked === 'on') {
- newSelectedIndices.push(label);
- }
- });
- setIndicesOptions(options);
- updatePolicyConfig({ indices: newSelectedIndices });
- setIndicesSelection(newSelectedIndices);
- }}
- searchable
- height={300}
- >
- {(list, search) => (
-
- {search}
- {list}
-
- )}
-
- ) : (
- ({ label: index }))}
- placeholder={i18n.translate(
- 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder',
- {
- defaultMessage: 'Enter index patterns, i.e. logstash-*',
- }
- )}
- selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))}
- onCreateOption={(pattern: string) => {
- if (!pattern.trim().length) {
- return;
- }
- const newPatterns = [...indexPatterns, pattern];
- setIndexPatterns(newPatterns);
- updatePolicyConfig({
- indices: newPatterns.join(','),
- });
- }}
- onChange={(patterns: Array<{ label: string }>) => {
- const newPatterns = patterns.map(({ label }) => label);
- setIndexPatterns(newPatterns);
- updatePolicyConfig({
- indices: newPatterns.join(','),
- });
- }}
- />
- )}
-
-
- )}
-
-
-
- );
- };
-
- const renderIgnoreUnavailableField = () => (
-
-
-
-
-
- }
- description={
-
- }
- fullWidth
- >
-
-
- }
- checked={Boolean(config.ignoreUnavailable)}
- onChange={(e) => {
- updatePolicyConfig({
- ignoreUnavailable: e.target.checked,
- });
- }}
- />
-
-
- );
-
- const renderPartialField = () => (
-
-
-
-
-
- }
- description={
-
- }
- fullWidth
- >
-
-
- }
- checked={Boolean(config.partial)}
- onChange={(e) => {
- updatePolicyConfig({
- partial: e.target.checked,
- });
- }}
- />
-
-
- );
-
- const renderIncludeGlobalStateField = () => (
-
-
-
-
-
- }
- description={
-
- }
- fullWidth
- >
-
-
- }
- checked={config.includeGlobalState === undefined || config.includeGlobalState}
- onChange={(e) => {
- updatePolicyConfig({
- includeGlobalState: e.target.checked,
- });
- }}
- />
-
-
- );
- return (
-
- {/* Step title and doc link */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {renderIndicesField()}
- {renderIgnoreUnavailableField()}
- {renderPartialField()}
- {renderIncludeGlobalStateField()}
-
- );
-};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts
new file mode 100644
index 0000000000000..e0d632a58e4e1
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx
new file mode 100644
index 0000000000000..3570c74fb8fd0
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+interface Props {
+ onSelectionChange: (selection: 'all' | 'none') => void;
+ selectedIndicesAndDataStreams: string[];
+ indices: string[];
+ dataStreams: string[];
+}
+
+export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({
+ onSelectionChange,
+ selectedIndicesAndDataStreams,
+ indices,
+ dataStreams,
+}) => {
+ if (selectedIndicesAndDataStreams.length === 0) {
+ return (
+ {
+ onSelectionChange('all');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+ }
+
+ const indicesCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (indices.includes(v) ? acc + 1 : acc),
+ 0
+ );
+ const dataStreamsCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc),
+ 0
+ );
+
+ return (
+ {
+ onSelectionChange('none');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts
new file mode 100644
index 0000000000000..9bf97af6400b5
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { determineListMode } from './helpers';
+
+describe('helpers', () => {
+ describe('determineListMode', () => {
+ test('list length (> 100)', () => {
+ expect(
+ determineListMode({
+ indices: Array.from(Array(101).keys()).map(String),
+ dataStreams: [],
+ configuredIndices: undefined,
+ })
+ ).toBe('custom');
+
+ // The length of indices and data streams are cumulative
+ expect(
+ determineListMode({
+ indices: Array.from(Array(51).keys()).map(String),
+ dataStreams: Array.from(Array(51).keys()).map(String),
+ configuredIndices: undefined,
+ })
+ ).toBe('custom');
+
+ // Other values should result in list mode
+ expect(
+ determineListMode({
+ indices: [],
+ dataStreams: [],
+ configuredIndices: undefined,
+ })
+ ).toBe('list');
+ });
+
+ test('configured indices is a string', () => {
+ expect(
+ determineListMode({
+ indices: [],
+ dataStreams: [],
+ configuredIndices: 'test',
+ })
+ ).toBe('custom');
+ });
+
+ test('configured indices not included in current indices and data streams', () => {
+ expect(
+ determineListMode({
+ indices: ['a'],
+ dataStreams: ['b'],
+ configuredIndices: ['a', 'b', 'c'],
+ })
+ ).toBe('custom');
+ });
+
+ test('configured indices included in current indices and data streams', () => {
+ expect(
+ determineListMode({
+ indices: ['a'],
+ dataStreams: ['b'],
+ configuredIndices: ['a', 'b'],
+ })
+ ).toBe('list');
+ });
+ });
+});
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx
new file mode 100644
index 0000000000000..98ad2fe9c5489
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.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 { EuiSelectableOption } from '@elastic/eui';
+import { orderDataStreamsAndIndices } from '../../../../../lib';
+import { DataStreamBadge } from '../../../../../data_stream_badge';
+
+export const mapSelectionToIndicesOptions = ({
+ allSelected,
+ selection,
+ dataStreams,
+ indices,
+}: {
+ allSelected: boolean;
+ selection: string[];
+ dataStreams: string[];
+ indices: string[];
+}): EuiSelectableOption[] => {
+ return orderDataStreamsAndIndices({
+ dataStreams: dataStreams.map(
+ (dataStream): EuiSelectableOption => {
+ return {
+ label: dataStream,
+ append: ,
+ checked: allSelected || selection.includes(dataStream) ? 'on' : undefined,
+ };
+ }
+ ),
+ indices: indices.map(
+ (index): EuiSelectableOption => {
+ return {
+ label: index,
+ checked: allSelected || selection.includes(index) ? 'on' : undefined,
+ };
+ }
+ ),
+ });
+};
+
+/**
+ * @remark
+ * Users with more than 100 indices will probably want to use an index pattern to select
+ * them instead, so we'll default to showing them the index pattern input. Also show the custom
+ * list if we have no exact matches in the configured array to some existing index.
+ */
+export const determineListMode = ({
+ configuredIndices,
+ indices,
+ dataStreams,
+}: {
+ configuredIndices: string | string[] | undefined;
+ indices: string[];
+ dataStreams: string[];
+}): 'custom' | 'list' => {
+ const indicesAndDataStreams = indices.concat(dataStreams);
+ return typeof configuredIndices === 'string' ||
+ indicesAndDataStreams.length > 100 ||
+ (Array.isArray(configuredIndices) &&
+ // If not every past configured index maps to an existing index or data stream
+ // we also show the custom list
+ !configuredIndices.every((c) => indicesAndDataStreams.some((i) => i === c)))
+ ? 'custom'
+ : 'list';
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts
new file mode 100644
index 0000000000000..e0d632a58e4e1
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field';
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
new file mode 100644
index 0000000000000..94854905e6686
--- /dev/null
+++ 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
@@ -0,0 +1,348 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment, FunctionComponent, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import {
+ EuiComboBox,
+ EuiDescribedFormGroup,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiLink,
+ EuiPanel,
+ EuiSelectable,
+ EuiSelectableOption,
+ EuiSpacer,
+ EuiSwitch,
+ EuiTitle,
+ EuiToolTip,
+} from '@elastic/eui';
+
+import { SlmPolicyPayload } from '../../../../../../../../common/types';
+import { useServices } from '../../../../../../app_context';
+import { PolicyValidation } from '../../../../../../services/validation';
+
+import { orderDataStreamsAndIndices } from '../../../../../lib';
+import { DataStreamBadge } from '../../../../../data_stream_badge';
+
+import { mapSelectionToIndicesOptions, determineListMode } from './helpers';
+
+import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text';
+
+interface Props {
+ isManagedPolicy: boolean;
+ policy: SlmPolicyPayload;
+ indices: string[];
+ dataStreams: string[];
+ onUpdate: (arg: { indices?: string[] | string }) => void;
+ errors: PolicyValidation['errors'];
+}
+
+/**
+ * In future we may be able to split data streams to its own field, but for now
+ * they share an array "indices" in the snapshot lifecycle policy config. See
+ * this github issue for progress: https://github.com/elastic/elasticsearch/issues/58474
+ */
+export const IndicesAndDataStreamsField: FunctionComponent = ({
+ isManagedPolicy,
+ dataStreams,
+ indices,
+ policy,
+ onUpdate,
+ errors,
+}) => {
+ const { i18n } = useServices();
+ const { config = {} } = policy;
+
+ const indicesAndDataStreams = indices.concat(dataStreams);
+
+ // We assume all indices if the config has no indices entry or if we receive an empty array
+ const [isAllIndices, setIsAllIndices] = useState(
+ !config.indices || (Array.isArray(config.indices) && config.indices.length === 0)
+ );
+
+ const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState(
+ () =>
+ Array.isArray(config.indices) && !isAllIndices
+ ? indicesAndDataStreams.filter((i) => (config.indices! as string[]).includes(i))
+ : [...indicesAndDataStreams]
+ );
+
+ // States for choosing all indices, or a subset, including caching previously chosen subset list
+ const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState<
+ EuiSelectableOption[]
+ >(() =>
+ mapSelectionToIndicesOptions({
+ selection: indicesAndDataStreamsSelection,
+ dataStreams,
+ indices,
+ allSelected: isAllIndices || typeof config.indices === 'string',
+ })
+ );
+
+ // State for using selectable indices list or custom patterns
+ const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(() =>
+ determineListMode({ configuredIndices: config.indices, dataStreams, indices })
+ );
+
+ // State for custom patterns
+ const [indexPatterns, setIndexPatterns] = useState(() =>
+ typeof config.indices === 'string'
+ ? (config.indices as string).split(',')
+ : Array.isArray(config.indices) && config.indices
+ ? config.indices
+ : []
+ );
+
+ const indicesSwitch = (
+
+ }
+ checked={isAllIndices}
+ disabled={isManagedPolicy}
+ data-test-subj="allIndicesToggle"
+ onChange={(e) => {
+ const isChecked = e.target.checked;
+ setIsAllIndices(isChecked);
+ if (isChecked) {
+ setIndicesAndDataStreamsSelection(indicesAndDataStreams);
+ setIndicesAndDataStreamsOptions(
+ mapSelectionToIndicesOptions({
+ allSelected: isAllIndices || typeof config.indices === 'string',
+ dataStreams,
+ indices,
+ selection: indicesAndDataStreamsSelection,
+ })
+ );
+ onUpdate({ indices: undefined });
+ } else {
+ onUpdate({
+ indices:
+ selectIndicesMode === 'custom'
+ ? indexPatterns.join(',')
+ : [...(indicesAndDataStreamsSelection || [])],
+ });
+ }
+ }}
+ />
+ );
+
+ return (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ {isManagedPolicy ? (
+
+
+
+ }
+ >
+ {indicesSwitch}
+
+ ) : (
+ indicesSwitch
+ )}
+ {isAllIndices ? null : (
+
+
+
+
+
+
+
+ {
+ setSelectIndicesMode('custom');
+ onUpdate({ indices: indexPatterns.join(',') });
+ }}
+ >
+
+
+
+
+ ) : (
+
+
+
+
+
+ {
+ setSelectIndicesMode('list');
+ onUpdate({ indices: indicesAndDataStreamsSelection });
+ }}
+ >
+
+
+
+
+ )
+ }
+ helpText={
+ selectIndicesMode === 'list' ? (
+ {
+ if (selection === 'all') {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = 'on';
+ });
+ onUpdate({ indices: [...indicesAndDataStreams] });
+ setIndicesAndDataStreamsSelection([...indicesAndDataStreams]);
+ } else {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = undefined;
+ });
+ onUpdate({ indices: [] });
+ setIndicesAndDataStreamsSelection([]);
+ }
+ }}
+ selectedIndicesAndDataStreams={indicesAndDataStreamsSelection}
+ indices={indices}
+ dataStreams={dataStreams}
+ />
+ ) : null
+ }
+ isInvalid={Boolean(errors.indices)}
+ error={errors.indices}
+ >
+ {selectIndicesMode === 'list' ? (
+ {
+ const newSelectedIndices: string[] = [];
+ options.forEach(({ label, checked }) => {
+ if (checked === 'on') {
+ newSelectedIndices.push(label);
+ }
+ });
+ setIndicesAndDataStreamsOptions(options);
+ onUpdate({ indices: newSelectedIndices });
+ setIndicesAndDataStreamsSelection(newSelectedIndices);
+ }}
+ searchable
+ height={300}
+ >
+ {(list, search) => (
+
+ {search}
+ {list}
+
+ )}
+
+ ) : (
+ ({
+ label: index,
+ value: { isDataStream: false },
+ })),
+ dataStreams: dataStreams.map((dataStream) => ({
+ label: dataStream,
+ value: { isDataStream: true },
+ })),
+ })}
+ renderOption={({ label, value }) => {
+ if (value?.isDataStream) {
+ return (
+
+ {label}
+
+
+
+
+ );
+ }
+ return label;
+ }}
+ placeholder={i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder',
+ {
+ defaultMessage: 'Enter index patterns, i.e. logstash-*',
+ }
+ )}
+ selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))}
+ onCreateOption={(pattern: string) => {
+ if (!pattern.trim().length) {
+ return;
+ }
+ const newPatterns = [...indexPatterns, pattern];
+ setIndexPatterns(newPatterns);
+ onUpdate({
+ indices: newPatterns.join(','),
+ });
+ }}
+ onChange={(patterns: Array<{ label: string }>) => {
+ const newPatterns = patterns.map(({ label }) => label);
+ setIndexPatterns(newPatterns);
+ onUpdate({
+ indices: newPatterns.join(','),
+ });
+ }}
+ />
+ )}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts
new file mode 100644
index 0000000000000..24e9b36e74889
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/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 { PolicyStepSettings } from './step_settings';
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
new file mode 100644
index 0000000000000..9d43c45d17ea7
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx
@@ -0,0 +1,206 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiFormRow,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiSwitch,
+} from '@elastic/eui';
+
+import { SlmPolicyPayload } from '../../../../../../common/types';
+import { documentationLinksService } from '../../../../services/documentation';
+import { StepProps } from '../';
+
+import { IndicesAndDataStreamsField } from './fields';
+
+export const PolicyStepSettings: React.FunctionComponent = ({
+ policy,
+ indices,
+ dataStreams,
+ updatePolicy,
+ errors,
+}) => {
+ const { config = {}, isManagedPolicy } = policy;
+
+ const updatePolicyConfig = (updatedFields: Partial): void => {
+ const newConfig = { ...config, ...updatedFields };
+ updatePolicy({
+ config: newConfig,
+ });
+ };
+
+ const renderIgnoreUnavailableField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ }
+ checked={Boolean(config.ignoreUnavailable)}
+ onChange={(e) => {
+ updatePolicyConfig({
+ ignoreUnavailable: e.target.checked,
+ });
+ }}
+ />
+
+
+ );
+
+ const renderPartialField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ }
+ checked={Boolean(config.partial)}
+ onChange={(e) => {
+ updatePolicyConfig({
+ partial: e.target.checked,
+ });
+ }}
+ />
+
+
+ );
+
+ const renderIncludeGlobalStateField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ }
+ checked={config.includeGlobalState === undefined || config.includeGlobalState}
+ onChange={(e) => {
+ updatePolicyConfig({
+ includeGlobalState: e.target.checked,
+ });
+ }}
+ />
+
+
+ );
+ return (
+
+ {/* Step title and doc link */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderIgnoreUnavailableField()}
+ {renderPartialField()}
+ {renderIncludeGlobalStateField()}
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts
index 3f3db0ff28eca..182d4ef8f583a 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts
@@ -14,6 +14,6 @@ export interface StepProps {
updateCurrentStep: (step: number) => void;
}
-export { RestoreSnapshotStepLogistics } from './step_logistics';
+export { RestoreSnapshotStepLogistics } from './step_logistics/step_logistics';
export { RestoreSnapshotStepSettings } from './step_settings';
export { RestoreSnapshotStepReview } from './step_review';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx
new file mode 100644
index 0000000000000..877dbe8963926
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+interface Props {
+ onSelectionChange: (selection: 'all' | 'none') => void;
+ selectedIndicesAndDataStreams: string[];
+ indices: string[];
+ dataStreams: string[];
+}
+
+export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({
+ onSelectionChange,
+ selectedIndicesAndDataStreams,
+ indices,
+ dataStreams,
+}) => {
+ if (selectedIndicesAndDataStreams.length === 0) {
+ return (
+ {
+ onSelectionChange('all');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+ }
+
+ const indicesCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (indices.includes(v) ? acc + 1 : acc),
+ 0
+ );
+ const dataStreamsCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc),
+ 0
+ );
+
+ return (
+ {
+ onSelectionChange('none');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx
new file mode 100644
index 0000000000000..64fce4dcfac43
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React, { FunctionComponent } from 'react';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+
+import { documentationLinksService } from '../../../../services/documentation';
+
+const i18nTexts = {
+ callout: {
+ title: (count: number) =>
+ i18n.translate('xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.title', {
+ defaultMessage:
+ 'This snapshot contains {count, plural, one {a data stream} other {data streams}}',
+ values: { count },
+ }),
+ body: () => (
+
+ {i18n.translate(
+ 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink',
+ { defaultMessage: 'Learn more' }
+ )}
+
+ ),
+ }}
+ />
+ ),
+ },
+};
+
+interface Props {
+ dataStreamsCount: number;
+}
+
+export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => {
+ return (
+
+ {i18nTexts.callout.body()}
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts
new file mode 100644
index 0000000000000..8f4efcf2a91f1
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/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 { RestoreSnapshotStepLogistics } from './step_logistics';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx
similarity index 69%
rename from x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx
rename to x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx
index c80c5a2e4c01d..d9fd4cca0d614 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx
@@ -21,10 +21,22 @@ import {
EuiComboBox,
} from '@elastic/eui';
import { EuiSelectableOption } from '@elastic/eui';
-import { RestoreSettings } from '../../../../../common/types';
-import { documentationLinksService } from '../../../services/documentation';
-import { useServices } from '../../../app_context';
-import { StepProps } from './';
+
+import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib';
+import { RestoreSettings } from '../../../../../../common/types';
+
+import { documentationLinksService } from '../../../../services/documentation';
+
+import { useServices } from '../../../../app_context';
+
+import { orderDataStreamsAndIndices } from '../../../lib';
+import { DataStreamBadge } from '../../../data_stream_badge';
+
+import { StepProps } from '../index';
+
+import { DataStreamsGlobalStateCallOut } from './data_streams_global_state_call_out';
+
+import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text';
export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({
snapshotDetails,
@@ -34,10 +46,30 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}) => {
const { i18n } = useServices();
const {
- indices: snapshotIndices,
+ indices: unfilteredSnapshotIndices,
+ dataStreams: snapshotDataStreams = [],
includeGlobalState: snapshotIncludeGlobalState,
} = snapshotDetails;
+ const snapshotIndices = unfilteredSnapshotIndices.filter(
+ (index) => !isDataStreamBackingIndex(index)
+ );
+ const snapshotIndicesAndDataStreams = snapshotIndices.concat(snapshotDataStreams);
+
+ const comboBoxOptions = orderDataStreamsAndIndices<{
+ label: string;
+ value: { isDataStream: boolean; name: string };
+ }>({
+ dataStreams: snapshotDataStreams.map((dataStream) => ({
+ label: dataStream,
+ value: { isDataStream: true, name: dataStream },
+ })),
+ indices: snapshotIndices.map((index) => ({
+ label: index,
+ value: { isDataStream: false, name: index },
+ })),
+ });
+
const {
indices: restoreIndices,
renamePattern,
@@ -47,28 +79,50 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
} = restoreSettings;
// States for choosing all indices, or a subset, including caching previously chosen subset list
- const [isAllIndices, setIsAllIndices] = useState(!Boolean(restoreIndices));
- const [indicesOptions, setIndicesOptions] = useState(
- snapshotIndices.map(
- (index): EuiSelectableOption => ({
- label: index,
- checked:
- isAllIndices ||
- // If indices is a string, we default to custom input mode, so we mark individual indices
- // as selected if user goes back to list mode
- typeof restoreIndices === 'string' ||
- (Array.isArray(restoreIndices) && restoreIndices.includes(index))
- ? 'on'
- : undefined,
- })
- )
+ const [isAllIndicesAndDataStreams, setIsAllIndicesAndDataStreams] = useState(
+ !Boolean(restoreIndices)
+ );
+ const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState<
+ EuiSelectableOption[]
+ >(() =>
+ orderDataStreamsAndIndices({
+ dataStreams: snapshotDataStreams.map(
+ (dataStream): EuiSelectableOption => ({
+ label: dataStream,
+ append: ,
+ checked:
+ isAllIndicesAndDataStreams ||
+ // If indices is a string, we default to custom input mode, so we mark individual indices
+ // as selected if user goes back to list mode
+ typeof restoreIndices === 'string' ||
+ (Array.isArray(restoreIndices) && restoreIndices.includes(dataStream))
+ ? 'on'
+ : undefined,
+ })
+ ),
+ indices: snapshotIndices.map(
+ (index): EuiSelectableOption => ({
+ label: index,
+ checked:
+ isAllIndicesAndDataStreams ||
+ // If indices is a string, we default to custom input mode, so we mark individual indices
+ // as selected if user goes back to list mode
+ typeof restoreIndices === 'string' ||
+ (Array.isArray(restoreIndices) && restoreIndices.includes(index))
+ ? 'on'
+ : undefined,
+ })
+ ),
+ })
);
// State for using selectable indices list or custom patterns
// Users with more than 100 indices will probably want to use an index pattern to select
// them instead, so we'll default to showing them the index pattern input.
const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(
- typeof restoreIndices === 'string' || snapshotIndices.length > 100 ? 'custom' : 'list'
+ typeof restoreIndices === 'string' || snapshotIndicesAndDataStreams.length > 100
+ ? 'custom'
+ : 'list'
);
// State for custom patterns
@@ -83,13 +137,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
// Caching state for togglable settings
const [cachedRestoreSettings, setCachedRestoreSettings] = useState({
- indices: [...snapshotIndices],
+ indices: [...snapshotIndicesAndDataStreams],
renamePattern: '',
renameReplacement: '',
});
return (
-
+
{/* Step title and doc link */}
@@ -118,6 +175,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
+
+ {snapshotDataStreams.length ? (
+ <>
+
+
+ >
+ ) : undefined}
+
{/* Indices */}
@@ -126,16 +191,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent
=
}
description={
}
@@ -146,14 +211,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
- checked={isAllIndices}
+ checked={isAllIndicesAndDataStreams}
onChange={(e) => {
const isChecked = e.target.checked;
- setIsAllIndices(isChecked);
+ setIsAllIndicesAndDataStreams(isChecked);
if (isChecked) {
updateRestoreSettings({ indices: undefined });
} else {
@@ -166,7 +231,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
}}
/>
- {isAllIndices ? null : (
+ {isAllIndicesAndDataStreams ? null : (
=
@@ -210,8 +275,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}}
>
@@ -220,52 +285,35 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
helpText={
selectIndicesMode === 'list' ? (
- 0 ? (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = undefined;
- });
- updateRestoreSettings({ indices: [] });
- setCachedRestoreSettings({
- ...cachedRestoreSettings,
- indices: [],
- });
- }}
- >
-
-
- ) : (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = 'on';
- });
- updateRestoreSettings({ indices: [...snapshotIndices] });
- setCachedRestoreSettings({
- ...cachedRestoreSettings,
- indices: [...snapshotIndices],
- });
- }}
- >
-
-
- ),
+ {
+ if (selection === 'all') {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = 'on';
+ });
+ updateRestoreSettings({
+ indices: [...snapshotIndicesAndDataStreams],
+ });
+ setCachedRestoreSettings({
+ ...cachedRestoreSettings,
+ indices: [...snapshotIndicesAndDataStreams],
+ });
+ } else {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = undefined;
+ });
+ updateRestoreSettings({ indices: [] });
+ setCachedRestoreSettings({
+ ...cachedRestoreSettings,
+ indices: [],
+ });
+ }
}}
+ selectedIndicesAndDataStreams={csvToArray(restoreIndices)}
+ indices={snapshotIndices}
+ dataStreams={snapshotDataStreams}
/>
) : null
}
@@ -275,7 +323,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
{selectIndicesMode === 'list' ? (
{
const newSelectedIndices: string[] = [];
options.forEach(({ label, checked }) => {
@@ -283,7 +331,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
newSelectedIndices.push(label);
}
});
- setIndicesOptions(options);
+ setIndicesAndDataStreamsOptions(options);
updateRestoreSettings({ indices: [...newSelectedIndices] });
setCachedRestoreSettings({
...cachedRestoreSettings,
@@ -302,7 +350,24 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
) : (
({ label: index }))}
+ options={comboBoxOptions}
+ renderOption={({ value }) => {
+ return value?.isDataStream ? (
+
+ {value.name}
+
+
+
+
+ ) : (
+ value?.name
+ );
+ }}
placeholder={i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder',
{
@@ -336,22 +401,22 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
- {/* Rename indices */}
+ {/* Rename data streams and indices */}
}
description={
}
fullWidth
@@ -361,8 +426,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
checked={isRenamingIndices}
@@ -405,7 +470,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
>
{
setCachedRestoreSettings({
...cachedRestoreSettings,
@@ -431,7 +496,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
>
{
setCachedRestoreSettings({
...cachedRestoreSettings,
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx
index 27a3717566d93..5dacba506fe18 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx
@@ -24,7 +24,7 @@ import {
import { serializeRestoreSettings } from '../../../../../common/lib';
import { useServices } from '../../../app_context';
import { StepProps } from './';
-import { CollapsibleIndicesList } from '../../collapsible_indices_list';
+import { CollapsibleIndicesList } from '../../collapsible_lists/collapsible_indices_list';
export const RestoreSnapshotStepReview: React.FunctionComponent = ({
restoreSettings,
@@ -73,8 +73,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx
index 5f3ebf804c5e1..b9a2d7e4b7cd9 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx
@@ -18,6 +18,7 @@ import {
EuiSwitch,
EuiTitle,
EuiLink,
+ EuiCallOut,
} from '@elastic/eui';
import { RestoreSettings } from '../../../../../common/types';
import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants';
@@ -28,10 +29,12 @@ import { StepProps } from './';
export const RestoreSnapshotStepSettings: React.FunctionComponent = ({
restoreSettings,
updateRestoreSettings,
+ snapshotDetails,
errors,
}) => {
const { i18n } = useServices();
const { indexSettings, ignoreIndexSettings } = restoreSettings;
+ const { dataStreams } = snapshotDetails;
// State for index setting toggles
const [isUsingIndexSettings, setIsUsingIndexSettings] = useState(Boolean(indexSettings));
@@ -96,6 +99,23 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = (
+ {dataStreams?.length ? (
+ <>
+
+
+
+
+ >
+ ) : undefined}
{/* Modify index settings */}
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
index 7bcee4f5f6621..e69b0fad8014e 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
@@ -236,8 +236,8 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => {
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
index 287a77493307d..1a0c26c854490 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
@@ -22,6 +22,7 @@ import {
DataPlaceholder,
FormattedDateTime,
CollapsibleIndicesList,
+ CollapsibleDataStreamsList,
} from '../../../../../components';
import { linkToPolicy } from '../../../../../services/navigation';
import { SnapshotState } from './snapshot_state';
@@ -40,6 +41,7 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => {
// TODO: Add a tooltip explaining that: a false value means that the cluster global state
// is not stored as part of the snapshot.
includeGlobalState,
+ dataStreams,
indices,
state,
startTimeInMillis,
@@ -135,6 +137,22 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => {
+
+
+
+
+
+
+
+
+
+
+
+
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 6d1a432be7f9f..90cd26c821c5e 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
@@ -25,13 +25,8 @@ export const PolicyAdd: React.FunctionComponent = ({
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
- const {
- error: errorLoadingIndices,
- isLoading: isLoadingIndices,
- data: { indices } = {
- indices: [],
- },
- } = useLoadIndices();
+ const { error: errorLoadingIndices, isLoading: isLoadingIndices, data } = useLoadIndices();
+ const { indices, dataStreams } = data ?? { indices: [], dataStreams: [] };
// Set breadcrumb and page title
useEffect(() => {
@@ -123,6 +118,7 @@ export const PolicyAdd: React.FunctionComponent = ({
{
};
export const useLoadIndices = () => {
- return useRequest({
+ return useRequest({
path: `${API_BASE_PATH}policies/indices`,
method: 'get',
});
diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts
index 27a565ccb74bc..b4d0493098bbc 100644
--- a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts
@@ -18,6 +18,6 @@ export const sendRequest = (config: SendRequestConfig) => {
return _sendRequest(httpService.httpClient, config);
};
-export const useRequest = (config: UseRequestConfig) => {
- return _useRequest(httpService.httpClient, config);
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(httpService.httpClient, config);
};
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 0720994ca7669..24960b2533230 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
@@ -48,6 +48,7 @@ export const validatePolicy = (
snapshotName: [],
schedule: [],
repository: [],
+ dataStreams: [],
indices: [],
expireAfterValue: [],
minCount: [],
@@ -106,7 +107,7 @@ export const validatePolicy = (
if (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 index.',
+ defaultMessage: 'You must select at least one data stream or index.',
})
);
}
diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts
index 5c1a1fbfab12d..93e278e51f093 100644
--- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts
@@ -48,7 +48,7 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida
if (Array.isArray(indices) && indices.length === 0) {
validation.errors.indices.push(
i18n.translate('xpack.snapshotRestore.restoreValidation.indicesRequiredError', {
- defaultMessage: 'You must select at least one index.',
+ defaultMessage: 'You must select at least one data stream or index.',
})
);
}
@@ -93,7 +93,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida
'xpack.snapshotRestore.restoreValidation.indexSettingsNotModifiableError',
{
defaultMessage: 'You can’t modify: {settings}',
- // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299
values: {
settings: unmodifiableSettings.map((setting: string, index: number) =>
index === 0 ? `${setting} ` : setting
@@ -131,7 +130,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida
validation.errors.ignoreIndexSettings.push(
i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsNotRemovableError', {
defaultMessage: 'You can’t reset: {settings}',
- // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299
values: {
settings: unremovableSettings.map((setting: string, index: number) =>
index === 0 ? `${setting} ` : setting
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
index eb29b7bad37e6..b96d305fa4a87 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
@@ -6,6 +6,7 @@
import { addBasePath } from '../helpers';
import { registerPolicyRoutes } from './policy';
import { RouterMock, routeDependencies, RequestMock } from '../../test/helpers';
+import { ResolveIndexResponseFromES } from '../../types';
describe('[Snapshot and Restore API Routes] Policy', () => {
const mockEsPolicy = {
@@ -324,27 +325,45 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
};
it('should arrify and sort index names returned from ES', async () => {
- const mockEsResponse = [
- {
- index: 'fooIndex',
- },
- {
- index: 'barIndex',
- },
- ];
+ const mockEsResponse: ResolveIndexResponseFromES = {
+ indices: [
+ {
+ name: 'fooIndex',
+ attributes: ['open'],
+ },
+ {
+ name: 'barIndex',
+ attributes: ['open'],
+ data_stream: 'testDataStream',
+ },
+ ],
+ aliases: [],
+ data_streams: [
+ {
+ name: 'testDataStream',
+ backing_indices: ['barIndex'],
+ timestamp_field: '@timestamp',
+ },
+ ],
+ };
router.callAsCurrentUserResponses = [mockEsResponse];
const expectedResponse = {
- indices: ['barIndex', 'fooIndex'],
+ indices: ['fooIndex'],
+ dataStreams: ['testDataStream'],
};
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
});
it('should return empty array if no indices returned from ES', async () => {
- const mockEsResponse: any[] = [];
+ const mockEsResponse: ResolveIndexResponseFromES = {
+ indices: [],
+ aliases: [],
+ data_streams: [],
+ };
router.callAsCurrentUserResponses = [mockEsResponse];
- const expectedResponse = { indices: [] };
+ const expectedResponse = { indices: [], dataStreams: [] };
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
});
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
index 90667eda23b35..b8e7012529554 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
@@ -5,10 +5,10 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { SlmPolicyEs } from '../../../common/types';
+import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types';
import { deserializePolicy, serializePolicy } from '../../../common/lib';
import { getManagedPolicyNames } from '../../lib';
-import { RouteDependencies } from '../../types';
+import { RouteDependencies, ResolveIndexResponseFromES } from '../../types';
import { addBasePath } from '../helpers';
import { nameParameterSchema, policySchema } from './validate_schemas';
@@ -232,17 +232,26 @@ export function registerPolicyRoutes({
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
try {
- const indices: Array<{
- index: string;
- }> = await callAsCurrentUser('cat.indices', {
- format: 'json',
- h: 'index',
- });
+ const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser(
+ 'transport.request',
+ {
+ method: 'GET',
+ path: `_resolve/index/*`,
+ query: {
+ expand_wildcards: 'all,hidden',
+ },
+ }
+ );
+
+ const body: PolicyIndicesResponse = {
+ dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(),
+ indices: resolvedIndicesResponse.indices
+ .flatMap((index) => (index.data_stream ? [] : index.name))
+ .sort(),
+ };
return res.ok({
- body: {
- indices: indices.map(({ index }) => index).sort(),
- },
+ body,
});
} catch (e) {
if (isEsError(e)) {
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
index f913299fc3992..a7e61d1e7c02a 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
@@ -15,6 +15,7 @@ const defaultSnapshot = {
versionId: undefined,
version: undefined,
indices: [],
+ dataStreams: [],
includeGlobalState: undefined,
state: undefined,
startTime: undefined,
diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts
index 7794156eb1b88..8cfcaec1a2cd1 100644
--- a/x-pack/plugins/snapshot_restore/server/types.ts
+++ b/x-pack/plugins/snapshot_restore/server/types.ts
@@ -31,4 +31,20 @@ export interface RouteDependencies {
};
}
+/**
+ * An object representing a resolved index, data stream or alias
+ */
+interface IndexAndAliasFromEs {
+ name: string;
+ // per https://github.com/elastic/elasticsearch/pull/57626
+ attributes: Array<'open' | 'closed' | 'hidden' | 'frozen'>;
+ data_stream?: string;
+}
+
+export interface ResolveIndexResponseFromES {
+ indices: IndexAndAliasFromEs[];
+ aliases: IndexAndAliasFromEs[];
+ data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>;
+}
+
export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];
diff --git a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
index d6a55579b322d..e59f4689d9e3f 100644
--- a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
+++ b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
@@ -13,13 +13,23 @@ export const getSnapshot = ({
state = 'SUCCESS',
indexFailures = [],
totalIndices = getRandomNumber(),
-} = {}) => ({
+ totalDataStreams = getRandomNumber(),
+}: Partial<{
+ repository: string;
+ snapshot: string;
+ uuid: string;
+ state: string;
+ indexFailures: any[];
+ totalIndices: number;
+ totalDataStreams: number;
+}> = {}) => ({
repository,
snapshot,
uuid,
versionId: 8000099,
version: '8.0.0',
indices: new Array(totalIndices).fill('').map(getRandomString),
+ dataStreams: new Array(totalDataStreams).fill('').map(getRandomString),
includeGlobalState: 1,
state,
startTime: '2019-05-23T06:25:15.896Z',
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 074e93e10fd12..5c5d270d324ff 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9214,9 +9214,6 @@
"xpack.maps.source.kbnTMSDescription": "kibana.yml で構成されたマップタイルです",
"xpack.maps.source.kbnTMSTitle": "カスタムタイルマップサービス",
"xpack.maps.source.mapSettingsPanel.initialLocationLabel": "マップの初期位置情報",
- "xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "レイヤー名",
- "xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大ズーム",
- "xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小ズーム",
"xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "ベトルタイルレイヤー",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "ズームレベル",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "レイヤー名",
@@ -9793,8 +9790,6 @@
"xpack.ml.explorer.jobIdLabel": "ジョブ ID",
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)",
"xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング… ({queryExample})",
- "xpack.ml.explorer.limitLabel": "制限",
- "xpack.ml.explorer.loadingLabel": "読み込み中",
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。",
"xpack.ml.explorer.noInfluencersFoundTitle": "{viewBySwimlaneFieldName}影響因子が見つかりません",
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの{viewBySwimlaneFieldName} 影響因子が見つかりません",
@@ -13390,7 +13385,6 @@
"xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "いいえ",
"xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "はい",
- "xpack.snapshotRestore.policyDetails.indicesLabel": "インデックス",
"xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "「{snapshotName}」が進行中",
"xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日付",
"xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "ポリシー「{name}」の前回のエラーの詳細",
@@ -13498,10 +13492,8 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "いいえ",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "はい",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "インデックス",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "ポリシー名",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "いいえ",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "部分シャードを許可",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "はい",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "レポジトリ",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "スケジュール",
@@ -13510,7 +13502,6 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "スナップショット名",
"xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "まとめ",
"xpack.snapshotRestore.policyForm.stepReviewTitle": "レビューポリシー",
- "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "システムインデックスを含むすべてのインデックス",
"xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "すべて選択解除",
"xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "スナップショット設定ドキュメント",
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "スナップショットの撮影時に利用不可能なインデックスを無視します。これが設定されていない場合、スナップショット全体がエラーになります。",
@@ -13518,19 +13509,15 @@
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "利用不可能なインデックスを無視",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "スナップショットの一部としてクラスターのグローバルステータスを格納します。",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "グローバルステータスを含める",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "バックアップするインデックスです。",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "インデックスパターン",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "インデックス",
"xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "インデックスパターンを使用",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "インデックスを選択",
"xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "クラウドで管理されたポリシーにはすべてのインデックスが必要です。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "利用不可能なプライマリシャードのインデックスのスナップショットを許可します。これが設定されていない場合、スナップショット全体がエラーになります。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "部分インデックスを許可",
"xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "部分インデックスを許可",
"xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "すべて選択",
- "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}がバックアップされます。{selectOrDeselectAllLink}",
"xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "インデックスを選択",
"xpack.snapshotRestore.policyForm.stepSettingsTitle": "スナップショット設定",
"xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "スナップショットライフサイクルポリシーを管理するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {missingPrivileges}。",
@@ -13877,31 +13864,22 @@
"xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "インデックス設定",
"xpack.snapshotRestore.restoreForm.nextButtonLabel": "次へ",
"xpack.snapshotRestore.restoreForm.savingButtonLabel": "復元中...",
- "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "システムインデックスを含むすべてのインデックス",
"xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "すべて選択解除",
"xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "スナップショットと復元ドキュメント",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続的な設定も復元します。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "存在しない場合は、新しいインデックスを作成します。閉じていて、スナップショットインデックスと同じ数のシャードがある場合は、既存のインデックスを復元します。",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "インデックスパターン",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "インデックス",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "インデックスパターンを使用",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "インデックスを選択",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "すべてのシャードのスナップショットがないインデックスを復元できます。",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分復元",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分復元",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "復元時にインデックス名を変更します。",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "インデックス名の変更",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "インデックス名の変更",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "正規表現を使用",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "取り込みパターン",
"xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "置換パターン",
"xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "すべて選択",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}が復元されます。{selectOrDeselectAllLink}",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "インデックスを選択",
"xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "詳細を復元",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "実行する設定を復元",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON",
@@ -13911,7 +13889,6 @@
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバル状態の復元",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "はい",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修正",
- "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "インデックス",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "インデックス設定の修正はありません",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "いいえ",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分復元",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 9d0bd95526670..c71215d2bfb74 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9218,9 +9218,6 @@
"xpack.maps.source.kbnTMSDescription": "在 kibana.yml 中配置的地图磁贴",
"xpack.maps.source.kbnTMSTitle": "定制磁贴地图服务",
"xpack.maps.source.mapSettingsPanel.initialLocationLabel": "初始地图位置",
- "xpack.maps.source.MVTSingleLayerVectorSource.layerNameMessage": "图层名称",
- "xpack.maps.source.MVTSingleLayerVectorSource.maxZoomMessage": "最大缩放",
- "xpack.maps.source.MVTSingleLayerVectorSource.minZoomMessage": "最小缩放",
"xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle": "矢量磁贴图层",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.dataZoomRangeMessage": "缩放级别",
"xpack.maps.source.MVTSingleLayerVectorSourceEditor.layerNameMessage": "图层名称",
@@ -9797,8 +9794,6 @@
"xpack.ml.explorer.jobIdLabel": "作业 ID",
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)",
"xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})",
- "xpack.ml.explorer.limitLabel": "限制",
- "xpack.ml.explorer.loadingLabel": "正在加载",
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "“顶级影响因素”列表被隐藏,因为没有为所选作业配置影响因素。",
"xpack.ml.explorer.noInfluencersFoundTitle": "未找到任何 {viewBySwimlaneFieldName} 影响因素",
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "对于指定筛选找不到任何 {viewBySwimlaneFieldName} 影响因素",
@@ -13395,7 +13390,6 @@
"xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "否",
"xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "是",
- "xpack.snapshotRestore.policyDetails.indicesLabel": "索引",
"xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "“{snapshotName}”正在进行中",
"xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日期",
"xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "策略“{name}”的上次失败详情",
@@ -13503,10 +13497,8 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "否",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "是",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "索引",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "策略名称",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "否",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "允许部分分片",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "是",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "存储库",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "计划",
@@ -13515,7 +13507,6 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "快照名称",
"xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "总结",
"xpack.snapshotRestore.policyForm.stepReviewTitle": "复查策略",
- "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "所有索引,包括系统索引",
"xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "取消全选",
"xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "快照设置文档",
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "拍取快照时忽略不可用的索引。否则,整个快照将失败。",
@@ -13523,19 +13514,15 @@
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "忽略不可用索引",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "将集群的全局状态存储为快照的一部分。",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "包括全局状态",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "要备份的索引。",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "索引模式",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "索引",
"xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "使用索引模式",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "选择索引",
"xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "云托管的策略需要所有索引。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "允许具有不可用主分片的索引的快照。否则,整个快照将失败。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "允许部分索引",
"xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "允许部分索引",
"xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "全选",
- "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "将备份 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}",
"xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "选择索引",
"xpack.snapshotRestore.policyForm.stepSettingsTitle": "快照设置",
"xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "要管理快照生命周期策略,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。",
@@ -13882,31 +13869,22 @@
"xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "索引设置",
"xpack.snapshotRestore.restoreForm.nextButtonLabel": "下一步",
"xpack.snapshotRestore.restoreForm.savingButtonLabel": "正在还原……",
- "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "所有索引,包括系统索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "取消全选",
"xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "快照和还原文档",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "如果不存在,则创建新索引。如果现有索引已关闭且与快照索引有相同数目的分片,则还原现有索引。",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "索引模式",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "使用索引模式",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "选择索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "允许还原不具有所有分片的快照的索引。",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分还原",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分还原",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "还原时重命名索引。",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "重命名索引",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "重命名索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "使用正则表达式",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "捕获模式",
"xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "替换模式",
"xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "全选",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "将还原 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "选择索引",
"xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "还原详情",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "还原要执行的设置",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON",
@@ -13916,7 +13894,6 @@
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "还原全局状态",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "鏄",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修改",
- "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "索引",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "无索引设置修改",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "否",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分还原",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx
index 244d431930f2e..a282fa08e8f38 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx
@@ -160,7 +160,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
setLoadingState(LoadingStateType.Idle);
}
})();
- /* eslint-disable react-hooks/exhaustive-deps */
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
index,
timeField,
@@ -175,12 +175,12 @@ export const ThresholdVisualization: React.FunctionComponent = ({
threshold,
startVisualizationAt,
]);
- /* eslint-enable react-hooks/exhaustive-deps */
if (!charts || !uiSettings || !dataFieldsFormats) {
return null;
}
const chartsTheme = charts.theme.useChartsTheme();
+ const chartsBaseTheme = charts.theme.useChartsBaseTheme();
const domain = getDomain(alertInterval, startVisualizationAt);
const visualizeOptions = {
@@ -261,6 +261,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
{
@@ -542,13 +543,10 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC
it('returns multiple levels of child process lifecycle events', async () => {
const { body }: { body: ResolverChildren } = await supertest
- .get(
- `/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&generations=1`
- )
+ .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&children=10`)
.expect(200);
+ expect(body.childNodes.length).to.eql(10);
expect(body.nextChild).to.be(null);
- expect(body.childNodes[0].nextChild).to.be(null);
- expect(body.childNodes.length).to.eql(8);
expect(body.childNodes[0].lifecycle.length).to.eql(1);
expect(
// for some reason the ts server doesn't think `endgame` exists even though we're using ResolverEvent
@@ -615,19 +613,27 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC
expect(body.childNodes.length).to.eql(12);
// there will be 4 parents, the origin of the tree, and it's 3 children
verifyChildren(body.childNodes, tree, 4, 3);
+ expect(body.nextChild).to.eql(null);
});
it('returns a single generation of children', async () => {
+ // this gets a node should have 3 children which were created in succession so that the timestamps
+ // are ordered correctly to be retrieved in a single call
+ const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id;
const { body }: { body: ResolverChildren } = await supertest
- .get(`/api/endpoint/resolver/${tree.origin.id}/children?generations=1`)
+ .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=3`)
.expect(200);
expect(body.childNodes.length).to.eql(3);
verifyChildren(body.childNodes, tree, 1, 3);
+ expect(body.nextChild).to.not.eql(null);
});
- it('paginates the children of the origin node', async () => {
+ it('paginates the children', async () => {
+ // this gets a node should have 3 children which were created in succession so that the timestamps
+ // are ordered correctly to be retrieved in a single call
+ const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id;
let { body }: { body: ResolverChildren } = await supertest
- .get(`/api/endpoint/resolver/${tree.origin.id}/children?generations=1&children=1`)
+ .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=1`)
.expect(200);
expect(body.childNodes.length).to.eql(1);
verifyChildren(body.childNodes, tree, 1, 1);
@@ -635,49 +641,41 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC
({ body } = await supertest
.get(
- `/api/endpoint/resolver/${tree.origin.id}/children?generations=1&afterChild=${body.nextChild}`
+ `/api/endpoint/resolver/${distantChildEntityID}/children?children=2&afterChild=${body.nextChild}`
)
.expect(200));
expect(body.childNodes.length).to.eql(2);
verifyChildren(body.childNodes, tree, 1, 2);
- expect(body.childNodes[0].nextChild).to.be(null);
- expect(body.childNodes[1].nextChild).to.be(null);
- });
-
- it('paginates the children of different nodes', async () => {
- let { body }: { body: ResolverChildren } = await supertest
- .get(`/api/endpoint/resolver/${tree.origin.id}/children?generations=2&children=2`)
- .expect(200);
- // it should return 4 nodes total, 2 for each level
- expect(body.childNodes.length).to.eql(4);
- verifyChildren(body.childNodes, tree, 2);
expect(body.nextChild).to.not.be(null);
- expect(body.childNodes[0].nextChild).to.not.be(null);
- // the second child will not have any results returned for it so it should not have pagination set (the first)
- // request to get it's children should start at the beginning aka not passing any pagination parameter
- expect(body.childNodes[1].nextChild).to.be(null);
- const firstChild = body.childNodes[0];
-
- // get the 3rd child of the origin of the tree
({ body } = await supertest
.get(
- `/api/endpoint/resolver/${tree.origin.id}/children?generations=1&children=10&afterChild=${body.nextChild}`
+ `/api/endpoint/resolver/${distantChildEntityID}/children?children=2&afterChild=${body.nextChild}`
)
.expect(200));
- expect(body.childNodes.length).to.be(1);
- verifyChildren(body.childNodes, tree, 1, 1);
- expect(body.childNodes[0].nextChild).to.be(null);
+ expect(body.childNodes.length).to.eql(0);
+ expect(body.nextChild).to.be(null);
+ });
+
+ it('gets all children in two queries', async () => {
+ // should get all the children of the origin
+ let { body }: { body: ResolverChildren } = await supertest
+ .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`)
+ .expect(200);
+ expect(body.childNodes.length).to.eql(3);
+ verifyChildren(body.childNodes, tree);
+ expect(body.nextChild).to.not.be(null);
+ const firstNodes = [...body.childNodes];
- // get the 1 child of the origin of the tree's last child
({ body } = await supertest
.get(
- `/api/endpoint/resolver/${firstChild.entityID}/children?generations=1&children=10&afterChild=${firstChild.nextChild}`
+ `/api/endpoint/resolver/${tree.origin.id}/children?children=10&afterChild=${body.nextChild}`
)
.expect(200));
- expect(body.childNodes.length).to.be(1);
- verifyChildren(body.childNodes, tree, 1, 1);
- expect(body.childNodes[0].nextChild).to.be(null);
+ expect(body.childNodes.length).to.eql(9);
+ // put all the results together and we should have all the children
+ verifyChildren([...firstNodes, ...body.childNodes], tree, 4, 3);
+ expect(body.nextChild).to.be(null);
});
});
});
@@ -703,7 +701,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC
it('returns a tree', async () => {
const { body }: { body: ResolverTree } = await supertest
.get(
- `/api/endpoint/resolver/${tree.origin.id}?children=100&generations=3&ancestors=5&events=4&alerts=4`
+ `/api/endpoint/resolver/${tree.origin.id}?children=100&ancestors=5&events=5&alerts=5`
)
.expect(200);
diff --git a/x-pack/test/api_integration/services/resolver.ts b/x-pack/test/api_integration/services/resolver.ts
index 7a100c37aea91..750d2f702fb84 100644
--- a/x-pack/test/api_integration/services/resolver.ts
+++ b/x-pack/test/api_integration/services/resolver.ts
@@ -18,6 +18,7 @@ export interface Options extends TreeOptions {
* Number of trees to generate.
*/
numTrees?: number;
+ seed?: string;
}
/**
@@ -38,8 +39,9 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) {
eventsIndex: string = 'logs-endpoint.events.process-default',
alertsIndex: string = 'logs-endpoint.alerts-default'
): Promise {
+ const seed = options.seed || 'resolver-seed';
const allTrees: Tree[] = [];
- const generator = new EndpointDocGenerator();
+ const generator = new EndpointDocGenerator(seed);
const numTrees = options.numTrees ?? 1;
for (let j = 0; j < numTrees; j++) {
const tree = generator.generateTree(options);
diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js
index 68d3c2536ee0b..5d75679432c97 100644
--- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js
+++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js
@@ -76,7 +76,11 @@ export default function ({ getPageObjects, getService }) {
const { lat, lon, zoom } = await PageObjects.maps.getView();
expect(Math.round(lat)).to.equal(41);
expect(Math.round(lon)).to.equal(-102);
- expect(Math.round(zoom)).to.equal(5);
+
+ // Centering is correct, but screen-size and dpi affect zoom level,
+ // causing this test to be brittle in different environments
+ // Expecting zoom-level to be between ]4,5]
+ expect(Math.ceil(zoom)).to.equal(5);
});
});
diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts
index 7c479a4234673..80df235bf6ff8 100644
--- a/x-pack/test/functional/services/ml/anomaly_explorer.ts
+++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts
@@ -76,7 +76,7 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid
async addAndEditSwimlaneInDashboard(dashboardTitle: string) {
await this.filterWithSearchString(dashboardTitle);
await testSubjects.isDisplayed('mlDashboardSelectionTable > checkboxSelectAll');
- await testSubjects.click('mlDashboardSelectionTable > checkboxSelectAll');
+ await testSubjects.clickWhenNotDisabled('mlDashboardSelectionTable > checkboxSelectAll');
expect(await testSubjects.isChecked('mlDashboardSelectionTable > checkboxSelectAll')).to.be(
true
);
diff --git a/yarn.lock b/yarn.lock
index ee61303e85f4a..ace6181797246 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2144,10 +2144,10 @@
dependencies:
"@elastic/apm-rum-core" "^5.3.0"
-"@elastic/charts@19.5.2":
- version "19.5.2"
- resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.5.2.tgz#6117f7b3acce6deef0a4f272ae491d99d2e3b1e0"
- integrity sha512-6GdqwVrDwQu+h5GUXpwy9a8dQ66oTNl3SO+ih1sljWvni+f/wcsrRcCTJUP99vtUcPQ8BT9Pn79QknBk1ZOH5Q==
+"@elastic/charts@19.6.3":
+ version "19.6.3"
+ resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-19.6.3.tgz#c23a1d7a8e245b1a800a3a4ef5fc4378b0da5e74"
+ integrity sha512-lB+rOODUKYZvsWCAcCxtAu8UxdZ2yIjZs+cjXwO1SlngY+jo+gc6XoEZG4kAczRPcr6cMdHesZ8LmFr3Enle5Q==
dependencies:
"@popperjs/core" "^2.4.0"
chroma-js "^2.1.0"
@@ -2168,6 +2168,9 @@
ts-debounce "^1.0.0"
utility-types "^3.10.0"
uuid "^3.3.2"
+ optionalDependencies:
+ redux-immutable-state-invariant "^2.1.0"
+ redux-logger "^3.0.6"
"@elastic/elasticsearch@^7.4.0":
version "7.4.0"
@@ -12005,6 +12008,11 @@ dedent@^0.7.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
+deep-diff@^0.3.5:
+ version "0.3.8"
+ resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
+ integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=
+
deep-eql@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
@@ -26502,6 +26510,21 @@ redux-devtools-extension@^2.13.8:
resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1"
integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==
+redux-immutable-state-invariant@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1"
+ integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg==
+ dependencies:
+ invariant "^2.1.0"
+ json-stringify-safe "^5.0.1"
+
+redux-logger@^3.0.6:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf"
+ integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8=
+ dependencies:
+ deep-diff "^0.3.5"
+
redux-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7"