diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
index 1eaf00c7a678d64..6229aeb9238e8d9 100644
--- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
+++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
@@ -16,6 +16,7 @@ Note that when generating absolute urls, the origin (protocol, host and port) ar
getUrlForApp(appId: string, options?: {
path?: string;
absolute?: boolean;
+ deepLinkId?: string;
}): string;
```
@@ -24,7 +25,7 @@ getUrlForApp(appId: string, options?: {
| Parameter | Type | Description |
| --- | --- | --- |
| appId | string
| |
-| options | {
path?: string;
absolute?: boolean;
}
| |
+| options | {
path?: string;
absolute?: boolean;
deepLinkId?: string;
}
| |
Returns:
diff --git a/docs/discover/search-sessions.asciidoc b/docs/discover/search-sessions.asciidoc
index fec1b8b26dd741a..b503e8cfba3b404 100644
--- a/docs/discover/search-sessions.asciidoc
+++ b/docs/discover/search-sessions.asciidoc
@@ -68,3 +68,19 @@ behaves differently:
* Relative dates are converted to absolute dates.
* Panning and zooming is disabled for maps.
* Changing a filter, query, or drilldown starts a new search session, which can be slow.
+
+[float]
+==== Limitations
+
+Certain visualization features do not fully support background search sessions yet. If a dashboard using these features gets restored,
+all panels using unsupported features won't load immediately, but instead send out additional data requests which can take a while to complete.
+In this case a warning *Your search session is still running* will be shown.
+
+You can either wait for these additional requests to complete or come back to the dashboard later when all data requests have been finished.
+
+A panel on a dashboard can behave like this if one of the following features is used:
+* *Lens* - A *top values* dimension with an enabled setting *Group other values as "Other"* (configurable in the *Advanced* section of the dimension)
+* *Lens* - An *intervals* dimension is used
+* *Aggregation based* visualizations - A *terms* aggregation is used with an enabled setting *Group other values in separate bucket*
+* *Aggregation based* visualizations - A *histogram* aggregation is used
+* *Maps* - Layers using joins, blended layers or tracks layers are used
diff --git a/docs/user/alerting/domain-specific-rules.asciidoc b/docs/user/alerting/domain-specific-rules.asciidoc
deleted file mode 100644
index f509f9e5288234f..000000000000000
--- a/docs/user/alerting/domain-specific-rules.asciidoc
+++ /dev/null
@@ -1,20 +0,0 @@
-[role="xpack"]
-[[domain-specific-rules]]
-== Domain-specific rules
-
-For domain-specific rules, refer to the documentation for that app.
-{kib} supports these rules:
-
-* {observability-guide}/create-alerts.html[Observability rules]
-* {security-guide}/prebuilt-rules.html[Security rules]
-* <>
-* {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[]
-
-[NOTE]
-==============================================
-Some rule types are subscription features, while others are free features.
-For a comparison of the Elastic subscription levels,
-see {subscriptions}[the subscription page].
-==============================================
-
-include::map-rules/geo-rule-types.asciidoc[]
diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc
index 68cf3ee070b0893..9ab6a2dc46ebf2d 100644
--- a/docs/user/alerting/index.asciidoc
+++ b/docs/user/alerting/index.asciidoc
@@ -3,6 +3,5 @@ include::alerting-setup.asciidoc[]
include::create-and-manage-rules.asciidoc[]
include::defining-rules.asciidoc[]
include::rule-management.asciidoc[]
-include::stack-rules.asciidoc[]
-include::domain-specific-rules.asciidoc[]
+include::rule-types.asciidoc[]
include::alerting-troubleshooting.asciidoc[]
diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc
new file mode 100644
index 000000000000000..bb840014fe80fb4
--- /dev/null
+++ b/docs/user/alerting/rule-types.asciidoc
@@ -0,0 +1,56 @@
+[role="xpack"]
+[[rule-types]]
+== Rule types
+
+A rule is a set of <>, <>, and <> that enable notifications. {kib} provides two types of rules: rules specific to the Elastic Stack and rules specific to a domain.
+
+[NOTE]
+==============================================
+Some rule types are subscription features, while others are free features.
+For a comparison of the Elastic subscription levels,
+see {subscriptions}[the subscription page].
+==============================================
+
+[float]
+[[stack-rules]]
+=== Stack rules
+
+<> are built into {kib}. To access the *Stack Rules* feature and create and edit rules, users require the `all` privilege. See <> for more information.
+
+[cols="2*<"]
+|===
+
+| <>
+| Aggregate field values from documents using {es} queries, compare them to threshold values, and schedule actions to run when the thresholds are met.
+
+| <>
+| Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met.
+
+|===
+
+[float]
+[[domain-specific-rules]]
+=== Domain rules
+
+Domain rules are registered by *Observability*, *Security*, <> and <>.
+
+[cols="2*<"]
+|===
+
+| {observability-guide}/create-alerts.html[Observability rules]
+| Detect complex conditions in the *Logs*, *Metrics*, and *Uptime* apps.
+
+| {security-guide}/prebuilt-rules.html[Security rules]
+| Detect suspicous source events with pre-built or custom rules and create alerts when a rule’s conditions are met.
+
+| <>
+| Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met.
+
+| {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[]
+| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered.
+
+|===
+
+include::rule-types/index-threshold.asciidoc[]
+include::rule-types/es-query.asciidoc[]
+include::rule-types/geo-rule-types.asciidoc[]
diff --git a/docs/user/alerting/stack-rules/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc
similarity index 100%
rename from docs/user/alerting/stack-rules/es-query.asciidoc
rename to docs/user/alerting/rule-types/es-query.asciidoc
diff --git a/docs/user/alerting/map-rules/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc
similarity index 74%
rename from docs/user/alerting/map-rules/geo-rule-types.asciidoc
rename to docs/user/alerting/rule-types/geo-rule-types.asciidoc
index eee7b592522054a..244cf90c855a7e1 100644
--- a/docs/user/alerting/map-rules/geo-rule-types.asciidoc
+++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc
@@ -1,16 +1,14 @@
[role="xpack"]
[[geo-alerting]]
-=== Geo rule type
+=== Tracking containment
-Alerting now includes one additional stack rule: <>.
-
-As with other stack rules, you need `all` access to the *Stack Rules* feature
-to be able to create and edit a geo rule.
-See <> for more information on configuring roles that provide access to this feature.
+<> offers the Tracking containment rule type which runs an {es} query over indices to determine whether any
+documents are currently contained within any boundaries from the specified boundary index.
+In the event that an entity is contained within a boundary, an alert may be generated.
[float]
-==== Geo alerting requirements
-To create a *Tracking containment* rule, the following requirements must be present:
+==== Requirements
+To create a Tracking containment rule, the following requirements must be present:
- *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field,
and some form of entity identifier. An entity identifier is a `keyword` or `number`
@@ -29,22 +27,12 @@ than the current time minus the amount of the interval. If data older than
`now - ` is ingested, it won't trigger a rule.
[float]
-==== Creating a geo rule
-Click the *Create* button in the <>.
-Complete the <>.
-
-[role="screenshot"]
-image::user/alerting/images/alert-types-tracking-select.png[Choosing a tracking rule type]
+==== Create the rule
-[float]
-[[rule-type-tracking-containment]]
-==== Tracking containment
-The Tracking containment rule type runs an {es} query over indices, determining if any
-documents are currently contained within any boundaries from the specified boundary index.
-In the event that an entity is contained within a boundary, an alert may be generated.
+Fill in the <>, then select Tracking containment.
[float]
-===== Defining the conditions
+==== Define the conditions
Tracking containment rules have 3 clauses that define the condition to detect,
as well as 2 Kuery bars used to provide additional filtering context for each of the indices.
@@ -61,6 +49,9 @@ Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_sha
identifying boundaries, and an optional *Human-readable boundary name* for better alerting
messages.
+[float]
+==== Add action
+
Conditions for how a rule is tracked can be specified uniquely for each individual action.
A rule can be triggered either when a containment condition is met or when an entity
is no longer contained.
diff --git a/docs/user/alerting/stack-rules/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc
similarity index 100%
rename from docs/user/alerting/stack-rules/index-threshold.asciidoc
rename to docs/user/alerting/rule-types/index-threshold.asciidoc
diff --git a/docs/user/alerting/stack-rules.asciidoc b/docs/user/alerting/stack-rules.asciidoc
deleted file mode 100644
index 483834c78806e23..000000000000000
--- a/docs/user/alerting/stack-rules.asciidoc
+++ /dev/null
@@ -1,27 +0,0 @@
-[role="xpack"]
-[[stack-rules]]
-== Stack rule types
-
-Kibana provides two types of rules:
-
-* Stack rules, which are built into {kib}
-* <>, which are registered by {kib} apps.
-
-{kib} provides two stack rules:
-
-* <>
-* <>
-
-Users require the `all` privilege to access the *Stack Rules* feature and create and edit rules.
-See <> for more information.
-
-[NOTE]
-==============================================
-Some rule types are subscription features, while others are free features.
-For a comparison of the Elastic subscription levels,
-see {subscriptions}[the subscription page].
-==============================================
-
-
-include::stack-rules/index-threshold.asciidoc[]
-include::stack-rules/es-query.asciidoc[]
diff --git a/package.json b/package.json
index 513352db3f81bb8..ff2f62f51308410 100644
--- a/package.json
+++ b/package.json
@@ -215,7 +215,6 @@
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
"d3-array": "1.2.4",
- "d3-cloud": "1.2.5",
"d3-scale": "1.0.7",
"d3-shape": "^1.1.0",
"d3-time": "^1.1.0",
diff --git a/packages/kbn-test/src/jest/utils/router_helpers.tsx b/packages/kbn-test/src/jest/utils/router_helpers.tsx
index e2245440274d190..85ef27488a4ce95 100644
--- a/packages/kbn-test/src/jest/utils/router_helpers.tsx
+++ b/packages/kbn-test/src/jest/utils/router_helpers.tsx
@@ -8,18 +8,39 @@
import React, { Component, ComponentType } from 'react';
import { MemoryRouter, Route, withRouter } from 'react-router-dom';
-import * as H from 'history';
+import { History, LocationDescriptor } from 'history';
-export const WithMemoryRouter = (initialEntries: string[] = ['/'], initialIndex: number = 0) => (
- WrappedComponent: ComponentType
-) => (props: any) => (
+const stringifyPath = (path: LocationDescriptor): string => {
+ if (typeof path === 'string') {
+ return path;
+ }
+
+ return path.pathname || '/';
+};
+
+const locationDescriptorToRoutePath = (
+ paths: LocationDescriptor | LocationDescriptor[]
+): string | string[] => {
+ if (Array.isArray(paths)) {
+ return paths.map((path: LocationDescriptor) => {
+ return stringifyPath(path);
+ });
+ }
+
+ return stringifyPath(paths);
+};
+
+export const WithMemoryRouter = (
+ initialEntries: LocationDescriptor[] = ['/'],
+ initialIndex: number = 0
+) => (WrappedComponent: ComponentType) => (props: any) => (
);
export const WithRoute = (
- componentRoutePath: string | string[] = '/',
+ componentRoutePath: LocationDescriptor | LocationDescriptor[] = ['/'],
onRouter = (router: any) => {}
) => (WrappedComponent: ComponentType) => {
// Create a class component that will catch the router
@@ -40,16 +61,16 @@ export const WithRoute = (
return (props: any) => (
}
/>
);
};
interface Router {
- history: Partial;
+ history: Partial;
route: {
- location: H.Location;
+ location: LocationDescriptor;
};
}
diff --git a/packages/kbn-test/src/jest/utils/testbed/types.ts b/packages/kbn-test/src/jest/utils/testbed/types.ts
index fdc000215c4f197..bba504951c0bc6e 100644
--- a/packages/kbn-test/src/jest/utils/testbed/types.ts
+++ b/packages/kbn-test/src/jest/utils/testbed/types.ts
@@ -8,6 +8,7 @@
import { Store } from 'redux';
import { ReactWrapper } from 'enzyme';
+import { LocationDescriptor } from 'history';
export type SetupFunc = (props?: any) => TestBed | Promise>;
@@ -161,11 +162,11 @@ export interface MemoryRouterConfig {
/** Flag to add or not the `MemoryRouter`. If set to `false`, there won't be any router and the component won't be wrapped on a ` `. */
wrapComponent?: boolean;
/** The React Router **initial entries** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */
- initialEntries?: string[];
+ initialEntries?: LocationDescriptor[];
/** The React Router **initial index** setting ([see documentation](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/MemoryRouter.md)) */
initialIndex?: number;
/** The route **path** for the mounted component (defaults to `"/"`) */
- componentRoutePath?: string | string[];
+ componentRoutePath?: LocationDescriptor | LocationDescriptor[];
/** A callBack that will be called with the React Router instance once mounted */
onRouter?: (router: any) => void;
}
diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts
new file mode 100644
index 000000000000000..25651a0dd21902f
--- /dev/null
+++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.test.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { parseArchive } from './parse_archive';
+
+jest.mock('fs/promises', () => ({
+ readFile: jest.fn(),
+}));
+
+const mockReadFile = jest.requireMock('fs/promises').readFile;
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('parses archives with \\n', async () => {
+ mockReadFile.mockResolvedValue(
+ `{
+ "foo": "abc"
+ }\n\n{
+ "foo": "xyz"
+ }`
+ );
+
+ const archive = await parseArchive('mock');
+ expect(archive).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "foo": "abc",
+ },
+ Object {
+ "foo": "xyz",
+ },
+ ]
+ `);
+});
+
+it('parses archives with \\r\\n', async () => {
+ mockReadFile.mockResolvedValue(
+ `{
+ "foo": "123"
+ }\r\n\r\n{
+ "foo": "456"
+ }`
+ );
+
+ const archive = await parseArchive('mock');
+ expect(archive).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "foo": "123",
+ },
+ Object {
+ "foo": "456",
+ },
+ ]
+ `);
+});
diff --git a/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts
new file mode 100644
index 000000000000000..b6b85ba521525bf
--- /dev/null
+++ b/packages/kbn-test/src/kbn_client/import_export/parse_archive.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import Fs from 'fs/promises';
+
+export interface SavedObject {
+ id: string;
+ type: string;
+ [key: string]: unknown;
+}
+
+export async function parseArchive(path: string): Promise {
+ return (await Fs.readFile(path, 'utf-8'))
+ .split(/\r?\n\r?\n/)
+ .filter((line) => !!line)
+ .map((line) => JSON.parse(line));
+}
diff --git a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
index 88953cdbaed7c98..4adae7d1cd031e8 100644
--- a/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
+++ b/packages/kbn-test/src/kbn_client/kbn_client_import_export.ts
@@ -16,25 +16,12 @@ import { ToolingLog, isAxiosResponseError, createFailError, REPO_ROOT } from '@k
import { KbnClientRequester, uriencode, ReqOptions } from './kbn_client_requester';
import { KbnClientSavedObjects } from './kbn_client_saved_objects';
+import { parseArchive } from './import_export/parse_archive';
interface ImportApiResponse {
success: boolean;
[key: string]: unknown;
}
-
-interface SavedObject {
- id: string;
- type: string;
- [key: string]: unknown;
-}
-
-async function parseArchive(path: string): Promise {
- return (await Fs.readFile(path, 'utf-8'))
- .split('\n\n')
- .filter((line) => !!line)
- .map((line) => JSON.parse(line));
-}
-
export class KbnClientImportExport {
constructor(
public readonly log: ToolingLog,
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
index 5658d3f62607729..3ed164088bf5c79 100644
--- a/src/core/public/application/application_service.test.ts
+++ b/src/core/public/application/application_service.test.ts
@@ -497,6 +497,56 @@ describe('#start()', () => {
expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link');
});
+ describe('deepLinkId option', () => {
+ it('ignores the deepLinkId parameter if it is unknown', async () => {
+ service.setup(setupDeps);
+
+ service.setup(setupDeps);
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1', { deepLinkId: 'unkown-deep-link' })).toBe(
+ '/base-path/app/app1'
+ );
+ });
+
+ it('creates URLs with deepLinkId parameter', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(
+ Symbol(),
+ createApp({
+ id: 'app1',
+ appRoute: '/custom/app-path',
+ deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
+ })
+ );
+
+ const { getUrlForApp } = await service.start(startDeps);
+
+ expect(getUrlForApp('app1', { deepLinkId: 'dl1' })).toBe(
+ '/base-path/custom/app-path/deep-link'
+ );
+ });
+
+ it('creates URLs with deepLinkId and path parameters', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(
+ Symbol(),
+ createApp({
+ id: 'app1',
+ appRoute: '/custom/app-path',
+ deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
+ })
+ );
+
+ const { getUrlForApp } = await service.start(startDeps);
+ expect(getUrlForApp('app1', { deepLinkId: 'dl1', path: 'foo/bar' })).toBe(
+ '/base-path/custom/app-path/deep-link/foo/bar'
+ );
+ });
+ });
+
it('does not append trailing slash if hash is provided in path parameter', async () => {
service.setup(setupDeps);
const { getUrlForApp } = await service.start(startDeps);
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index 32d45b32c32ffd5..8c6090caabce193 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -282,8 +282,19 @@ export class ApplicationService {
history: this.history!,
getUrlForApp: (
appId,
- { path, absolute = false }: { path?: string; absolute?: boolean } = {}
+ {
+ path,
+ absolute = false,
+ deepLinkId,
+ }: { path?: string; absolute?: boolean; deepLinkId?: string } = {}
) => {
+ if (deepLinkId) {
+ const deepLinkPath = getAppDeepLinkPath(availableMounters, appId, deepLinkId);
+ if (deepLinkPath) {
+ path = appendAppPath(deepLinkPath, path);
+ }
+ }
+
const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path));
return absolute ? relativeToAbsolute(relUrl) : relUrl;
},
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index 60b0dbf158dd910..5803f2e3779abc9 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -780,7 +780,10 @@ export interface ApplicationStart {
* @param options.path - optional path inside application to deep link to
* @param options.absolute - if true, will returns an absolute url instead of a relative one
*/
- getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string;
+ getUrlForApp(
+ appId: string,
+ options?: { path?: string; absolute?: boolean; deepLinkId?: string }
+ ): string;
/**
* An observable that emits the current application id and each subsequent id update.
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 53428edf4b345f2..06277d9351922c2 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -142,7 +142,7 @@ export class DocLinksService {
dataStreams: `${ELASTICSEARCH_DOCS}data-streams.html`,
indexModules: `${ELASTICSEARCH_DOCS}index-modules.html`,
indexSettings: `${ELASTICSEARCH_DOCS}index-modules.html#index-modules-settings`,
- indexTemplates: `${ELASTICSEARCH_DOCS}indices-templates.html`,
+ indexTemplates: `${ELASTICSEARCH_DOCS}index-templates.html`,
mapping: `${ELASTICSEARCH_DOCS}mapping.html`,
mappingAnalyzer: `${ELASTICSEARCH_DOCS}analyzer.html`,
mappingCoerce: `${ELASTICSEARCH_DOCS}coerce.html`,
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 235110aeb4633c7..d3426b50f76143b 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -150,6 +150,7 @@ export interface ApplicationStart {
getUrlForApp(appId: string, options?: {
path?: string;
absolute?: boolean;
+ deepLinkId?: string;
}): string;
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise;
navigateToUrl(url: string): Promise;
diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts
index 69687f75f309828..feff425cc48edd3 100644
--- a/src/plugins/expressions/common/execution/execution.test.ts
+++ b/src/plugins/expressions/common/execution/execution.test.ts
@@ -834,8 +834,8 @@ describe('Execution', () => {
expect((chain[0].arguments.val[0] as ExpressionAstExpression).chain[0].debug!.args).toEqual(
{
- name: 'foo',
- value: 5,
+ name: ['foo'],
+ value: [5],
}
);
});
diff --git a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts
index 0a9f022ce89cad7..cdcae61215fa423 100644
--- a/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/tests/var_set.test.ts
@@ -9,6 +9,8 @@
import { functionWrapper } from './utils';
import { variableSet } from '../var_set';
import { ExecutionContext } from '../../../execution/types';
+import { createUnitTestExecutor } from '../../../test_helpers';
+import { first } from 'rxjs/operators';
describe('expression_functions', () => {
describe('var_set', () => {
@@ -32,21 +34,49 @@ describe('expression_functions', () => {
});
it('updates a variable', () => {
- const actual = fn(input, { name: 'test', value: 2 }, context);
+ const actual = fn(input, { name: ['test'], value: [2] }, context);
expect(variables.test).toEqual(2);
expect(actual).toEqual(input);
});
it('sets a new variable', () => {
- const actual = fn(input, { name: 'new', value: 3 }, context);
+ const actual = fn(input, { name: ['new'], value: [3] }, context);
expect(variables.new).toEqual(3);
expect(actual).toEqual(input);
});
it('stores context if value is not set', () => {
- const actual = fn(input, { name: 'test' }, context);
+ const actual = fn(input, { name: ['test'], value: [] }, context);
expect(variables.test).toEqual(input);
expect(actual).toEqual(input);
});
+
+ it('sets multiple variables', () => {
+ const actual = fn(input, { name: ['new1', 'new2', 'new3'], value: [1, , 3] }, context);
+ expect(variables.new1).toEqual(1);
+ expect(variables.new2).toEqual(input);
+ expect(variables.new3).toEqual(3);
+ expect(actual).toEqual(input);
+ });
+
+ describe('running function thru executor', () => {
+ const executor = createUnitTestExecutor();
+ executor.registerFunction(variableSet);
+
+ it('sets the variables', async () => {
+ const vars = {};
+ const result = await executor
+ .run('var_set name=test1 name=test2 value=1', 2, { variables: vars })
+ .pipe(first())
+ .toPromise();
+
+ expect(result).toEqual(2);
+
+ expect(vars).toEqual({
+ test1: 1,
+ test2: 2,
+ });
+ });
+ });
});
});
diff --git a/src/plugins/expressions/common/expression_functions/specs/var_set.ts b/src/plugins/expressions/common/expression_functions/specs/var_set.ts
index 490c7781a01a1e6..f3ac6a2ab80d4a8 100644
--- a/src/plugins/expressions/common/expression_functions/specs/var_set.ts
+++ b/src/plugins/expressions/common/expression_functions/specs/var_set.ts
@@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../types';
interface Arguments {
- name: string;
- value?: any;
+ name: string[];
+ value: any[];
}
export type ExpressionFunctionVarSet = ExpressionFunctionDefinition<
@@ -31,12 +31,14 @@ export const variableSet: ExpressionFunctionVarSet = {
types: ['string'],
aliases: ['_'],
required: true,
+ multi: true,
help: i18n.translate('expressions.functions.varset.name.help', {
defaultMessage: 'Specify the name of the variable.',
}),
},
value: {
aliases: ['val'],
+ multi: true,
help: i18n.translate('expressions.functions.varset.val.help', {
defaultMessage:
'Specify the value for the variable. When unspecified, the input context is used.',
@@ -45,7 +47,9 @@ export const variableSet: ExpressionFunctionVarSet = {
},
fn(input, args, context) {
const variables: Record = context.variables;
- variables[args.name] = args.value === undefined ? input : args.value;
+ args.name.forEach((name, i) => {
+ variables[name] = args.value[i] === undefined ? input : args.value[i];
+ });
return input;
},
};
diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts
index a12a2ff195211d4..267769d33fba2cc 100644
--- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts
+++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts
@@ -280,7 +280,7 @@ export const getSavedObjects = (): SavedObject[] => [
defaultMessage: '[eCommerce] Top Selling Products',
}),
visState:
- '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
+ '{"title":"[eCommerce] Top Selling Products","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"products.product_name.keyword","size":7,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
uiStateJSON: '{}',
description: '',
version: 1,
diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts
index 05a3d012d707c17..816322dbe5299cb 100644
--- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts
+++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts
@@ -242,7 +242,7 @@ export const getSavedObjects = (): SavedObject[] => [
defaultMessage: '[Flights] Destination Weather',
}),
visState:
- '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
+ '{"title":"[Flights] Destination Weather","type":"tagcloud","params":{"scale":"linear","orientation":"single","minFontSize":18,"maxFontSize":72,"showLabel":false,"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"DestWeather","size":10,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
uiStateJSON: '{}',
description: '',
version: 1,
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
index 21248ac9d1dc0bc..38a9e4701441685 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
@@ -14,46 +14,46 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
exports[`CreateIndexPatternWizard renders index pattern step when there are indices 1`] = `
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
-
-
-
-
+
+
-
+ }
+ />
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
-
-
-
-
+
+
-
+ }
+ showSystemIndices={true}
+ />
}
>
-
-
-
+
Create test index pattern
-
-
-
- Beta
-
-
-
-
-
+
+
+
+
+ }
+ >
+
-
-
+ Create test index pattern
+
+
+
+
+
+ }
+ responsive={true}
>
-
-
- multiple
- ,
- "single":
- filebeat-4-3-22
- ,
- "star":
- filebeat-*
- ,
- }
- }
+
+
-
- An index pattern can match a single source, for example,
-
-
-
-
- filebeat-4-3-22
-
-
-
-
- , or
-
- multiple
-
- data sources,
-
-
+
+
-
-
- filebeat-*
-
-
-
-
- .
-
-
-
-
+
+ Create test index pattern
+
+
+
+ Beta
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
- Read documentation
-
-
-
-
-
-
-
-
-
-
-
- Test prompt
-
-
+
+
+ multiple
+ ,
+ "single":
+ filebeat-4-3-22
+ ,
+ "star":
+ filebeat-*
+ ,
+ }
+ }
+ >
+
+ An index pattern can match a single source, for example,
+
+
+
+
+ filebeat-4-3-22
+
+
+
+
+ , or
+
+ multiple
+
+ data sources,
+
+
+
+
+ filebeat-*
+
+
+
+
+ .
+
+
+
+
+
+
+
+ Read documentation
+
+
+
+
+
+
+
+
+
+
+
+ Test prompt
+
+
+
+
+
+
`;
@@ -146,100 +203,145 @@ exports[`Header should render normally 1`] = `
}
indexPatternName="test index pattern"
>
-
-
-
+
Create test index pattern
-
-
-
+ }
+ >
+
-
-
+ Create test index pattern
+
+ }
+ responsive={true}
>
-
-
- multiple
- ,
- "single":
- filebeat-4-3-22
- ,
- "star":
- filebeat-*
- ,
- }
- }
+
+
-
- An index pattern can match a single source, for example,
-
-
-
-
- filebeat-4-3-22
-
-
-
-
- , or
-
- multiple
-
- data sources,
-
-
+
+
-
-
- filebeat-*
-
-
-
-
- .
-
-
-
-
+
+ Create test index pattern
+
+
+
+
+
+
+
-
-
+
+
+
+
-
- Read documentation
-
-
-
-
-
-
-
-
+
+
+ multiple
+ ,
+ "single":
+ filebeat-4-3-22
+ ,
+ "star":
+ filebeat-*
+ ,
+ }
+ }
+ >
+
+ An index pattern can match a single source, for example,
+
+
+
+
+ filebeat-4-3-22
+
+
+
+
+ , or
+
+ multiple
+
+ data sources,
+
+
+
+
+ filebeat-*
+
+
+
+
+ .
+
+
+
+
+
+
+
+ Read documentation
+
+
+
+
+
+
+
+
+
+
+
+
`;
@@ -254,99 +356,144 @@ exports[`Header should render without including system indices 1`] = `
}
indexPatternName="test index pattern"
>
-
-
-
+
Create test index pattern
-
-
-
+ }
+ >
+
-
-
+ Create test index pattern
+
+ }
+ responsive={true}
>
-
-
- multiple
- ,
- "single":
- filebeat-4-3-22
- ,
- "star":
- filebeat-*
- ,
- }
- }
+
+
-
- An index pattern can match a single source, for example,
-
-
-
-
- filebeat-4-3-22
-
-
-
-
- , or
-
- multiple
-
- data sources,
-
-
+
+
-
-
- filebeat-*
-
-
-
-
- .
-
-
-
-
+
+ Create test index pattern
+
+
+
+
+
+
+
-
-
+
+
+
+
-
- Read documentation
-
-
-
-
-
-
-
-
+
+
+ multiple
+ ,
+ "single":
+ filebeat-4-3-22
+ ,
+ "star":
+ filebeat-*
+ ,
+ }
+ }
+ >
+
+ An index pattern can match a single source, for example,
+
+
+
+
+ filebeat-4-3-22
+
+
+
+
+ , or
+
+ multiple
+
+ data sources,
+
+
+
+
+ filebeat-*
+
+
+
+
+ .
+
+
+
+
+
+
+
+ Read documentation
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx
index a7e3b2ded75dc68..c708bd3cac33e70 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/header.tsx
@@ -8,7 +8,7 @@
import React from 'react';
-import { EuiBetaBadge, EuiSpacer, EuiTitle, EuiText, EuiCode, EuiLink } from '@elastic/eui';
+import { EuiBetaBadge, EuiCode, EuiLink, EuiPageHeader, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -39,9 +39,9 @@ export const Header = ({
changeTitle(createIndexPatternHeader);
return (
-
-
-
+
{createIndexPatternHeader}
{isBeta ? (
<>
@@ -53,9 +53,10 @@ export const Header = ({
/>
>
) : null}
-
-
-
+ >
+ }
+ bottomBorder
+ >
) : null}
-
+
);
};
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
index 633906feb785b4a..5bc53105dbcf87b 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx
@@ -6,17 +6,12 @@
* Side Public License, v 1.
*/
-import React, { ReactElement, Component } from 'react';
-
-import {
- EuiGlobalToastList,
- EuiGlobalToastListToast,
- EuiPageContent,
- EuiHorizontalRule,
-} from '@elastic/eui';
+import React, { Component, ReactElement } from 'react';
+
+import { EuiGlobalToastList, EuiGlobalToastListToast, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
import { DocLinksStart } from 'src/core/public';
import { StepIndexPattern } from './components/step_index_pattern';
import { StepTimeField } from './components/step_time_field';
@@ -227,9 +222,9 @@ export class CreateIndexPatternWizard extends Component<
const initialQuery = new URLSearchParams(location.search).get('id') || undefined;
return (
-
+ <>
{header}
-
+
-
+ >
);
}
if (step === 2) {
return (
-
+ <>
{header}
-
+
-
+ >
);
}
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx
index 5aa9853c5e766f0..0c0adc6dd502959 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx
@@ -7,15 +7,15 @@
*/
import React from 'react';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
-import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
+import { EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IndexPattern, IndexPatternField } from '../../../../../../plugins/data/public';
import { useKibana } from '../../../../../../plugins/kibana_react/public';
import { IndexPatternManagmentContext } from '../../../types';
import { IndexHeader } from '../index_header';
-import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS } from '../constants';
+import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS } from '../constants';
import { FieldEditor } from '../../field_editor';
@@ -76,26 +76,18 @@ export const CreateEditField = withRouter(
if (spec) {
return (
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+ >
);
} else {
return <>>;
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
index e314c00bc8176f8..6609605da87d197 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
@@ -17,7 +17,6 @@ import {
EuiText,
EuiLink,
EuiCallOut,
- EuiPanel,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -145,15 +144,13 @@ export const EditIndexPattern = withRouter(
const kibana = useKibana();
const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping;
return (
-
-
-
-
+
+
{showTagsSection && (
{Boolean(indexPattern.timeFieldName) && (
@@ -193,19 +190,19 @@ export const EditIndexPattern = withRouter(
>
)}
-
- {
- setFields(indexPattern.getNonScriptedFields());
- }}
- />
-
-
+
+
+
{
+ setFields(indexPattern.getNonScriptedFields());
+ }}
+ />
+
);
}
);
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx
index 482cd574c8f1d65..c141c228a68f256 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFlexGroup, EuiToolTip, EuiFlexItem, EuiTitle, EuiButtonIcon } from '@elastic/eui';
+import { EuiButtonIcon, EuiPageHeader, EuiToolTip } from '@elastic/eui';
import { IIndexPattern } from 'src/plugins/data/public';
interface IndexHeaderProps {
@@ -40,50 +40,42 @@ const removeTooltip = i18n.translate('indexPatternManagement.editIndexPattern.re
defaultMessage: 'Remove index pattern.',
});
-export function IndexHeader({
+export const IndexHeader: React.FC = ({
defaultIndex,
indexPattern,
setDefault,
deleteIndexPatternClick,
-}: IndexHeaderProps) {
+ children,
+}) => {
return (
-
-
-
- {indexPattern.title}
-
-
-
-
- {defaultIndex !== indexPattern.id && setDefault && (
-
-
-
-
-
- )}
-
- {deleteIndexPatternClick && (
-
-
-
-
-
- )}
-
-
-
+ {indexPattern.title}}
+ rightSideItems={[
+ defaultIndex !== indexPattern.id && setDefault && (
+
+
+
+ ),
+ deleteIndexPatternClick && (
+
+
+
+ ),
+ ].filter(Boolean)}
+ >
+ {children}
+
);
-}
+};
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap
index c5e6d1220d8bf8d..bc69fa29e690443 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap
@@ -3,9 +3,11 @@
exports[`EmptyIndexPatternPrompt should render normally 1`] = `
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap
index 1310488c65fab86..957c94c80680d9f 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap
@@ -4,9 +4,11 @@ exports[`EmptyState should render normally 1`] = `
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx
index 240e732752916cb..c05f6a1f193b7af 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx
@@ -63,8 +63,10 @@ export const EmptyState = ({
diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
index f018294f27c84af..6bd06528084ce9a 100644
--- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
+++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx
@@ -8,24 +8,20 @@
import {
EuiBadge,
+ EuiBadgeGroup,
EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
EuiInMemoryTable,
+ EuiPageHeader,
EuiSpacer,
- EuiText,
- EuiBadgeGroup,
- EuiPageContent,
- EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import { withRouter, RouteComponentProps } from 'react-router-dom';
-import React, { useState, useEffect } from 'react';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public';
import { IndexPatternManagmentContext } from '../../types';
import { CreateButton } from '../create_button';
-import { IndexPatternTableItem, IndexPatternCreationOption } from '../types';
+import { IndexPatternCreationOption, IndexPatternTableItem } from '../types';
import { getIndexPatterns } from '../utils';
import { getListBreadcrumbs } from '../breadcrumbs';
import { EmptyState } from './empty_state';
@@ -54,10 +50,6 @@ const search = {
},
};
-const ariaRegion = i18n.translate('indexPatternManagement.editIndexPatternLiveRegionAriaLabel', {
- defaultMessage: 'Index patterns',
-});
-
const title = i18n.translate('indexPatternManagement.indexPatternTable.title', {
defaultMessage: 'Index patterns',
});
@@ -197,25 +189,21 @@ export const IndexPatternTable = ({ canSave, history }: Props) => {
}
return (
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
- {createButton}
-
-
+
+
+ }
+ bottomBorder
+ rightSideItems={[createButton]}
+ />
+
+
+
{
sorting={sorting}
search={search}
/>
-
+
);
};
diff --git a/src/plugins/index_pattern_management/public/constants.ts b/src/plugins/index_pattern_management/public/constants.ts
new file mode 100644
index 000000000000000..e5010d133f0f301
--- /dev/null
+++ b/src/plugins/index_pattern_management/public/constants.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns';
diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts
index 6c709fb14f08d73..7671a532d1cb862 100644
--- a/src/plugins/index_pattern_management/public/mocks.ts
+++ b/src/plugins/index_pattern_management/public/mocks.ts
@@ -19,14 +19,7 @@ import {
} from './plugin';
import { IndexPatternManagmentContext } from './types';
-const createSetupContract = (): IndexPatternManagementSetup => ({
- creation: {
- addCreationConfig: jest.fn(),
- } as any,
- list: {
- addListConfig: jest.fn(),
- } as any,
-});
+const createSetupContract = (): IndexPatternManagementSetup => {};
const createStartContract = (): IndexPatternManagementStart => ({
creation: {
diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts
index e3c156927bface6..610b3541620b00b 100644
--- a/src/plugins/index_pattern_management/public/plugin.ts
+++ b/src/plugins/index_pattern_management/public/plugin.ts
@@ -81,7 +81,10 @@ export class IndexPatternManagementPlugin
},
});
- return this.indexPatternManagementService.setup({ httpClient: core.http });
+ return this.indexPatternManagementService.setup({
+ httpClient: core.http,
+ uiSettings: core.uiSettings,
+ });
}
public start(core: CoreStart, plugins: IndexPatternManagementStartDependencies) {
diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts
similarity index 53%
rename from x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js
rename to src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts
index 1d9eff8227c0acd..d1fc2fa242eb1b2 100644
--- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/index.js
+++ b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/index.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
export { RollupPrompt } from './rollup_prompt';
diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx
similarity index 76%
rename from x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js
rename to src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx
index 9306ab082dff49d..81fcdaedb90c90a 100644
--- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js
+++ b/src/plugins/index_pattern_management/public/service/creation/components/rollup_prompt/rollup_prompt.tsx
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import React from 'react';
@@ -14,7 +15,7 @@ export const RollupPrompt = () => (
{i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text',
+ 'indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text',
{
defaultMessage:
"Kibana's support for rollup index patterns is in beta. You might encounter issues using " +
@@ -25,7 +26,7 @@ export const RollupPrompt = () => (
{i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text',
+ 'indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text',
{
defaultMessage:
'You can match a rollup index pattern against one rollup index and zero or more regular ' +
diff --git a/src/plugins/index_pattern_management/public/service/creation/index.ts b/src/plugins/index_pattern_management/public/service/creation/index.ts
index 51610bc83e371bb..e1f464b01e5505d 100644
--- a/src/plugins/index_pattern_management/public/service/creation/index.ts
+++ b/src/plugins/index_pattern_management/public/service/creation/index.ts
@@ -8,3 +8,5 @@
export { IndexPatternCreationConfig, IndexPatternCreationOption } from './config';
export { IndexPatternCreationManager } from './manager';
+// @ts-ignore
+export { RollupIndexPatternCreationConfig } from './rollup_creation_config';
diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js b/src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js
similarity index 84%
rename from x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js
rename to src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js
index 8e5203fca903477..2a85dfa01143c79 100644
--- a/x-pack/plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js
+++ b/src/plugins/index_pattern_management/public/service/creation/rollup_creation_config.js
@@ -1,43 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { RollupPrompt } from './components/rollup_prompt';
-import { IndexPatternCreationConfig } from '../../../../../src/plugins/index_pattern_management/public';
+import { IndexPatternCreationConfig } from '.';
const rollupIndexPatternTypeName = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName',
{ defaultMessage: 'rollup index pattern' }
);
const rollupIndexPatternButtonText = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonText',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText',
{ defaultMessage: 'Rollup index pattern' }
);
const rollupIndexPatternButtonDescription = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonDescription',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription',
{ defaultMessage: 'Perform limited aggregations against summarized data' }
);
const rollupIndexPatternNoMatchError = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.noMatchError',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError',
{ defaultMessage: 'Rollup index pattern error: must match one rollup index' }
);
const rollupIndexPatternTooManyMatchesError = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.tooManyMatchesError',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError',
{ defaultMessage: 'Rollup index pattern error: can only match one rollup index' }
);
const rollupIndexPatternIndexLabel = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.indexLabel',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel',
{ defaultMessage: 'Rollup' }
);
@@ -127,7 +128,7 @@ export class RollupIndexPatternCreationConfig extends IndexPatternCreationConfig
if (error) {
const errorMessage = i18n.translate(
- 'xpack.rollupJobs.editRollupIndexPattern.createIndex.uncaughtError',
+ 'indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError',
{
defaultMessage: 'Rollup index pattern error: {error}',
values: {
diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts
index f30ccfcb9f3ed79..19346dbf31d185d 100644
--- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts
+++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts
@@ -6,11 +6,22 @@
* Side Public License, v 1.
*/
-import { HttpSetup } from '../../../../core/public';
-import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation';
-import { IndexPatternListManager, IndexPatternListConfig } from './list';
+import { HttpSetup, CoreSetup } from '../../../../core/public';
+import {
+ IndexPatternCreationManager,
+ IndexPatternCreationConfig,
+ RollupIndexPatternCreationConfig,
+} from './creation';
+import {
+ IndexPatternListManager,
+ IndexPatternListConfig,
+ RollupIndexPatternListConfig,
+} from './list';
+
+import { CONFIG_ROLLUPS } from '../constants';
interface SetupDependencies {
httpClient: HttpSetup;
+ uiSettings: CoreSetup['uiSettings'];
}
/**
@@ -27,17 +38,17 @@ export class IndexPatternManagementService {
this.indexPatternListConfig = new IndexPatternListManager();
}
- public setup({ httpClient }: SetupDependencies) {
+ public setup({ httpClient, uiSettings }: SetupDependencies) {
const creationManagerSetup = this.indexPatternCreationManager.setup(httpClient);
creationManagerSetup.addCreationConfig(IndexPatternCreationConfig);
const indexPatternListConfigSetup = this.indexPatternListConfig.setup();
indexPatternListConfigSetup.addListConfig(IndexPatternListConfig);
- return {
- creation: creationManagerSetup,
- list: indexPatternListConfigSetup,
- };
+ if (uiSettings.get(CONFIG_ROLLUPS)) {
+ creationManagerSetup.addCreationConfig(RollupIndexPatternCreationConfig);
+ indexPatternListConfigSetup.addListConfig(RollupIndexPatternListConfig);
+ }
}
public start() {
diff --git a/src/plugins/index_pattern_management/public/service/list/index.ts b/src/plugins/index_pattern_management/public/service/list/index.ts
index 620d4c7600733b1..738b807ac762466 100644
--- a/src/plugins/index_pattern_management/public/service/list/index.ts
+++ b/src/plugins/index_pattern_management/public/service/list/index.ts
@@ -8,3 +8,5 @@
export { IndexPatternListConfig } from './config';
export { IndexPatternListManager } from './manager';
+// @ts-ignore
+export { RollupIndexPatternListConfig } from './rollup_list_config';
diff --git a/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js b/src/plugins/index_pattern_management/public/service/list/rollup_list_config.js
similarity index 86%
rename from x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js
rename to src/plugins/index_pattern_management/public/service/list/rollup_list_config.js
index 43eee6ca27f9a01..9a80d5fd0d622b9 100644
--- a/x-pack/plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js
+++ b/src/plugins/index_pattern_management/public/service/list/rollup_list_config.js
@@ -1,11 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
-import { IndexPatternListConfig } from '../../../../../src/plugins/index_pattern_management/public';
+import { IndexPatternListConfig } from '.';
function isRollup(indexPattern) {
return (
diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json
index b9f37b67f6921b5..0e7ae7cd11c35fb 100644
--- a/src/plugins/newsfeed/kibana.json
+++ b/src/plugins/newsfeed/kibana.json
@@ -2,5 +2,6 @@
"id": "newsfeed",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredPlugins": ["screenshotMode"]
}
diff --git a/src/plugins/newsfeed/public/lib/api.test.mocks.ts b/src/plugins/newsfeed/public/lib/api.test.mocks.ts
index 677bc203cbef3ff..8ac66eae6c2f6c9 100644
--- a/src/plugins/newsfeed/public/lib/api.test.mocks.ts
+++ b/src/plugins/newsfeed/public/lib/api.test.mocks.ts
@@ -8,6 +8,7 @@
import { storageMock } from './storage.mock';
import { driverMock } from './driver.mock';
+import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver';
export const storageInstanceMock = storageMock.create();
jest.doMock('./storage', () => ({
@@ -18,3 +19,7 @@ export const driverInstanceMock = driverMock.create();
jest.doMock('./driver', () => ({
NewsfeedApiDriver: jest.fn().mockImplementation(() => driverInstanceMock),
}));
+
+jest.doMock('./never_fetch_driver', () => ({
+ NeverFetchNewsfeedApiDriver: jest.fn(() => new NeverFetchNewsfeedApiDriver()),
+}));
diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts
index a4894573932e6c6..58d06e72cd77c4a 100644
--- a/src/plugins/newsfeed/public/lib/api.test.ts
+++ b/src/plugins/newsfeed/public/lib/api.test.ts
@@ -7,12 +7,16 @@
*/
import { driverInstanceMock, storageInstanceMock } from './api.test.mocks';
+
import moment from 'moment';
import { getApi } from './api';
import { TestScheduler } from 'rxjs/testing';
import { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
import { take } from 'rxjs/operators';
+import { NewsfeedApiDriver as MockNewsfeedApiDriver } from './driver';
+import { NeverFetchNewsfeedApiDriver as MockNeverFetchNewsfeedApiDriver } from './never_fetch_driver';
+
const kibanaVersion = '8.0.0';
const newsfeedId = 'test';
@@ -46,6 +50,8 @@ describe('getApi', () => {
afterEach(() => {
storageInstanceMock.isAnyUnread$.mockReset();
driverInstanceMock.fetchNewsfeedItems.mockReset();
+ (MockNewsfeedApiDriver as jest.Mock).mockClear();
+ (MockNeverFetchNewsfeedApiDriver as jest.Mock).mockClear();
});
it('merges the newsfeed and unread observables', () => {
@@ -60,7 +66,7 @@ describe('getApi', () => {
a: createFetchResult({ feedItems: ['item' as any] }),
})
);
- const api = getApi(createConfig(1000), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(1000), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(1))).toBe('(a|)', {
a: createFetchResult({
@@ -83,7 +89,7 @@ describe('getApi', () => {
a: createFetchResult({ feedItems: ['item' as any] }),
})
);
- const api = getApi(createConfig(2), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(2), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(2))).toBe('a-(b|)', {
a: createFetchResult({
@@ -111,7 +117,7 @@ describe('getApi', () => {
a: createFetchResult({}),
})
);
- const api = getApi(createConfig(10), kibanaVersion, newsfeedId);
+ const api = getApi(createConfig(10), kibanaVersion, newsfeedId, false);
expectObservable(api.fetchResults$.pipe(take(2))).toBe('a--(b|)', {
a: createFetchResult({
@@ -123,4 +129,16 @@ describe('getApi', () => {
});
});
});
+
+ it('uses the news feed API driver if in not screenshot mode', () => {
+ getApi(createConfig(10), kibanaVersion, newsfeedId, false);
+ expect(MockNewsfeedApiDriver).toHaveBeenCalled();
+ expect(MockNeverFetchNewsfeedApiDriver).not.toHaveBeenCalled();
+ });
+
+ it('uses the never fetch news feed API driver if in not screenshot mode', () => {
+ getApi(createConfig(10), kibanaVersion, newsfeedId, true);
+ expect(MockNewsfeedApiDriver).not.toHaveBeenCalled();
+ expect(MockNeverFetchNewsfeedApiDriver).toHaveBeenCalled();
+ });
});
diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts
index 4fbbd8687b73fcd..7aafc9fd2762502 100644
--- a/src/plugins/newsfeed/public/lib/api.ts
+++ b/src/plugins/newsfeed/public/lib/api.ts
@@ -11,6 +11,7 @@ import { map, catchError, filter, mergeMap, tap } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
import { NewsfeedApiDriver } from './driver';
+import { NeverFetchNewsfeedApiDriver } from './never_fetch_driver';
import { NewsfeedStorage } from './storage';
export enum NewsfeedApiEndpoint {
@@ -40,13 +41,23 @@ export interface NewsfeedApi {
export function getApi(
config: NewsfeedPluginBrowserConfig,
kibanaVersion: string,
- newsfeedId: string
+ newsfeedId: string,
+ isScreenshotMode: boolean
): NewsfeedApi {
- const userLanguage = i18n.getLocale();
- const fetchInterval = config.fetchInterval.asMilliseconds();
- const mainInterval = config.mainInterval.asMilliseconds();
const storage = new NewsfeedStorage(newsfeedId);
- const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage);
+ const mainInterval = config.mainInterval.asMilliseconds();
+
+ const createNewsfeedApiDriver = () => {
+ if (isScreenshotMode) {
+ return new NeverFetchNewsfeedApiDriver();
+ }
+
+ const userLanguage = i18n.getLocale();
+ const fetchInterval = config.fetchInterval.asMilliseconds();
+ return new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval, storage);
+ };
+
+ const driver = createNewsfeedApiDriver();
const results$ = timer(0, mainInterval).pipe(
filter(() => driver.shouldFetch()),
diff --git a/src/plugins/newsfeed/public/lib/driver.ts b/src/plugins/newsfeed/public/lib/driver.ts
index 0efa981e8c89d67..1762c4a42878440 100644
--- a/src/plugins/newsfeed/public/lib/driver.ts
+++ b/src/plugins/newsfeed/public/lib/driver.ts
@@ -10,6 +10,7 @@ import moment from 'moment';
import * as Rx from 'rxjs';
import { NEWSFEED_DEFAULT_SERVICE_BASE_URL } from '../../common/constants';
import { ApiItem, FetchResult, NewsfeedPluginBrowserConfig } from '../types';
+import { INewsfeedApiDriver } from './types';
import { convertItems } from './convert_items';
import type { NewsfeedStorage } from './storage';
@@ -19,7 +20,7 @@ interface NewsfeedResponse {
items: ApiItem[];
}
-export class NewsfeedApiDriver {
+export class NewsfeedApiDriver implements INewsfeedApiDriver {
private readonly kibanaVersion: string;
private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service
diff --git a/src/plugins/newsfeed/public/lib/never_fetch_driver.ts b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts
new file mode 100644
index 000000000000000..e95ca9c2d499a76
--- /dev/null
+++ b/src/plugins/newsfeed/public/lib/never_fetch_driver.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Observable } from 'rxjs';
+import { FetchResult } from '../types';
+import { INewsfeedApiDriver } from './types';
+
+/**
+ * NewsfeedApiDriver variant that never fetches results. This is useful for instances where Kibana is started
+ * without any user interaction like when generating a PDF or PNG report.
+ */
+export class NeverFetchNewsfeedApiDriver implements INewsfeedApiDriver {
+ shouldFetch(): boolean {
+ return false;
+ }
+
+ fetchNewsfeedItems(): Observable {
+ throw new Error('Not implemented!');
+ }
+}
diff --git a/src/plugins/newsfeed/public/lib/types.ts b/src/plugins/newsfeed/public/lib/types.ts
new file mode 100644
index 000000000000000..5a62a929eeb7ff4
--- /dev/null
+++ b/src/plugins/newsfeed/public/lib/types.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Observable } from 'rxjs';
+import type { FetchResult, NewsfeedPluginBrowserConfig } from '../types';
+
+export interface INewsfeedApiDriver {
+ /**
+ * Check whether newsfeed items should be (re-)fetched
+ */
+ shouldFetch(): boolean;
+
+ fetchNewsfeedItems(config: NewsfeedPluginBrowserConfig['service']): Observable;
+}
diff --git a/src/plugins/newsfeed/public/plugin.test.ts b/src/plugins/newsfeed/public/plugin.test.ts
new file mode 100644
index 000000000000000..4be69feb79f5557
--- /dev/null
+++ b/src/plugins/newsfeed/public/plugin.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { take } from 'rxjs/operators';
+import { coreMock } from '../../../core/public/mocks';
+import { NewsfeedPublicPlugin } from './plugin';
+import { NewsfeedApiEndpoint } from './lib/api';
+
+describe('Newsfeed plugin', () => {
+ let plugin: NewsfeedPublicPlugin;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(() => {
+ plugin = new NewsfeedPublicPlugin(coreMock.createPluginInitializerContext());
+ });
+
+ describe('#start', () => {
+ beforeEach(() => {
+ plugin.setup(coreMock.createSetup());
+ });
+
+ beforeEach(() => {
+ /**
+ * We assume for these tests that the newsfeed stream exposed by start will fetch newsfeed items
+ * on the first tick for new subscribers
+ */
+ jest.spyOn(window, 'fetch');
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('base case', () => {
+ it('makes fetch requests', () => {
+ const startContract = plugin.start(coreMock.createStart(), {
+ screenshotMode: { isScreenshotMode: () => false },
+ });
+ const sub = startContract
+ .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do
+ .pipe(take(1))
+ .subscribe(() => {});
+ jest.runOnlyPendingTimers();
+ expect(window.fetch).toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+ });
+
+ describe('when in screenshot mode', () => {
+ it('makes no fetch requests in screenshot mode', () => {
+ const startContract = plugin.start(coreMock.createStart(), {
+ screenshotMode: { isScreenshotMode: () => true },
+ });
+ const sub = startContract
+ .createNewsFeed$(NewsfeedApiEndpoint.KIBANA) // Any endpoint will do
+ .pipe(take(1))
+ .subscribe(() => {});
+ jest.runOnlyPendingTimers();
+ expect(window.fetch).not.toHaveBeenCalled();
+ sub.unsubscribe();
+ });
+ });
+ });
+});
diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx
index fdda0a24b8bd568..656fc2ef00bb9f6 100644
--- a/src/plugins/newsfeed/public/plugin.tsx
+++ b/src/plugins/newsfeed/public/plugin.tsx
@@ -13,7 +13,7 @@ import React from 'react';
import moment from 'moment';
import { I18nProvider } from '@kbn/i18n/react';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
-import { NewsfeedPluginBrowserConfig } from './types';
+import { NewsfeedPluginBrowserConfig, NewsfeedPluginStartDependencies } from './types';
import { NewsfeedNavButton } from './components/newsfeed_header_nav_button';
import { getApi, NewsfeedApi, NewsfeedApiEndpoint } from './lib/api';
@@ -41,8 +41,10 @@ export class NewsfeedPublicPlugin
return {};
}
- public start(core: CoreStart) {
- const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA);
+ public start(core: CoreStart, { screenshotMode }: NewsfeedPluginStartDependencies) {
+ const isScreenshotMode = screenshotMode.isScreenshotMode();
+
+ const api = this.createNewsfeedApi(this.config, NewsfeedApiEndpoint.KIBANA, isScreenshotMode);
core.chrome.navControls.registerRight({
order: 1000,
mount: (target) => this.mount(api, target),
@@ -56,7 +58,7 @@ export class NewsfeedPublicPlugin
pathTemplate: `/${endpoint}/v{VERSION}.json`,
},
});
- const { fetchResults$ } = this.createNewsfeedApi(config, endpoint);
+ const { fetchResults$ } = this.createNewsfeedApi(config, endpoint, isScreenshotMode);
return fetchResults$;
},
};
@@ -68,9 +70,10 @@ export class NewsfeedPublicPlugin
private createNewsfeedApi(
config: NewsfeedPluginBrowserConfig,
- newsfeedId: NewsfeedApiEndpoint
+ newsfeedId: NewsfeedApiEndpoint,
+ isScreenshotMode: boolean
): NewsfeedApi {
- const api = getApi(config, this.kibanaVersion, newsfeedId);
+ const api = getApi(config, this.kibanaVersion, newsfeedId, isScreenshotMode);
return {
markAsRead: api.markAsRead,
fetchResults$: api.fetchResults$.pipe(
diff --git a/src/plugins/newsfeed/public/types.ts b/src/plugins/newsfeed/public/types.ts
index cca656565f4ca56..a7ff917f6f9750b 100644
--- a/src/plugins/newsfeed/public/types.ts
+++ b/src/plugins/newsfeed/public/types.ts
@@ -7,6 +7,10 @@
*/
import { Duration, Moment } from 'moment';
+import type { ScreenshotModePluginStart } from 'src/plugins/screenshot_mode/public';
+export interface NewsfeedPluginStartDependencies {
+ screenshotMode: ScreenshotModePluginStart;
+}
// Ideally, we may want to obtain the type from the configSchema and exposeToBrowser keys...
export interface NewsfeedPluginBrowserConfig {
diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json
index 66244a22336c777..18e6f2de1bc6fb2 100644
--- a/src/plugins/newsfeed/tsconfig.json
+++ b/src/plugins/newsfeed/tsconfig.json
@@ -7,13 +7,9 @@
"declaration": true,
"declarationMap": true
},
- "include": [
- "public/**/*",
- "server/**/*",
- "common/*",
- "../../../typings/**/*"
- ],
+ "include": ["public/**/*", "server/**/*", "common/*", "../../../typings/**/*"],
"references": [
- { "path": "../../core/tsconfig.json" }
+ { "path": "../../core/tsconfig.json" },
+ { "path": "../screenshot_mode/tsconfig.json" }
]
}
diff --git a/src/plugins/screenshot_mode/public/index.ts b/src/plugins/screenshot_mode/public/index.ts
index a5ad37dd5b760d5..012f57e837f4166 100644
--- a/src/plugins/screenshot_mode/public/index.ts
+++ b/src/plugins/screenshot_mode/public/index.ts
@@ -18,4 +18,4 @@ export {
KBN_SCREENSHOT_MODE_ENABLED_KEY,
} from '../common';
-export { ScreenshotModePluginSetup } from './types';
+export { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types';
diff --git a/src/plugins/screenshot_mode/public/plugin.test.ts b/src/plugins/screenshot_mode/public/plugin.test.ts
index 33ae5014668760f..f2c0970d0ff60cc 100644
--- a/src/plugins/screenshot_mode/public/plugin.test.ts
+++ b/src/plugins/screenshot_mode/public/plugin.test.ts
@@ -21,7 +21,7 @@ describe('Screenshot mode public', () => {
setScreenshotModeDisabled();
});
- describe('setup contract', () => {
+ describe('public contract', () => {
it('detects screenshot mode "true"', () => {
setScreenshotModeEnabled();
const screenshotMode = plugin.setup(coreMock.createSetup());
@@ -34,10 +34,4 @@ describe('Screenshot mode public', () => {
expect(screenshotMode.isScreenshotMode()).toBe(false);
});
});
-
- describe('start contract', () => {
- it('returns nothing', () => {
- expect(plugin.start(coreMock.createStart())).toBe(undefined);
- });
- });
});
diff --git a/src/plugins/screenshot_mode/public/plugin.ts b/src/plugins/screenshot_mode/public/plugin.ts
index 7a166566a0173bc..a005bb7c3d055dd 100644
--- a/src/plugins/screenshot_mode/public/plugin.ts
+++ b/src/plugins/screenshot_mode/public/plugin.ts
@@ -8,18 +8,22 @@
import { CoreSetup, CoreStart, Plugin } from '../../../core/public';
-import { ScreenshotModePluginSetup } from './types';
+import { ScreenshotModePluginSetup, ScreenshotModePluginStart } from './types';
import { getScreenshotMode } from '../common';
export class ScreenshotModePlugin implements Plugin {
+ private publicContract = Object.freeze({
+ isScreenshotMode: () => getScreenshotMode() === true,
+ });
+
public setup(core: CoreSetup): ScreenshotModePluginSetup {
- return {
- isScreenshotMode: () => getScreenshotMode() === true,
- };
+ return this.publicContract;
}
- public start(core: CoreStart) {}
+ public start(core: CoreStart): ScreenshotModePluginStart {
+ return this.publicContract;
+ }
public stop() {}
}
diff --git a/src/plugins/screenshot_mode/public/types.ts b/src/plugins/screenshot_mode/public/types.ts
index 744ea8615f2a790..f6963de0cbd63f2 100644
--- a/src/plugins/screenshot_mode/public/types.ts
+++ b/src/plugins/screenshot_mode/public/types.ts
@@ -15,3 +15,4 @@ export interface IScreenshotModeService {
}
export type ScreenshotModePluginSetup = IScreenshotModeService;
+export type ScreenshotModePluginStart = IScreenshotModeService;
diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
index 17a91a4d43cc767..cbfece0b081c611 100644
--- a/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
+++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap
@@ -5,6 +5,7 @@ Object {
"as": "tagloud_vis",
"type": "render",
"value": Object {
+ "syncColors": false,
"visData": Object {
"columns": Array [
Object {
@@ -20,6 +21,12 @@ Object {
"type": "datatable",
},
"visParams": Object {
+ "bucket": Object {
+ "accessor": 1,
+ "format": Object {
+ "id": "number",
+ },
+ },
"maxFontSize": 72,
"metric": Object {
"accessor": 0,
@@ -29,6 +36,10 @@ Object {
},
"minFontSize": 18,
"orientation": "single",
+ "palette": Object {
+ "name": "default",
+ "type": "palette",
+ },
"scale": "linear",
"showLabel": true,
},
diff --git a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap
index a8bc0b4c51678a3..fed6fb54288f27c 100644
--- a/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap
+++ b/src/plugins/vis_type_tagcloud/public/__snapshots__/to_ast.test.ts.snap
@@ -84,6 +84,9 @@ Object {
"orientation": Array [
"single",
],
+ "palette": Array [
+ "default",
+ ],
"scale": Array [
"linear",
],
diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
deleted file mode 100644
index 88ed7c66a79a2b5..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foo bar foobar "`;
diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
deleted file mode 100644
index d7707f64d8a4fce..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CN IN US DE BR "`;
-
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CN IN US DE BR "`;
-
-exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CN IN US DE BR "`;
diff --git a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js
deleted file mode 100644
index 9e1d66b0a2faaea..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/feedback_message.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import React, { Component, Fragment } from 'react';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiIconTip } from '@elastic/eui';
-
-export class FeedbackMessage extends Component {
- constructor() {
- super();
- this.state = { shouldShowTruncate: false, shouldShowIncomplete: false };
- }
-
- render() {
- if (!this.state.shouldShowTruncate && !this.state.shouldShowIncomplete) {
- return '';
- }
-
- return (
-
- {this.state.shouldShowTruncate && (
-
-
-
- )}
- {this.state.shouldShowIncomplete && (
-
-
-
- )}
-
- }
- />
- );
- }
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.tsx
new file mode 100644
index 000000000000000..82663bbf7070cae
--- /dev/null
+++ b/src/plugins/vis_type_tagcloud/public/components/get_tag_cloud_options.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { lazy } from 'react';
+import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
+import { TagCloudVisParams, TagCloudTypeProps } from '../types';
+
+const TagCloudOptionsLazy = lazy(() => import('./tag_cloud_options'));
+
+export const getTagCloudOptions = ({ palettes }: TagCloudTypeProps) => (
+ props: VisEditorOptionsProps
+) => ;
diff --git a/src/plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js
deleted file mode 100644
index 028a001cfbe634e..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/label.js
+++ /dev/null
@@ -1,27 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import React, { Component } from 'react';
-
-export class Label extends Component {
- constructor() {
- super();
- this.state = { label: '', shouldShowLabel: true };
- }
-
- render() {
- return (
-
- {this.state.label}
-
- );
- }
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
deleted file mode 100644
index 254d210eebf3767..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js
+++ /dev/null
@@ -1,409 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import d3 from 'd3';
-import d3TagCloud from 'd3-cloud';
-import { EventEmitter } from 'events';
-
-const ORIENTATIONS = {
- single: () => 0,
- 'right angled': (tag) => {
- return hashWithinRange(tag.text, 2) * 90;
- },
- multiple: (tag) => {
- return hashWithinRange(tag.text, 12) * 15 - 90; //fan out 12 * 15 degrees over top-right and bottom-right quadrant (=-90 deg offset)
- },
-};
-const D3_SCALING_FUNCTIONS = {
- linear: () => d3.scale.linear(),
- log: () => d3.scale.log(),
- 'square root': () => d3.scale.sqrt(),
-};
-
-export class TagCloud extends EventEmitter {
- constructor(domNode, colorScale) {
- super();
-
- //DOM
- this._element = domNode;
- this._d3SvgContainer = d3.select(this._element).append('svg');
- this._svgGroup = this._d3SvgContainer.append('g');
- this._size = [1, 1];
- this.resize();
-
- //SETTING (non-configurable)
- /**
- * the fontFamily should be set explicitly for calculating a layout
- * and to avoid words overlapping
- */
- this._fontFamily = 'Inter UI, sans-serif';
- this._fontStyle = 'normal';
- this._fontWeight = 'normal';
- this._spiral = 'archimedean'; //layout shape
- this._timeInterval = 1000; //time allowed for layout algorithm
- this._padding = 5;
-
- //OPTIONS
- this._orientation = 'single';
- this._minFontSize = 10;
- this._maxFontSize = 36;
- this._textScale = 'linear';
- this._optionsAsString = null;
-
- //DATA
- this._words = null;
-
- //UTIL
- this._colorScale = colorScale;
- this._setTimeoutId = null;
- this._pendingJob = null;
- this._layoutIsUpdating = null;
- this._allInViewBox = false;
- this._DOMisUpdating = false;
- }
-
- setOptions(options) {
- if (JSON.stringify(options) === this._optionsAsString) {
- return;
- }
- this._optionsAsString = JSON.stringify(options);
- this._orientation = options.orientation;
- this._minFontSize = Math.min(options.minFontSize, options.maxFontSize);
- this._maxFontSize = Math.max(options.minFontSize, options.maxFontSize);
- this._textScale = options.scale;
- this._invalidate(false);
- }
-
- resize() {
- const newWidth = this._element.offsetWidth;
- const newHeight = this._element.offsetHeight;
-
- if (newWidth === this._size[0] && newHeight === this._size[1]) {
- return;
- }
-
- const wasInside = this._size[0] >= this._cloudWidth && this._size[1] >= this._cloudHeight;
- const willBeInside = this._cloudWidth <= newWidth && this._cloudHeight <= newHeight;
- this._size[0] = newWidth;
- this._size[1] = newHeight;
- if (wasInside && willBeInside && this._allInViewBox) {
- this._invalidate(true);
- } else {
- this._invalidate(false);
- }
- }
-
- setData(data) {
- this._words = data;
- this._invalidate(false);
- }
-
- destroy() {
- clearTimeout(this._setTimeoutId);
- this._element.innerHTML = '';
- }
-
- getStatus() {
- return this._allInViewBox ? TagCloud.STATUS.COMPLETE : TagCloud.STATUS.INCOMPLETE;
- }
-
- _updateContainerSize() {
- this._d3SvgContainer.attr('width', this._size[0]);
- this._d3SvgContainer.attr('height', this._size[1]);
- this._svgGroup.attr('width', this._size[0]);
- this._svgGroup.attr('height', this._size[1]);
- }
-
- _isJobRunning() {
- return this._setTimeoutId || this._layoutIsUpdating || this._DOMisUpdating;
- }
-
- async _processPendingJob() {
- if (!this._pendingJob) {
- return;
- }
-
- if (this._isJobRunning()) {
- return;
- }
-
- this._completedJob = null;
- const job = await this._pickPendingJob();
- if (job.words.length) {
- if (job.refreshLayout) {
- await this._updateLayout(job);
- }
- await this._updateDOM(job);
- const cloudBBox = this._svgGroup[0][0].getBBox();
- this._cloudWidth = cloudBBox.width;
- this._cloudHeight = cloudBBox.height;
- this._allInViewBox =
- cloudBBox.x >= 0 &&
- cloudBBox.y >= 0 &&
- cloudBBox.x + cloudBBox.width <= this._element.offsetWidth &&
- cloudBBox.y + cloudBBox.height <= this._element.offsetHeight;
- } else {
- this._emptyDOM(job);
- }
-
- if (this._pendingJob) {
- this._processPendingJob(); //pick up next job
- } else {
- this._completedJob = job;
- this.emit('renderComplete');
- }
- }
-
- async _pickPendingJob() {
- return await new Promise((resolve) => {
- this._setTimeoutId = setTimeout(async () => {
- const job = this._pendingJob;
- this._pendingJob = null;
- this._setTimeoutId = null;
- resolve(job);
- }, 0);
- });
- }
-
- _emptyDOM() {
- this._svgGroup.selectAll('text').remove();
- this._cloudWidth = 0;
- this._cloudHeight = 0;
- this._allInViewBox = true;
- this._DOMisUpdating = false;
- }
-
- async _updateDOM(job) {
- const canSkipDomUpdate = this._pendingJob || this._setTimeoutId;
- if (canSkipDomUpdate) {
- this._DOMisUpdating = false;
- return;
- }
-
- this._DOMisUpdating = true;
- const affineTransform = positionWord.bind(
- null,
- this._element.offsetWidth / 2,
- this._element.offsetHeight / 2
- );
- const svgTextNodes = this._svgGroup.selectAll('text');
- const stage = svgTextNodes.data(job.words, getText);
-
- await new Promise((resolve) => {
- const enterSelection = stage.enter();
- const enteringTags = enterSelection.append('text');
- enteringTags.style('font-size', getSizeInPixels);
- enteringTags.style('font-style', this._fontStyle);
- enteringTags.style('font-weight', () => this._fontWeight);
- enteringTags.style('font-family', () => this._fontFamily);
- enteringTags.style('fill', this.getFill.bind(this));
- enteringTags.attr('text-anchor', () => 'middle');
- enteringTags.attr('transform', affineTransform);
- enteringTags.attr('data-test-subj', getDisplayText);
- enteringTags.text(getDisplayText);
-
- const self = this;
- enteringTags.on({
- click: function (event) {
- self.emit('select', event);
- },
- mouseover: function () {
- d3.select(this).style('cursor', 'pointer');
- },
- mouseout: function () {
- d3.select(this).style('cursor', 'default');
- },
- });
-
- const movingTags = stage.transition();
- movingTags.duration(600);
- movingTags.style('font-size', getSizeInPixels);
- movingTags.style('font-style', this._fontStyle);
- movingTags.style('font-weight', () => this._fontWeight);
- movingTags.style('font-family', () => this._fontFamily);
- movingTags.attr('transform', affineTransform);
-
- const exitingTags = stage.exit();
- const exitTransition = exitingTags.transition();
- exitTransition.duration(200);
- exitingTags.style('fill-opacity', 1e-6);
- exitingTags.attr('font-size', 1);
- exitingTags.remove();
-
- let exits = 0;
- let moves = 0;
- const resolveWhenDone = () => {
- if (exits === 0 && moves === 0) {
- this._DOMisUpdating = false;
- resolve(true);
- }
- };
- exitTransition.each(() => exits++);
- exitTransition.each('end', () => {
- exits--;
- resolveWhenDone();
- });
- movingTags.each(() => moves++);
- movingTags.each('end', () => {
- moves--;
- resolveWhenDone();
- });
- });
- }
-
- _makeTextSizeMapper() {
- const mapSizeToFontSize = D3_SCALING_FUNCTIONS[this._textScale]();
- const range =
- this._words.length === 1
- ? [this._maxFontSize, this._maxFontSize]
- : [this._minFontSize, this._maxFontSize];
- mapSizeToFontSize.range(range);
- if (this._words) {
- mapSizeToFontSize.domain(d3.extent(this._words, getValue));
- }
- return mapSizeToFontSize;
- }
-
- _makeNewJob() {
- return {
- refreshLayout: true,
- size: this._size.slice(),
- words: this._words,
- };
- }
-
- _makeJobPreservingLayout() {
- return {
- refreshLayout: false,
- size: this._size.slice(),
- words: this._completedJob.words.map((tag) => {
- return {
- x: tag.x,
- y: tag.y,
- rotate: tag.rotate,
- size: tag.size,
- rawText: tag.rawText || tag.text,
- displayText: tag.displayText,
- meta: tag.meta,
- };
- }),
- };
- }
-
- _invalidate(keepLayout) {
- if (!this._words) {
- return;
- }
-
- this._updateContainerSize();
-
- const canReuseLayout = keepLayout && !this._isJobRunning() && this._completedJob;
- this._pendingJob = canReuseLayout ? this._makeJobPreservingLayout() : this._makeNewJob();
- this._processPendingJob();
- }
-
- async _updateLayout(job) {
- if (job.size[0] <= 0 || job.size[1] <= 0) {
- // If either width or height isn't above 0 we don't relayout anything,
- // since the d3-cloud will be stuck in an infinite loop otherwise.
- return;
- }
-
- const mapSizeToFontSize = this._makeTextSizeMapper();
- const tagCloudLayoutGenerator = d3TagCloud();
- tagCloudLayoutGenerator.size(job.size);
- tagCloudLayoutGenerator.padding(this._padding);
- tagCloudLayoutGenerator.rotate(ORIENTATIONS[this._orientation]);
- tagCloudLayoutGenerator.font(this._fontFamily);
- tagCloudLayoutGenerator.fontStyle(this._fontStyle);
- tagCloudLayoutGenerator.fontWeight(this._fontWeight);
- tagCloudLayoutGenerator.fontSize((tag) => mapSizeToFontSize(tag.value));
- tagCloudLayoutGenerator.random(seed);
- tagCloudLayoutGenerator.spiral(this._spiral);
- tagCloudLayoutGenerator.words(job.words);
- tagCloudLayoutGenerator.text(getDisplayText);
- tagCloudLayoutGenerator.timeInterval(this._timeInterval);
-
- this._layoutIsUpdating = true;
- await new Promise((resolve) => {
- tagCloudLayoutGenerator.on('end', () => {
- this._layoutIsUpdating = false;
- resolve(true);
- });
- tagCloudLayoutGenerator.start();
- });
- }
-
- /**
- * Returns debug info. For debugging only.
- * @return {*}
- */
- getDebugInfo() {
- const debug = {};
- debug.positions = this._completedJob
- ? this._completedJob.words.map((tag) => {
- return {
- displayText: tag.displayText,
- rawText: tag.rawText || tag.text,
- x: tag.x,
- y: tag.y,
- rotate: tag.rotate,
- };
- })
- : [];
- debug.size = {
- width: this._size[0],
- height: this._size[1],
- };
- return debug;
- }
-
- getFill(tag) {
- return this._colorScale(tag.text);
- }
-}
-
-TagCloud.STATUS = { COMPLETE: 0, INCOMPLETE: 1 };
-
-function seed() {
- return 0.5; //constant seed (not random) to ensure constant layouts for identical data
-}
-
-function getText(word) {
- return word.rawText;
-}
-
-function getDisplayText(word) {
- return word.displayText;
-}
-
-function positionWord(xTranslate, yTranslate, word) {
- if (isNaN(word.x) || isNaN(word.y) || isNaN(word.rotate)) {
- //move off-screen
- return `translate(${xTranslate * 3}, ${yTranslate * 3})rotate(0)`;
- }
-
- return `translate(${word.x + xTranslate}, ${word.y + yTranslate})rotate(${word.rotate})`;
-}
-
-function getValue(tag) {
- return tag.value;
-}
-
-function getSizeInPixels(tag) {
- return `${tag.size}px`;
-}
-
-function hashWithinRange(str, max) {
- str = JSON.stringify(str);
- let hash = 0;
- for (const ch of str) {
- hash = (hash * 31 + ch.charCodeAt(0)) % max;
- }
- return Math.abs(hash) % max;
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss
index 37867f1ed1c178c..51b5e9dedd84420 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.scss
@@ -5,18 +5,14 @@
// tgcChart__legend--small
// tgcChart__legend-isLoading
-.tgcChart__container, .tgcChart__wrapper {
+.tgcChart__wrapper {
flex: 1 1 0;
display: flex;
+ flex-direction: column;
}
-.tgcChart {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
+.tgcChart__wrapper text {
+ cursor: pointer;
}
.tgcChart__label {
@@ -24,3 +20,7 @@
text-align: center;
font-weight: $euiFontWeightBold;
}
+
+.tgcChart__warning {
+ width: $euiSize;
+}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js
deleted file mode 100644
index eb575457146c5d6..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js
+++ /dev/null
@@ -1,507 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import _ from 'lodash';
-import d3 from 'd3';
-import 'jest-canvas-mock';
-
-import { fromNode, delay } from 'bluebird';
-import { TagCloud } from './tag_cloud';
-import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest';
-
-describe('tag cloud tests', () => {
- let SVGElementGetBBoxSpyInstance;
- let HTMLElementOffsetMockInstance;
-
- beforeEach(() => {
- setupDOM();
- });
-
- afterEach(() => {
- SVGElementGetBBoxSpyInstance.mockRestore();
- HTMLElementOffsetMockInstance.mockRestore();
- });
-
- const minValue = 1;
- const maxValue = 9;
- const midValue = (minValue + maxValue) / 2;
- const baseTest = {
- data: [
- { rawText: 'foo', displayText: 'foo', value: minValue },
- { rawText: 'bar', displayText: 'bar', value: midValue },
- { rawText: 'foobar', displayText: 'foobar', value: maxValue },
- ],
- options: {
- orientation: 'single',
- scale: 'linear',
- minFontSize: 10,
- maxFontSize: 36,
- },
- expected: [
- {
- text: 'foo',
- fontSize: '10px',
- },
- {
- text: 'bar',
- fontSize: '23px',
- },
- {
- text: 'foobar',
- fontSize: '36px',
- },
- ],
- };
-
- const singleLayoutTest = _.cloneDeep(baseTest);
-
- const rightAngleLayoutTest = _.cloneDeep(baseTest);
- rightAngleLayoutTest.options.orientation = 'right angled';
-
- const multiLayoutTest = _.cloneDeep(baseTest);
- multiLayoutTest.options.orientation = 'multiple';
-
- const mapWithLog = d3.scale.log();
- mapWithLog.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]);
- mapWithLog.domain([minValue, maxValue]);
- const logScaleTest = _.cloneDeep(baseTest);
- logScaleTest.options.scale = 'log';
- logScaleTest.expected[1].fontSize = Math.round(mapWithLog(midValue)) + 'px';
-
- const mapWithSqrt = d3.scale.sqrt();
- mapWithSqrt.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]);
- mapWithSqrt.domain([minValue, maxValue]);
- const sqrtScaleTest = _.cloneDeep(baseTest);
- sqrtScaleTest.options.scale = 'square root';
- sqrtScaleTest.expected[1].fontSize = Math.round(mapWithSqrt(midValue)) + 'px';
-
- const biggerFontTest = _.cloneDeep(baseTest);
- biggerFontTest.options.minFontSize = 36;
- biggerFontTest.options.maxFontSize = 72;
- biggerFontTest.expected[0].fontSize = '36px';
- biggerFontTest.expected[1].fontSize = '54px';
- biggerFontTest.expected[2].fontSize = '72px';
-
- const trimDataTest = _.cloneDeep(baseTest);
- trimDataTest.data.splice(1, 1);
- trimDataTest.expected.splice(1, 1);
-
- let domNode;
- let tagCloud;
-
- const colorScale = d3.scale
- .ordinal()
- .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']);
-
- function setupDOM() {
- domNode = document.createElement('div');
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox();
- HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512);
-
- document.body.appendChild(domNode);
- }
-
- function teardownDOM() {
- domNode.innerHTML = '';
- document.body.removeChild(domNode);
- }
-
- [
- singleLayoutTest,
- rightAngleLayoutTest,
- multiLayoutTest,
- logScaleTest,
- sqrtScaleTest,
- biggerFontTest,
- trimDataTest,
- ].forEach(function (currentTest) {
- describe(`should position elements correctly for options: ${JSON.stringify(
- currentTest.options
- )}`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(currentTest.data);
- tagCloud.setOptions(currentTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
-
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(currentTest.expected, textElements, tagCloud);
- })
- );
- });
- });
-
- [5, 100, 200, 300, 500].forEach((timeout) => {
- // FLAKY: https://github.com/elastic/kibana/issues/94043
- describe.skip(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => {
- beforeEach(async () => {
- //TagCloud takes at least 600ms to complete (due to d3 animation)
- //renderComplete should only notify at the last one
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
-
- //this timeout modifies the settings before the cloud is rendered.
- //the cloud needs to use the correct options
- setTimeout(() => tagCloud.setOptions(logScaleTest.options), timeout);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
-
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(logScaleTest.expected, textElements, tagCloud);
- })
- );
- });
- });
-
- describe('should use the latest state before notifying (when modifying options multiple times)', () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- tagCloud.setOptions(logScaleTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(logScaleTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe('should use the latest state before notifying (when modifying data multiple times)', () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- tagCloud.setData(trimDataTest.data);
-
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(trimDataTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe('should not get multiple render-events', () => {
- let counter;
- beforeEach(() => {
- counter = 0;
-
- return new Promise((resolve, reject) => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
-
- setTimeout(() => {
- //this should be overridden by later changes
- tagCloud.setData(sqrtScaleTest.data);
- tagCloud.setOptions(sqrtScaleTest.options);
- }, 100);
-
- setTimeout(() => {
- //latest change
- tagCloud.setData(logScaleTest.data);
- tagCloud.setOptions(logScaleTest.options);
- }, 300);
-
- tagCloud.on('renderComplete', function onRender() {
- if (counter > 0) {
- reject('Should not get multiple render events');
- }
- counter += 1;
- resolve(true);
- });
- });
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(logScaleTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe('should show correct data when state-updates are interleaved with resize event', () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(logScaleTest.data);
- tagCloud.setOptions(logScaleTest.options);
-
- await delay(1000); //let layout run
-
- SVGElementGetBBoxSpyInstance.mockRestore();
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600);
-
- tagCloud.resize(); //triggers new layout
- setTimeout(() => {
- //change the options at the very end too
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- }, 200);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- test(
- 'positions should be ok',
- handleExpectedBlip(() => {
- const textElements = domNode.querySelectorAll('text');
- verifyTagProperties(baseTest.expected, textElements, tagCloud);
- })
- );
- });
-
- describe(`should not put elements in view when container is too small`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test('completeness should not be ok', () => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE);
- });
- test('positions should not be ok', () => {
- const textElements = domNode.querySelectorAll('text');
- for (let i = 0; i < textElements; i++) {
- const bbox = textElements[i].getBoundingClientRect();
- verifyBbox(bbox, false, tagCloud);
- }
- });
- });
-
- describe(`tags should fit after making container bigger`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
-
- //make bigger
- tagCloud._size = [600, 600];
- tagCloud.resize();
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test(
- 'completeness should be ok',
- handleExpectedBlip(() => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE);
- })
- );
- });
-
- describe(`tags should no longer fit after making container smaller`, () => {
- beforeEach(async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
-
- //make smaller
- tagCloud._size = [];
- tagCloud.resize();
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
- });
-
- afterEach(teardownDOM);
-
- test('completeness should not be ok', () => {
- expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE);
- });
- });
-
- describe('tagcloudscreenshot', () => {
- afterEach(teardownDOM);
-
- test('should render simple image', async () => {
- tagCloud = new TagCloud(domNode, colorScale);
- tagCloud.setData(baseTest.data);
- tagCloud.setOptions(baseTest.options);
-
- await fromNode((cb) => tagCloud.once('renderComplete', cb));
-
- expect(domNode.innerHTML).toMatchSnapshot();
- });
- });
-
- function verifyTagProperties(expectedValues, actualElements, tagCloud) {
- expect(actualElements.length).toEqual(expectedValues.length);
- expectedValues.forEach((test, index) => {
- try {
- expect(actualElements[index].style.fontSize).toEqual(test.fontSize);
- } catch (e) {
- throw new Error('fontsize is not correct: ' + e.message);
- }
- try {
- expect(actualElements[index].innerHTML).toEqual(test.text);
- } catch (e) {
- throw new Error('fontsize is not correct: ' + e.message);
- }
- isInsideContainer(actualElements[index], tagCloud);
- });
- }
-
- function isInsideContainer(actualElement, tagCloud) {
- const bbox = actualElement.getBoundingClientRect();
- verifyBbox(bbox, true, tagCloud);
- }
-
- function verifyBbox(bbox, shouldBeInside, tagCloud) {
- const message = ` | bbox-of-tag: ${JSON.stringify([
- bbox.left,
- bbox.top,
- bbox.right,
- bbox.bottom,
- ])} vs
- bbox-of-container: ${domNode.offsetWidth},${domNode.offsetHeight}
- debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`;
-
- try {
- expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message
- );
- }
- try {
- expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'bottom boundary of tag should have been ' +
- (shouldBeInside ? 'inside' : 'outside') +
- message
- );
- }
- try {
- expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message
- );
- }
- try {
- expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside);
- } catch (e) {
- throw new Error(
- 'right boundary of tag should have been ' +
- (shouldBeInside ? 'inside' : 'outside') +
- message
- );
- }
- }
-
- /**
- * In CI, this entire suite "blips" about 1/5 times.
- * This blip causes the majority of these tests fail for the exact same reason: One tag is centered inside the container,
- * while the others are moved out.
- * This has not been reproduced locally yet.
- * It may be an issue with the 3rd party d3-cloud that snags.
- *
- * The test suite should continue to catch reliably catch regressions of other sorts: unexpected and other uncaught errors,
- * scaling issues, ordering issues
- *
- */
- function shouldAssert() {
- const debugInfo = tagCloud.getDebugInfo();
- const count = debugInfo.positions.length;
- const largest = debugInfo.positions.pop(); //test suite puts largest tag at the end.
-
- const centered = largest[1] === 0 && largest[2] === 0;
- const halfWidth = debugInfo.size.width / 2;
- const halfHeight = debugInfo.size.height / 2;
- const inside = debugInfo.positions.filter((position) => {
- const x = position.x + halfWidth;
- const y = position.y + halfHeight;
- return 0 <= x && x <= debugInfo.size.width && 0 <= y && y <= debugInfo.size.height;
- });
-
- return centered && inside.length === count - 1;
- }
-
- function handleExpectedBlip(assertion) {
- return () => {
- if (!shouldAssert()) {
- return;
- }
- assertion();
- };
- }
-});
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx
new file mode 100644
index 000000000000000..b4d4e70d5ffe3e2
--- /dev/null
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.test.tsx
@@ -0,0 +1,150 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+import { Wordcloud, Settings } from '@elastic/charts';
+import { chartPluginMock } from '../../../charts/public/mocks';
+import type { Datatable } from '../../../expressions/public';
+import { mount } from 'enzyme';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import TagCloudChart, { TagCloudChartProps } from './tag_cloud_chart';
+import { TagCloudVisParams } from '../types';
+
+jest.mock('../services', () => ({
+ getFormatService: jest.fn(() => {
+ return {
+ deserialize: jest.fn(),
+ };
+ }),
+}));
+
+const palettesRegistry = chartPluginMock.createPaletteRegistry();
+const visData = ({
+ columns: [
+ {
+ id: 'col-0',
+ name: 'geo.dest: Descending',
+ },
+ {
+ id: 'col-1',
+ name: 'Count',
+ },
+ ],
+ rows: [
+ { 'col-0': 'CN', 'col-1': 26 },
+ { 'col-0': 'IN', 'col-1': 17 },
+ { 'col-0': 'US', 'col-1': 6 },
+ { 'col-0': 'DE', 'col-1': 4 },
+ { 'col-0': 'BR', 'col-1': 3 },
+ ],
+} as unknown) as Datatable;
+
+const visParams = {
+ bucket: { accessor: 0, format: {} },
+ metric: { accessor: 1, format: {} },
+ scale: 'linear',
+ orientation: 'single',
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ minFontSize: 12,
+ maxFontSize: 70,
+ showLabel: true,
+} as TagCloudVisParams;
+
+describe('TagCloudChart', function () {
+ let wrapperProps: TagCloudChartProps;
+
+ beforeAll(() => {
+ wrapperProps = {
+ visData,
+ visParams,
+ palettesRegistry,
+ fireEvent: jest.fn(),
+ renderComplete: jest.fn(),
+ syncColors: false,
+ visType: 'tagcloud',
+ };
+ });
+
+ it('renders the Wordcloud component', async () => {
+ const component = mount( );
+ expect(component.find(Wordcloud).length).toBe(1);
+ });
+
+ it('renders the label correctly', async () => {
+ const component = mount( );
+ const label = findTestSubject(component, 'tagCloudLabel');
+ expect(label.text()).toEqual('geo.dest: Descending - Count');
+ });
+
+ it('not renders the label if showLabel setting is off', async () => {
+ const newVisParams = { ...visParams, showLabel: false };
+ const newProps = { ...wrapperProps, visParams: newVisParams };
+ const component = mount( );
+ const label = findTestSubject(component, 'tagCloudLabel');
+ expect(label.length).toBe(0);
+ });
+
+ it('receives the data on the correct format', () => {
+ const component = mount( );
+ expect(component.find(Wordcloud).prop('data')).toStrictEqual([
+ {
+ color: 'black',
+ text: 'CN',
+ weight: 1,
+ },
+ {
+ color: 'black',
+ text: 'IN',
+ weight: 0.6086956521739131,
+ },
+ {
+ color: 'black',
+ text: 'US',
+ weight: 0.13043478260869565,
+ },
+ {
+ color: 'black',
+ text: 'DE',
+ weight: 0.043478260869565216,
+ },
+ {
+ color: 'black',
+ text: 'BR',
+ weight: 0,
+ },
+ ]);
+ });
+
+ it('sets the angles correctly', async () => {
+ const newVisParams = { ...visParams, orientation: 'right angled' } as TagCloudVisParams;
+ const newProps = { ...wrapperProps, visParams: newVisParams };
+ const component = mount( );
+ expect(component.find(Wordcloud).prop('endAngle')).toBe(90);
+ expect(component.find(Wordcloud).prop('angleCount')).toBe(2);
+ });
+
+ it('calls filter callback', () => {
+ const component = mount( );
+ component.find(Settings).prop('onElementClick')!([
+ [
+ {
+ text: 'BR',
+ weight: 0.17391304347826086,
+ color: '#d36086',
+ },
+ {
+ specId: 'tagCloud',
+ key: 'tagCloud',
+ },
+ ],
+ ]);
+ expect(wrapperProps.fireEvent).toHaveBeenCalled();
+ });
+});
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
index f668e22815b60f2..b89fe2fa90ede0a 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_chart.tsx
@@ -6,64 +6,225 @@
* Side Public License, v 1.
*/
-import React, { useEffect, useMemo, useRef } from 'react';
-import { EuiResizeObserver } from '@elastic/eui';
+import React, { useCallback, useState, useMemo } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
import { throttle } from 'lodash';
-
-import { TagCloudVisDependencies } from '../plugin';
+import { EuiIconTip, EuiResizeObserver } from '@elastic/eui';
+import { Chart, Settings, Wordcloud, RenderChangeListener } from '@elastic/charts';
+import type { PaletteRegistry } from '../../../charts/public';
+import type { IInterpreterRenderHandlers } from '../../../expressions/public';
+import { getFormatService } from '../services';
import { TagCloudVisRenderValue } from '../tag_cloud_fn';
-// @ts-ignore
-import { TagCloudVisualization } from './tag_cloud_visualization';
import './tag_cloud.scss';
-type TagCloudChartProps = TagCloudVisDependencies &
- TagCloudVisRenderValue & {
- fireEvent: (event: any) => void;
- renderComplete: () => void;
- };
+const MAX_TAG_COUNT = 200;
+
+export type TagCloudChartProps = TagCloudVisRenderValue & {
+ fireEvent: IInterpreterRenderHandlers['event'];
+ renderComplete: IInterpreterRenderHandlers['done'];
+ palettesRegistry: PaletteRegistry;
+};
+
+const calculateWeight = (value: number, x1: number, y1: number, x2: number, y2: number) =>
+ ((value - x1) * (y2 - x2)) / (y1 - x1) + x2;
+
+const getColor = (
+ palettes: PaletteRegistry,
+ activePalette: string,
+ text: string,
+ values: string[],
+ syncColors: boolean
+) => {
+ return palettes?.get(activePalette).getCategoricalColor(
+ [
+ {
+ name: text,
+ rankAtDepth: values.length ? values.findIndex((name) => name === text) : 0,
+ totalSeriesAtDepth: values.length || 1,
+ },
+ ],
+ {
+ maxDepth: 1,
+ totalSeries: values.length || 1,
+ behindText: false,
+ syncColors,
+ }
+ );
+};
+
+const ORIENTATIONS = {
+ single: {
+ endAngle: 0,
+ angleCount: 360,
+ },
+ 'right angled': {
+ endAngle: 90,
+ angleCount: 2,
+ },
+ multiple: {
+ endAngle: -90,
+ angleCount: 12,
+ },
+};
export const TagCloudChart = ({
- colors,
visData,
visParams,
+ palettesRegistry,
fireEvent,
renderComplete,
+ syncColors,
}: TagCloudChartProps) => {
- const chartDiv = useRef(null);
- const visController = useRef(null);
+ const [warning, setWarning] = useState(false);
+ const { bucket, metric, scale, palette, showLabel, orientation } = visParams;
+ const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null;
- useEffect(() => {
- if (chartDiv.current) {
- visController.current = new TagCloudVisualization(chartDiv.current, colors, fireEvent);
- }
- return () => {
- visController.current.destroy();
- visController.current = null;
- };
- }, [colors, fireEvent]);
-
- useEffect(() => {
- if (visController.current) {
- visController.current.render(visData, visParams).then(renderComplete);
- }
- }, [visData, visParams, renderComplete]);
+ const tagCloudData = useMemo(() => {
+ const tagColumn = bucket ? visData.columns[bucket.accessor].id : -1;
+ const metricColumn = visData.columns[metric.accessor]?.id;
+
+ const metrics = visData.rows.map((row) => row[metricColumn]);
+ const values = bucket ? visData.rows.map((row) => row[tagColumn]) : [];
+ const maxValue = Math.max(...metrics);
+ const minValue = Math.min(...metrics);
+
+ return visData.rows.map((row) => {
+ const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn];
+ return {
+ text: (bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag) as string,
+ weight:
+ tag === 'all' || visData.rows.length <= 1
+ ? 1
+ : calculateWeight(row[metricColumn], minValue, maxValue, 0, 1) || 0,
+ color: getColor(palettesRegistry, palette.name, tag, values, syncColors) || 'rgba(0,0,0,0)',
+ };
+ });
+ }, [
+ bucket,
+ bucketFormatter,
+ metric.accessor,
+ palette.name,
+ palettesRegistry,
+ syncColors,
+ visData.columns,
+ visData.rows,
+ ]);
+
+ const label = bucket
+ ? `${visData.columns[bucket.accessor].name} - ${visData.columns[metric.accessor].name}`
+ : '';
+
+ const onRenderChange = useCallback(
+ (isRendered) => {
+ if (isRendered) {
+ renderComplete();
+ }
+ },
+ [renderComplete]
+ );
- const updateChartSize = useMemo(
+ const updateChart = useMemo(
() =>
throttle(() => {
- if (visController.current) {
- visController.current.render(visData, visParams).then(renderComplete);
- }
+ setWarning(false);
}, 300),
- [renderComplete, visData, visParams]
+ []
+ );
+
+ const handleWordClick = useCallback(
+ (d) => {
+ if (!bucket) {
+ return;
+ }
+ const termsBucket = visData.columns[bucket.accessor];
+ const clickedValue = d[0][0].text;
+
+ const rowIndex = visData.rows.findIndex((row) => {
+ const formattedValue = bucketFormatter
+ ? bucketFormatter.convert(row[termsBucket.id], 'text')
+ : row[termsBucket.id];
+ return formattedValue === clickedValue;
+ });
+
+ if (rowIndex < 0) {
+ return;
+ }
+
+ fireEvent({
+ name: 'filterBucket',
+ data: {
+ data: [
+ {
+ table: visData,
+ column: bucket.accessor,
+ row: rowIndex,
+ },
+ ],
+ },
+ });
+ },
+ [bucket, bucketFormatter, fireEvent, visData]
);
return (
-
+
{(resizeRef) => (
-
-
+
+
+
+ {
+ setWarning(true);
+ }}
+ />
+
+ {label && showLabel && (
+
+ {label}
+
+ )}
+ {warning && (
+
+
+ }
+ />
+
+ )}
+ {tagCloudData.length > MAX_TAG_COUNT && (
+
+
+ }
+ />
+
+ )}
)}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
index d5e005a63868060..6682799a8038adc 100644
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
+++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx
@@ -6,16 +6,22 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
-import { SelectOption, SwitchOption } from '../../../vis_default_editor/public';
+import type { PaletteRegistry } from '../../../charts/public';
+import { VisEditorOptionsProps } from '../../../visualizations/public';
+import { SelectOption, SwitchOption, PalettePicker } from '../../../vis_default_editor/public';
import { ValidatedDualRange } from '../../../kibana_react/public';
-import { TagCloudVisParams } from '../types';
+import { TagCloudVisParams, TagCloudTypeProps } from '../types';
import { collections } from './collections';
-function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps
) {
+interface TagCloudOptionsProps
+ extends VisEditorOptionsProps,
+ TagCloudTypeProps {}
+
+function TagCloudOptions({ stateParams, setValue, palettes }: TagCloudOptionsProps) {
+ const [palettesRegistry, setPalettesRegistry] = useState(undefined);
const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => {
setValue('minFontSize', Number(minFontSize));
setValue('maxFontSize', Number(maxFontSize));
@@ -24,6 +30,14 @@ function TagCloudOptions({ stateParams, setValue }: VisEditorOptionsProps {
+ const fetchPalettes = async () => {
+ const palettesService = await palettes?.getPalettes();
+ setPalettesRegistry(palettesService);
+ };
+ fetchPalettes();
+ }, [palettes]);
+
return (
+ {palettesRegistry && (
+ {
+ setValue(paramName, value);
+ }}
+ />
+ )}
+
{
- if (!this._visParams.bucket) {
- return;
- }
-
- fireEvent({
- name: 'filterBucket',
- data: {
- data: [
- {
- table: event.meta.data,
- column: 0,
- row: event.meta.rowIndex,
- },
- ],
- },
- });
- });
- this._renderComplete$ = Rx.fromEvent(this._tagCloud, 'renderComplete');
-
- this._feedbackNode = document.createElement('div');
- this._containerNode.appendChild(this._feedbackNode);
- this._feedbackMessage = React.createRef();
- render(
-
-
- ,
- this._feedbackNode
- );
-
- this._labelNode = document.createElement('div');
- this._containerNode.appendChild(this._labelNode);
- this._label = React.createRef();
- render( , this._labelNode);
- }
-
- async render(data, visParams) {
- this._updateParams(visParams);
- this._updateData(data);
- this._resize();
-
- await this._renderComplete$.pipe(take(1)).toPromise();
-
- if (data && data.columns.length !== 2 && this._feedbackMessage.current) {
- this._feedbackMessage.current.setState({
- shouldShowTruncate: false,
- shouldShowIncomplete: false,
- });
- return;
- }
-
- if (data && this._label.current) {
- this._label.current.setState({
- label: `${data.columns[0].name} - ${data.columns[1].name}`,
- shouldShowLabel: visParams.showLabel,
- });
- }
-
- if (this._feedbackMessage.current) {
- this._feedbackMessage.current.setState({
- shouldShowTruncate: this._truncated,
- shouldShowIncomplete: this._tagCloud.getStatus() === TagCloud.STATUS.INCOMPLETE,
- });
- }
- }
-
- destroy() {
- this._tagCloud.destroy();
- unmountComponentAtNode(this._feedbackNode);
- unmountComponentAtNode(this._labelNode);
- }
-
- _updateData(data) {
- if (!data || !data.rows.length) {
- this._tagCloud.setData([]);
- return;
- }
-
- const bucket = this._visParams.bucket;
- const metric = this._visParams.metric;
- const bucketFormatter = bucket ? getFormatService().deserialize(bucket.format) : null;
- const tagColumn = bucket ? data.columns[bucket.accessor].id : -1;
- const metricColumn = data.columns[metric.accessor].id;
- const tags = data.rows.map((row, rowIndex) => {
- const tag = row[tagColumn] === undefined ? 'all' : row[tagColumn];
- const metric = row[metricColumn];
- return {
- displayText: bucketFormatter ? bucketFormatter.convert(tag, 'text') : tag,
- rawText: tag,
- value: metric,
- meta: {
- data: data,
- rowIndex: rowIndex,
- },
- };
- });
-
- if (tags.length > MAX_TAG_COUNT) {
- tags.length = MAX_TAG_COUNT;
- this._truncated = true;
- } else {
- this._truncated = false;
- }
-
- this._tagCloud.setData(tags);
- }
-
- _updateParams(visParams) {
- this._visParams = visParams;
- this._tagCloud.setOptions(visParams);
- }
-
- _resize() {
- this._tagCloud.resize();
- }
-}
diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js
deleted file mode 100644
index 26da8b7e72dd1af..000000000000000
--- a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js
+++ /dev/null
@@ -1,128 +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
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import 'jest-canvas-mock';
-
-import { TagCloudVisualization } from './tag_cloud_visualization';
-import { setFormatService } from '../services';
-import { dataPluginMock } from '../../../data/public/mocks';
-import { setHTMLElementOffset, setSVGElementGetBBox } from '@kbn/test/jest';
-
-const seedColors = ['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d'];
-
-describe('TagCloudVisualizationTest', () => {
- let domNode;
- let visParams;
- let SVGElementGetBBoxSpyInstance;
- let HTMLElementOffsetMockInstance;
-
- const dummyTableGroup = {
- columns: [
- {
- id: 'col-0',
- title: 'geo.dest: Descending',
- },
- {
- id: 'col-1',
- title: 'Count',
- },
- ],
- rows: [
- { 'col-0': 'CN', 'col-1': 26 },
- { 'col-0': 'IN', 'col-1': 17 },
- { 'col-0': 'US', 'col-1': 6 },
- { 'col-0': 'DE', 'col-1': 4 },
- { 'col-0': 'BR', 'col-1': 3 },
- ],
- };
-
- const originTransformSVGElement = window.SVGElement.prototype.transform;
-
- beforeAll(() => {
- setFormatService(dataPluginMock.createStartContract().fieldFormats);
- Object.defineProperties(window.SVGElement.prototype, {
- transform: {
- get: () => ({
- baseVal: {
- consolidate: () => {},
- },
- }),
- configurable: true,
- },
- });
- });
-
- afterAll(() => {
- SVGElementGetBBoxSpyInstance.mockRestore();
- HTMLElementOffsetMockInstance.mockRestore();
- window.SVGElement.prototype.transform = originTransformSVGElement;
- });
-
- describe('TagCloudVisualization - basics', () => {
- beforeEach(async () => {
- setupDOM(512, 512);
-
- visParams = {
- bucket: { accessor: 0, format: {} },
- metric: { accessor: 0, format: {} },
- scale: 'linear',
- orientation: 'single',
- };
- });
-
- test('simple draw', async () => {
- const tagcloudVisualization = new TagCloudVisualization(domNode, {
- seedColors,
- });
-
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- const svgNode = domNode.querySelector('svg');
- expect(svgNode.outerHTML).toMatchSnapshot();
- });
-
- test('with resize', async () => {
- const tagcloudVisualization = new TagCloudVisualization(domNode, {
- seedColors,
- });
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- const svgNode = domNode.querySelector('svg');
- expect(svgNode.outerHTML).toMatchSnapshot();
- });
-
- test('with param change', async function () {
- const tagcloudVisualization = new TagCloudVisualization(domNode, {
- seedColors,
- });
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- SVGElementGetBBoxSpyInstance.mockRestore();
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(256, 368);
-
- HTMLElementOffsetMockInstance.mockRestore();
- HTMLElementOffsetMockInstance = setHTMLElementOffset(256, 386);
-
- visParams.orientation = 'right angled';
- visParams.minFontSize = 70;
- await tagcloudVisualization.render(dummyTableGroup, visParams);
-
- const svgNode = domNode.querySelector('svg');
- expect(svgNode.outerHTML).toMatchSnapshot();
- });
- });
-
- function setupDOM(width, height) {
- domNode = document.createElement('div');
-
- HTMLElementOffsetMockInstance = setHTMLElementOffset(width, height);
- SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(width, height);
- }
-});
diff --git a/src/plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_type_tagcloud/public/plugin.ts
index a48e0726e45fe0b..b2414762f6e4756 100644
--- a/src/plugins/vis_type_tagcloud/public/plugin.ts
+++ b/src/plugins/vis_type_tagcloud/public/plugin.ts
@@ -12,7 +12,7 @@ import { VisualizationsSetup } from '../../visualizations/public';
import { ChartsPluginSetup } from '../../charts/public';
import { createTagCloudFn } from './tag_cloud_fn';
-import { tagCloudVisTypeDefinition } from './tag_cloud_type';
+import { getTagCloudVisTypeDefinition } from './tag_cloud_type';
import { DataPublicPluginStart } from '../../data/public';
import { setFormatService } from './services';
import { ConfigSchema } from '../config';
@@ -27,7 +27,7 @@ export interface TagCloudPluginSetupDependencies {
/** @internal */
export interface TagCloudVisDependencies {
- colors: ChartsPluginSetup['legacyColors'];
+ palettes: ChartsPluginSetup['palettes'];
}
/** @internal */
@@ -48,11 +48,15 @@ export class TagCloudPlugin implements Plugin {
{ expressions, visualizations, charts }: TagCloudPluginSetupDependencies
) {
const visualizationDependencies: TagCloudVisDependencies = {
- colors: charts.legacyColors,
+ palettes: charts.palettes,
};
expressions.registerFunction(createTagCloudFn);
expressions.registerRenderer(getTagCloudVisRenderer(visualizationDependencies));
- visualizations.createBaseVisualization(tagCloudVisTypeDefinition);
+ visualizations.createBaseVisualization(
+ getTagCloudVisTypeDefinition({
+ palettes: charts.palettes,
+ })
+ );
}
public start(core: CoreStart, { data }: TagCloudVisPluginStartDependencies) {
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
index 0b6a224eee7b510..5dcdffffde01d08 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts
@@ -24,6 +24,7 @@ describe('interpreter/functions#tagcloud', () => {
maxFontSize: 72,
showLabel: true,
metric: { accessor: 0, format: { id: 'number' } },
+ bucket: { accessor: 1, format: { id: 'number' } },
};
it('returns an object with the correct structure', () => {
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
index d831ba8c760df3d..17855db9150b5dd 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts
@@ -9,19 +9,19 @@
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
-import { TagCloudVisParams } from './types';
+import { TagCloudVisParams, TagCloudVisConfig } from './types';
const name = 'tagcloud';
-interface Arguments extends TagCloudVisParams {
- metric: any; // these aren't typed yet
- bucket?: any; // these aren't typed yet
+interface Arguments extends TagCloudVisConfig {
+ palette: string;
}
export interface TagCloudVisRenderValue {
visType: typeof name;
visData: Datatable;
- visParams: Arguments;
+ visParams: TagCloudVisParams;
+ syncColors: boolean;
}
export type TagcloudExpressionFunctionDefinition = ExpressionFunctionDefinition<
@@ -70,6 +70,13 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
default: true,
help: '',
},
+ palette: {
+ types: ['string'],
+ help: i18n.translate('visTypeTagCloud.function.paletteHelpText', {
+ defaultMessage: 'Defines the chart palette name',
+ }),
+ default: 'default',
+ },
metric: {
types: ['vis_dimension'],
help: i18n.translate('visTypeTagCloud.function.metric.help', {
@@ -92,11 +99,14 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
maxFontSize: args.maxFontSize,
showLabel: args.showLabel,
metric: args.metric,
- } as Arguments;
-
- if (args.bucket !== undefined) {
- visParams.bucket = args.bucket;
- }
+ ...(args.bucket && {
+ bucket: args.bucket,
+ }),
+ palette: {
+ type: 'palette',
+ name: args.palette,
+ },
+ } as TagCloudVisParams;
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', input);
@@ -108,6 +118,7 @@ export const createTagCloudFn = (): TagcloudExpressionFunctionDefinition => ({
visData: input,
visType: name,
visParams,
+ syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
},
};
},
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
index 960122c178caa13..b3ab5cd3d7af740 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
@@ -10,10 +10,11 @@ import { i18n } from '@kbn/i18n';
import { AggGroupNames } from '../../data/public';
import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
-import { TagCloudOptions } from './components/tag_cloud_options';
+import { getTagCloudOptions } from './components/get_tag_cloud_options';
import { toExpressionAst } from './to_ast';
+import { TagCloudVisDependencies } from './plugin';
-export const tagCloudVisTypeDefinition = {
+export const getTagCloudVisTypeDefinition = ({ palettes }: TagCloudVisDependencies) => ({
name: 'tagcloud',
title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag cloud' }),
icon: 'visTagCloud',
@@ -30,11 +31,17 @@ export const tagCloudVisTypeDefinition = {
minFontSize: 18,
maxFontSize: 72,
showLabel: true,
+ palette: {
+ name: 'default',
+ type: 'palette',
+ },
},
},
toExpressionAst,
editorConfig: {
- optionsTemplate: TagCloudOptions,
+ optionsTemplate: getTagCloudOptions({
+ palettes,
+ }),
schemas: [
{
group: AggGroupNames.Metrics,
@@ -69,4 +76,4 @@ export const tagCloudVisTypeDefinition = {
],
},
requiresSearch: true,
-};
+});
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx
index 3f05c35ab1dbb2f..279bfdfffee6749 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx
@@ -8,6 +8,7 @@
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
+import { I18nProvider } from '@kbn/i18n/react';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
@@ -18,7 +19,7 @@ const TagCloudChart = lazy(() => import('./components/tag_cloud_chart'));
export const getTagCloudVisRenderer: (
deps: TagCloudVisDependencies
-) => ExpressionRenderDefinition = ({ colors }) => ({
+) => ExpressionRenderDefinition = ({ palettes }) => ({
name: 'tagloud_vis',
displayName: 'Tag Cloud visualization',
reuseDomNode: true,
@@ -26,16 +27,20 @@ export const getTagCloudVisRenderer: (
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
+ const palettesRegistry = await palettes.getPalettes();
render(
-
-
- ,
+
+
+
+
+ ,
domNode
);
},
diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.test.ts b/src/plugins/vis_type_tagcloud/public/to_ast.test.ts
index 186f621f583d7fa..4da9c525a4f9334 100644
--- a/src/plugins/vis_type_tagcloud/public/to_ast.test.ts
+++ b/src/plugins/vis_type_tagcloud/public/to_ast.test.ts
@@ -66,6 +66,11 @@ describe('tagcloud vis toExpressionAst function', () => {
minFontSize: 5,
maxFontSize: 15,
showLabel: true,
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ metric: { accessor: 0, format: { id: 'number' } },
};
const actual = toExpressionAst(vis, {} as any);
expect(actual).toMatchSnapshot();
diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.ts b/src/plugins/vis_type_tagcloud/public/to_ast.ts
index 38f2ef9271b3ddc..8a2fb4e84397303 100644
--- a/src/plugins/vis_type_tagcloud/public/to_ast.ts
+++ b/src/plugins/vis_type_tagcloud/public/to_ast.ts
@@ -39,7 +39,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, para
});
const schemas = getVisSchemas(vis, params);
- const { scale, orientation, minFontSize, maxFontSize, showLabel } = vis.params;
+ const { scale, orientation, minFontSize, maxFontSize, showLabel, palette } = vis.params;
const tagcloud = buildExpressionFunction('tagcloud', {
scale,
@@ -48,6 +48,7 @@ export const toExpressionAst: VisToExpressionAst = (vis, para
maxFontSize,
showLabel,
metric: prepareDimension(schemas.metric[0]),
+ palette: palette?.name,
});
if (schemas.segment) {
diff --git a/src/plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_type_tagcloud/public/types.ts
index d1c63d8c634bbc7..710547667069366 100644
--- a/src/plugins/vis_type_tagcloud/public/types.ts
+++ b/src/plugins/vis_type_tagcloud/public/types.ts
@@ -5,11 +5,37 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import type { ChartsPluginSetup, PaletteOutput } from '../../charts/public';
+import type { SerializedFieldFormat } from '../../expressions/public';
+import { ExpressionValueVisDimension } from '../../visualizations/public';
-export interface TagCloudVisParams {
+interface Dimension {
+ accessor: number;
+ format: {
+ id?: string;
+ params?: SerializedFieldFormat;
+ };
+}
+
+interface TagCloudCommonParams {
scale: 'linear' | 'log' | 'square root';
orientation: 'single' | 'right angled' | 'multiple';
minFontSize: number;
maxFontSize: number;
showLabel: boolean;
}
+
+export interface TagCloudVisConfig extends TagCloudCommonParams {
+ metric: ExpressionValueVisDimension;
+ bucket?: ExpressionValueVisDimension;
+}
+
+export interface TagCloudVisParams extends TagCloudCommonParams {
+ palette: PaletteOutput;
+ metric: Dimension;
+ bucket?: Dimension;
+}
+
+export interface TagCloudTypeProps {
+ palettes: ChartsPluginSetup['palettes'];
+}
diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
index edfd05b84dfc8ed..0f549584af6724f 100644
--- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
+++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts
@@ -15,6 +15,7 @@ import {
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
commonMigrateVislibPie,
commonAddEmptyValueColorRule,
+ commonMigrateTagCloud,
} from '../migrations/visualization_common_migrations';
const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => {
@@ -52,6 +53,13 @@ const byValueMigrateVislibPie = (state: SerializableState) => {
};
};
+const byValueMigrateTagcloud = (state: SerializableState) => {
+ return {
+ ...state,
+ savedVis: commonMigrateTagCloud(state.savedVis),
+ };
+};
+
export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
return {
id: 'visualization',
@@ -63,7 +71,8 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
byValueHideTSVBLastValueIndicator,
byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel
)(state),
- '7.14.0': (state) => flow(byValueAddEmptyValueColorRule, byValueMigrateVislibPie)(state),
+ '7.14.0': (state) =>
+ flow(byValueAddEmptyValueColorRule, byValueMigrateVislibPie, byValueMigrateTagcloud)(state),
},
};
};
diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
index f5afeee0ff35eaf..17b1470a40062e0 100644
--- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts
@@ -114,3 +114,25 @@ export const commonMigrateVislibPie = (visState: any) => {
return visState;
};
+
+export const commonMigrateTagCloud = (visState: any) => {
+ if (visState && visState.type === 'tagcloud') {
+ const { params } = visState;
+ const hasPalette = params?.palette;
+
+ return {
+ ...visState,
+ params: {
+ ...visState.params,
+ ...(!hasPalette && {
+ palette: {
+ type: 'palette',
+ name: 'kibana_palette',
+ },
+ }),
+ },
+ };
+ }
+
+ return visState;
+};
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
index 7ee43f36c864e28..7debc9412925e4c 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts
@@ -2162,4 +2162,45 @@ describe('migration visualization', () => {
expect(distinctColors).toBe(true);
});
});
+
+ describe('7.14.0 update tagcloud defaults', () => {
+ const migrate = (doc: any) =>
+ visualizationSavedObjectTypeMigrations['7.14.0'](
+ doc as Parameters[0],
+ savedObjectMigrationContext
+ );
+ const getTestDoc = (hasPalette = false) => ({
+ attributes: {
+ title: 'My Vis',
+ description: 'This is my super cool vis.',
+ visState: JSON.stringify({
+ type: 'tagcloud',
+ title: '[Flights] Delay Type',
+ params: {
+ type: 'tagcloud',
+ ...(hasPalette && {
+ palette: {
+ type: 'palette',
+ name: 'default',
+ },
+ }),
+ },
+ }),
+ },
+ });
+
+ it('should decorate existing docs with the kibana legacy palette if the palette is not defined - pie', () => {
+ const migratedTestDoc = migrate(getTestDoc());
+ const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params;
+
+ expect(palette.name).toEqual('kibana_palette');
+ });
+
+ it('should not overwrite the palette with the legacy one if the palette already exists in the saved object', () => {
+ const migratedTestDoc = migrate(getTestDoc(true));
+ const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params;
+
+ expect(palette.name).toEqual('default');
+ });
+ });
});
diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
index f386d9eb12091ed..7fb54b042593537 100644
--- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
+++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts
@@ -17,6 +17,7 @@ import {
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
commonMigrateVislibPie,
commonAddEmptyValueColorRule,
+ commonMigrateTagCloud,
} from './visualization_common_migrations';
const migrateIndexPattern: SavedObjectMigrationFn = (doc) => {
@@ -1014,6 +1015,29 @@ const migrateVislibPie: SavedObjectMigrationFn = (doc) => {
return doc;
};
+// [Tagcloud] Migrate to the new palette service
+const migrateTagCloud: SavedObjectMigrationFn = (doc) => {
+ const visStateJSON = get(doc, 'attributes.visState');
+ let visState;
+
+ if (visStateJSON) {
+ try {
+ visState = JSON.parse(visStateJSON);
+ } catch (e) {
+ // Let it go, the data is invalid and we'll leave it as is
+ }
+ const newVisState = commonMigrateTagCloud(visState);
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ visState: JSON.stringify(newVisState),
+ },
+ };
+ }
+ return doc;
+};
+
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@@ -1060,5 +1084,5 @@ export const visualizationSavedObjectTypeMigrations = {
hideTSVBLastValueIndicator,
removeDefaultIndexPatternAndTimeFieldFromTSVBModel
),
- '7.14.0': flow(addEmptyValueColorRule, migrateVislibPie),
+ '7.14.0': flow(addEmptyValueColorRule, migrateVislibPie, migrateTagCloud),
};
diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts
index b5a40030b26016a..5d7c16c3c6408e1 100644
--- a/test/functional/apps/discover/_doc_table.ts
+++ b/test/functional/apps/discover/_doc_table.ts
@@ -68,7 +68,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
});
- it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () {
+ it('should load more rows when scrolling down the document table', async function () {
const initialRows = await testSubjects.findAll('docTableRow');
await testSubjects.scrollIntoView('discoverBackToTop');
// now count the rows
@@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.discover.waitUntilSearchingHasFinished();
});
- it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () {
+ it('should load more rows when scrolling down the document table', async function () {
const initialRows = await testSubjects.findAll('docTableRow');
await testSubjects.scrollIntoView('discoverBackToTop');
// now count the rows
diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts
index a6ac324d9dc615b..d18c85f3b58be29 100644
--- a/test/functional/apps/visualize/_tag_cloud.ts
+++ b/test/functional/apps/visualize/_tag_cloud.ts
@@ -63,8 +63,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(data).to.eql([
'32,212,254,720',
'21,474,836,480',
- '20,401,094,656',
'19,327,352,832',
+ '20,401,094,656',
'18,253,611,008',
]);
});
@@ -91,8 +91,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(data).to.eql([
'32,212,254,720',
'21,474,836,480',
- '20,401,094,656',
'19,327,352,832',
+ '20,401,094,656',
'18,253,611,008',
]);
});
@@ -106,8 +106,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(data).to.eql([
'32,212,254,720',
'21,474,836,480',
- '20,401,094,656',
'19,327,352,832',
+ '20,401,094,656',
'18,253,611,008',
]);
});
@@ -122,7 +122,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should show the tags and relative size', function () {
return PageObjects.tagCloud.getTextSizes().then(function (results) {
log.debug('results here ' + results);
- expect(results).to.eql(['72px', '63px', '25px', '32px', '18px']);
+ expect(results).to.eql(['72px', '63px', '32px', '25px', '18px']);
});
});
@@ -177,7 +177,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should format tags with field formatter', async function () {
const data = await PageObjects.tagCloud.getTextTag();
log.debug(data);
- expect(data).to.eql(['30GB', '20GB', '19GB', '18GB', '17GB']);
+ expect(data).to.eql(['30GB', '20GB', '18GB', '19GB', '17GB']);
});
it('should apply filter with unformatted value', async function () {
diff --git a/test/functional/page_objects/tag_cloud_page.ts b/test/functional/page_objects/tag_cloud_page.ts
index 61e844c813df8cb..ce51959390a422b 100644
--- a/test/functional/page_objects/tag_cloud_page.ts
+++ b/test/functional/page_objects/tag_cloud_page.ts
@@ -11,15 +11,24 @@ import { WebElementWrapper } from '../services/lib/web_element_wrapper';
export class TagCloudPageObject extends FtrService {
private readonly find = this.ctx.getService('find');
- private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly header = this.ctx.getPageObject('header');
private readonly visChart = this.ctx.getPageObject('visChart');
public async selectTagCloudTag(tagDisplayText: string) {
- await this.testSubjects.click(tagDisplayText);
+ const elements = await this.find.allByCssSelector('text');
+ const targetElement = elements.find(
+ async (element) => (await element.getVisibleText()) === tagDisplayText
+ );
+ await targetElement?.click();
await this.header.waitUntilLoadingHasFinished();
}
+ public async getTextTagByElement(webElement: WebElementWrapper) {
+ await this.visChart.waitForVisualization();
+ const elements = await webElement.findAllByCssSelector('text');
+ return await Promise.all(elements.map(async (element) => await element.getVisibleText()));
+ }
+
public async getTextTag() {
await this.visChart.waitForVisualization();
const elements = await this.find.allByCssSelector('text');
diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts
index c22eddb032cf9e3..0a689c0091edc5a 100644
--- a/test/functional/services/dashboard/expectations.ts
+++ b/test/functional/services/dashboard/expectations.ts
@@ -16,8 +16,10 @@ export class DashboardExpectService extends FtrService {
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly find = this.ctx.getService('find');
private readonly filterBar = this.ctx.getService('filterBar');
+
private readonly dashboard = this.ctx.getPageObject('dashboard');
private readonly visChart = this.ctx.getPageObject('visChart');
+ private readonly tagCloud = this.ctx.getPageObject('tagCloud');
private readonly findTimeout = 2500;
async panelCount(expectedCount: number) {
@@ -166,8 +168,9 @@ export class DashboardExpectService extends FtrService {
const tagCloudVisualizations = await this.testSubjects.findAll('tagCloudVisualization');
const matches = await Promise.all(
tagCloudVisualizations.map(async (tagCloud) => {
+ const tagCloudData = await this.tagCloud.getTextTagByElement(tagCloud);
for (let i = 0; i < values.length; i++) {
- const valueExists = await this.testSubjects.descendantExists(values[i], tagCloud);
+ const valueExists = tagCloudData.includes(values[i]);
if (!valueExists) {
return false;
}
diff --git a/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js b/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js
index 88a6ad0e3ab15c1..514d1bb1d9d7b22 100644
--- a/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js
+++ b/test/functional/services/lib/web_element_wrapper/scroll_into_view_if_necessary.js
@@ -27,7 +27,7 @@
* SOFTWARE.
*/
-export function scrollIntoViewIfNecessary(target, fixedHeaderHeight) {
+export function scrollIntoViewIfNecessary(target, fixedHeaderHeight, fixedFooterHeight) {
var rootScroller = document.scrollingElement || document.documentElement;
if (!rootScroller) {
throw new Error('Unable to find document.scrollingElement or document.documentElement');
@@ -63,4 +63,11 @@ export function scrollIntoViewIfNecessary(target, fixedHeaderHeight) {
if (additionalScrollNecessary > 0) {
rootScroller.scrollTop = rootScroller.scrollTop - additionalScrollNecessary;
}
+
+ if (fixedFooterHeight) {
+ var bottomOfVisibility = viewportHeight - fixedFooterHeight;
+ if (bottomOfVisibility < boundingRect.bottom) {
+ rootScroller.scrollTop = rootScroller.scrollTop + fixedFooterHeight;
+ }
+ }
}
diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
index 148c21ffac191b8..4b164402bfb70a7 100644
--- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
+++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
@@ -692,11 +692,22 @@ export class WebElementWrapper {
* @nonstandard
* @return {Promise}
*/
- public async scrollIntoViewIfNecessary(topOffset?: number): Promise {
+ public async scrollIntoViewIfNecessary(
+ topOffsetOrOptions?: number | { topOffset?: number; bottomOffset?: number }
+ ): Promise {
+ let topOffset: undefined | number;
+ let bottomOffset: undefined | number;
+ if (typeof topOffsetOrOptions === 'number') {
+ topOffset = topOffsetOrOptions;
+ } else {
+ topOffset = topOffsetOrOptions?.topOffset;
+ bottomOffset = topOffsetOrOptions?.bottomOffset;
+ }
await this.driver.executeScript(
scrollIntoViewIfNecessary,
this._webElement,
- topOffset || this.fixedHeaderHeight
+ topOffset || this.fixedHeaderHeight,
+ bottomOffset
);
}
diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json
index 14c8428c6d432a6..e0b62688d06629b 100644
--- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json
+++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json
index 073fca760b9a249..d85444f5d3b6ba5 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json
index 93f8d8a27d2334e..2c81c9447b826ea 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json
index 0c50947beca975b..687b669b18e614d 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_invalid_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json
index e8c47efdbe622b1..b49953f9a023bcc 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json
index 38683082975f8fc..fc7e289dfbd3a10 100644
--- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json
+++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"default","type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json
index 14c8428c6d432a6..e0b62688d06629b 100644
--- a/test/interpreter_functional/snapshots/session/partial_test_1.json
+++ b/test/interpreter_functional/snapshots/session/partial_test_1.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json
index 073fca760b9a249..d85444f5d3b6ba5 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json
index 93f8d8a27d2334e..2c81c9447b826ea 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json
index 0c50947beca975b..687b669b18e614d 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_invalid_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[],"meta":{},"rows":[],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json
index e8c47efdbe622b1..b49953f9a023bcc 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","palette":{"name":"default","type":"palette"},"scale":"linear","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json
index 38683082975f8fc..fc7e289dfbd3a10 100644
--- a/test/interpreter_functional/snapshots/session/tagcloud_options.json
+++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json
@@ -1 +1 @@
-{"as":"tagloud_vis","type":"render","value":{"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
+{"as":"tagloud_vis","type":"render","value":{"syncColors":false,"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visParams":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","palette":{"name":"default","type":"palette"},"scale":"log","showLabel":true},"visType":"tagcloud"}}
\ No newline at end of file
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index bfc0a3daf6f0edf..77e7f2834b080de 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -107,7 +107,7 @@ export class ApmPlugin implements Plugin {
// APM navigation
{
label: 'APM',
- sortKey: 200,
+ sortKey: 400,
entries: [
{ label: servicesTitle, app: 'apm', path: '/services' },
{ label: tracesTitle, app: 'apm', path: '/traces' },
@@ -118,7 +118,7 @@ export class ApmPlugin implements Plugin {
// UX navigation
{
label: 'User Experience',
- sortKey: 201,
+ sortKey: 600,
entries: [
{
label: i18n.translate('xpack.apm.ux.overview.heading', {
diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
index 79eb0fbce54987b..b54e9726667e6c7 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap
@@ -58,6 +58,11 @@ Object {
"transaction.type": "page-load",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
@@ -492,6 +497,11 @@ Object {
"field": "transaction.marks.navigationTiming.fetchStart",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
@@ -534,6 +544,11 @@ Object {
"transaction.type": "page-load",
},
},
+ Object {
+ "term": Object {
+ "service.environment": "staging",
+ },
+ },
],
},
},
diff --git a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
index aaf6401b9f407ab..452f451b11f8635 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/queries.test.ts
@@ -16,6 +16,7 @@ import { getRumServices } from './get_rum_services';
import { getLongTaskMetrics } from './get_long_task_metrics';
import { getWebCoreVitals } from './get_web_core_vitals';
import { getJSErrors } from './get_js_errors';
+import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
describe('rum client dashboard queries', () => {
let mock: SearchParamsMock;
@@ -25,32 +26,38 @@ describe('rum client dashboard queries', () => {
});
it('fetches client metrics', async () => {
- mock = await inspectSearchParams((setup) =>
- getClientMetrics({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getClientMetrics({
+ setup,
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
it('fetches page view trends', async () => {
- mock = await inspectSearchParams((setup) =>
- getPageViewTrends({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getPageViewTrends({
+ setup,
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
it('fetches page load distribution', async () => {
- mock = await inspectSearchParams((setup) =>
- getPageLoadDistribution({
- setup,
- minPercentile: '0',
- maxPercentile: '99',
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getPageLoadDistribution({
+ setup,
+ minPercentile: '0',
+ maxPercentile: '99',
+ }),
+ { uiFilters: { environment: 'staging' } }
);
expect(mock.params).toMatchSnapshot();
});
@@ -65,10 +72,12 @@ describe('rum client dashboard queries', () => {
});
it('fetches rum core vitals', async () => {
- mock = await inspectSearchParams((setup) =>
- getWebCoreVitals({
- setup,
- })
+ mock = await inspectSearchParams(
+ (setup) =>
+ getWebCoreVitals({
+ setup,
+ }),
+ { uiFilters: { environment: ENVIRONMENT_ALL.value } }
);
expect(mock.params).toMatchSnapshot();
});
diff --git a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
index 43cbb485c4510f2..6b8cee244a19269 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/get_es_filter.ts
@@ -8,6 +8,7 @@
import { ESFilter } from '../../../../../../../typings/elasticsearch';
import { UIFilters } from '../../../../typings/ui_filters';
import { localUIFilters, localUIFilterNames } from './local_ui_filters/config';
+import { environmentQuery } from '../../../utils/queries';
export function getEsFilter(uiFilters: UIFilters) {
const localFilterValues = uiFilters;
@@ -23,5 +24,5 @@ export function getEsFilter(uiFilters: UIFilters) {
};
}) as ESFilter[];
- return mappedFilters;
+ return [...mappedFilters, ...environmentQuery(uiFilters.environment)];
}
diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx
index 9c63f0140bdf2bc..8e1d971c9cd2e71 100644
--- a/x-pack/plugins/apm/server/utils/test_helpers.tsx
+++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx
@@ -17,6 +17,7 @@ interface Options {
mockResponse?: (
request: ESSearchRequest
) => ESSearchResponse;
+ uiFilters?: Record;
}
interface MockSetup {
@@ -86,7 +87,7 @@ export async function inspectSearchParams(
},
}
) as APMConfig,
- uiFilters: {},
+ uiFilters: options?.uiFilters ?? {},
indices: {
/* eslint-disable @typescript-eslint/naming-convention */
'apm_oss.sourcemapIndices': 'myIndex',
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx
index 0e5be4b5a51e9f4..c398c92abf3a90a 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx
@@ -5,25 +5,15 @@
* 2.0.
*/
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiHorizontalRule,
- EuiPageBody,
- EuiPageContent,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, HttpStart } from 'kibana/public';
import React from 'react';
import type { SessionsConfigSchema } from '../';
+import { IManagementSectionsPluginsSetup } from '../';
import type { SearchSessionsMgmtAPI } from '../lib/api';
import type { AsyncSearchIntroDocumentation } from '../lib/documentation';
-import { TableText } from './';
import { SearchSessionsMgmtTable } from './table';
-import { IManagementSectionsPluginsSetup } from '../';
interface Props {
documentation: AsyncSearchIntroDocumentation;
@@ -37,46 +27,37 @@ interface Props {
export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+ }
+ description={
+
+ }
+ bottomBorder
+ rightSideItems={[
+
-
-
-
-
+ ,
+ ]}
+ />
-
-
-
+
+
+ >
);
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.test.tsx
new file mode 100644
index 000000000000000..8ad7b10685488c0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.test.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { FormattedRelative } from '@kbn/i18n/react';
+
+import { FormattedDateTime } from '../../../utils/formatted_date_time';
+
+import { CustomFormattedTimestamp } from './custom_formatted_timestamp';
+
+describe('CustomFormattedTimestamp', () => {
+ const mockToday = jest
+ .spyOn(global.Date, 'now')
+ .mockImplementation(() => new Date('1970-01-02').valueOf());
+
+ afterAll(() => mockToday.mockRestore());
+
+ it('uses a relative time format (x minutes ago) if the timestamp is from today', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(FormattedRelative).prop('value')).toEqual(new Date('1970-01-02'));
+ });
+
+ it('uses a date if the timestamp is before today', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(FormattedDateTime).prop('date')).toEqual(new Date('1970-01-01'));
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.tsx
new file mode 100644
index 000000000000000..da6d1ac9f2cb1d3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/custom_formatted_timestamp.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { FormattedRelative } from '@kbn/i18n/react';
+
+import { FormattedDateTime } from '../../../utils/formatted_date_time';
+
+interface CustomFormattedTimestampProps {
+ timestamp: string;
+}
+
+export const CustomFormattedTimestamp: React.FC = ({
+ timestamp,
+}) => {
+ const date = new Date(timestamp);
+ const isDateToday = date >= new Date(new Date(Date.now()).toDateString());
+ return isDateToday ? (
+
+ ) : (
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx
new file mode 100644
index 000000000000000..aab2909d630ede6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.test.tsx
@@ -0,0 +1,154 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { mockKibanaValues, setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
+import '../../../__mocks__/engine_logic.mock';
+
+import React from 'react';
+
+import { shallow, ShallowWrapper } from 'enzyme';
+
+import { EuiBasicTable, EuiButtonIcon, EuiInMemoryTable } from '@elastic/eui';
+
+import { mountWithIntl } from '../../../../test_helpers';
+
+import { CrawlerDomain } from '../types';
+
+import { DomainsTable } from './domains_table';
+
+const domains: CrawlerDomain[] = [
+ {
+ id: '1234',
+ documentCount: 9999,
+ url: 'elastic.co',
+ crawlRules: [],
+ entryPoints: [],
+ sitemaps: [],
+ lastCrawl: '2020-01-01T00:00:00-05:00',
+ createdOn: '2020-01-01T00:00:00-05:00',
+ },
+];
+
+const values = {
+ // EngineLogic
+ engineName: 'some-engine',
+ // CrawlerOverviewLogic
+ domains,
+ // AppLogic
+ myRole: { canManageEngineCrawler: false },
+};
+
+const actions = {
+ // CrawlerOverviewLogic
+ deleteDomain: jest.fn(),
+};
+
+describe('DomainsTable', () => {
+ let wrapper: ShallowWrapper;
+ let tableContent: string;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+ beforeAll(() => {
+ setMockValues(values);
+ setMockActions(actions);
+ wrapper = shallow( );
+ tableContent = mountWithIntl( )
+ .find(EuiInMemoryTable)
+ .text();
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(EuiInMemoryTable)).toHaveLength(1);
+ });
+
+ describe('columns', () => {
+ const getTable = () => wrapper.find(EuiInMemoryTable).dive().find(EuiBasicTable).dive();
+
+ beforeEach(() => {
+ wrapper = shallow( );
+ tableContent = mountWithIntl( )
+ .find(EuiInMemoryTable)
+ .text();
+ });
+
+ it('renders a url column', () => {
+ expect(tableContent).toContain('elastic.co');
+ });
+
+ it('renders a last crawled column', () => {
+ expect(tableContent).toContain('Last activity');
+ expect(tableContent).toContain('Jan 1, 2020');
+ });
+
+ it('renders a document count column', () => {
+ expect(tableContent).toContain('Documents');
+ expect(tableContent).toContain('9,999');
+ });
+
+ describe('actions column', () => {
+ const getActions = () => getTable().find('ExpandedItemActions');
+ const getActionItems = () => getActions().dive().find('DefaultItemAction');
+
+ it('will hide the action buttons if the user cannot manage/delete engines', () => {
+ setMockValues({
+ ...values,
+ // AppLogic
+ myRole: { canManageEngineCrawler: false },
+ });
+ wrapper = shallow( );
+
+ expect(getActions()).toHaveLength(0);
+ });
+
+ describe('when the user can manage/delete engines', () => {
+ const getManageAction = () => getActionItems().at(0).dive().find(EuiButtonIcon);
+ const getDeleteAction = () => getActionItems().at(1).dive().find(EuiButtonIcon);
+
+ beforeEach(() => {
+ setMockValues({
+ ...values,
+ // AppLogic
+ myRole: { canManageEngineCrawler: true },
+ });
+ wrapper = shallow( );
+ });
+
+ describe('manage action', () => {
+ it('sends the user to the engine overview on click', () => {
+ const { navigateToUrl } = mockKibanaValues;
+
+ getManageAction().simulate('click');
+
+ expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/crawler/domains/1234');
+ });
+ });
+
+ describe('delete action', () => {
+ it('clicking the action and confirming deletes the domain', () => {
+ jest.spyOn(global, 'confirm').mockReturnValueOnce(true);
+
+ getDeleteAction().simulate('click');
+
+ expect(actions.deleteDomain).toHaveBeenCalledWith(
+ expect.objectContaining({ id: '1234' })
+ );
+ });
+
+ it('clicking the action and not confirming does not delete the engine', () => {
+ jest.spyOn(global, 'confirm').mockReturnValueOnce(false);
+
+ getDeleteAction().simulate('click');
+
+ expect(actions.deleteDomain).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx
new file mode 100644
index 000000000000000..0da3b8462cfcf8b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/domains_table.tsx
@@ -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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { useActions, useValues } from 'kea';
+
+import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { FormattedNumber } from '@kbn/i18n/react';
+
+import { DELETE_BUTTON_LABEL, MANAGE_BUTTON_LABEL } from '../../../../shared/constants';
+import { KibanaLogic } from '../../../../shared/kibana';
+import { AppLogic } from '../../../app_logic';
+import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../routes';
+import { generateEnginePath } from '../../engine';
+import { CrawlerOverviewLogic } from '../crawler_overview_logic';
+import { CrawlerDomain } from '../types';
+
+import { CustomFormattedTimestamp } from './custom_formatted_timestamp';
+
+export const DomainsTable: React.FC = () => {
+ const { domains } = useValues(CrawlerOverviewLogic);
+
+ const { deleteDomain } = useActions(CrawlerOverviewLogic);
+
+ const {
+ myRole: { canManageEngineCrawler },
+ } = useValues(AppLogic);
+
+ const columns: Array> = [
+ {
+ field: 'url',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.domainURL',
+ {
+ defaultMessage: 'Domain URL',
+ }
+ ),
+ },
+ {
+ field: 'lastCrawl',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.lastActivity',
+ {
+ defaultMessage: 'Last activity',
+ }
+ ),
+ render: (lastCrawl: CrawlerDomain['lastCrawl']) =>
+ lastCrawl ? : '',
+ },
+ {
+ field: 'documentCount',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.documents',
+ {
+ defaultMessage: 'Documents',
+ }
+ ),
+ render: (documentCount: CrawlerDomain['documentCount']) => (
+
+ ),
+ },
+ ];
+
+ const actionsColumn: EuiBasicTableColumn = {
+ name: i18n.translate('xpack.enterpriseSearch.appSearch.crawler.domainsTable.column.actions', {
+ defaultMessage: 'Actions',
+ }),
+ actions: [
+ {
+ name: MANAGE_BUTTON_LABEL,
+ description: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.manage.buttonLabel',
+ {
+ defaultMessage: 'Manage this domain',
+ }
+ ),
+ type: 'icon',
+ icon: 'eye',
+ onClick: (domain) =>
+ KibanaLogic.values.navigateToUrl(
+ generateEnginePath(ENGINE_CRAWLER_DOMAIN_PATH, { domainId: domain.id })
+ ),
+ },
+ {
+ name: DELETE_BUTTON_LABEL,
+ description: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.buttonLabel',
+ {
+ defaultMessage: 'Delete this domain',
+ }
+ ),
+ type: 'icon',
+ icon: 'trash',
+ color: 'danger',
+ onClick: (domain) => {
+ if (
+ window.confirm(
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.confirmationPopupMessage',
+ {
+ defaultMessage:
+ 'Are you sure you want to remove the domain "{domainUrl}" and all of its settings?',
+ values: {
+ domainUrl: domain.url,
+ },
+ }
+ )
+ )
+ ) {
+ deleteDomain(domain);
+ }
+ },
+ },
+ ],
+ };
+
+ if (canManageEngineCrawler) {
+ columns.push(actionsColumn);
+ }
+
+ return ;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
index cc3fa21f7330971..affc2fd08e34c8a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
@@ -12,53 +12,46 @@ import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
-import { EuiCode } from '@elastic/eui';
-
import { Loading } from '../../../shared/loading';
import { rerender } from '../../../test_helpers';
+import { DomainsTable } from './components/domains_table';
import { CrawlerOverview } from './crawler_overview';
-const actions = {
- fetchCrawlerData: jest.fn(),
-};
+describe('CrawlerOverview', () => {
+ const mockActions = {
+ fetchCrawlerData: jest.fn(),
+ };
-const values = {
- dataLoading: false,
- domains: [],
-};
+ const mockValues = {
+ dataLoading: false,
+ domains: [],
+ };
-describe('CrawlerOverview', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
jest.clearAllMocks();
- setMockValues(values);
- setMockActions(actions);
+ setMockValues(mockValues);
+ setMockActions(mockActions);
wrapper = shallow( );
});
- it('renders', () => {
- expect(wrapper.find(EuiCode)).toHaveLength(1);
- });
-
it('calls fetchCrawlerData on page load', () => {
- expect(actions.fetchCrawlerData).toHaveBeenCalledTimes(1);
+ expect(mockActions.fetchCrawlerData).toHaveBeenCalledTimes(1);
});
- // TODO after DomainsTable is built in a future PR
- // it('contains a DomainsTable', () => {})
+ it('renders', () => {
+ expect(wrapper.find(DomainsTable)).toHaveLength(1);
- // TODO after CrawlRequestsTable is built in a future PR
- // it('containss a CrawlRequestsTable,() => {})
+ // TODO test for CrawlRequestsTable after it is built in a future PR
- // TODO after AddDomainForm is built in a future PR
- // it('contains an AddDomainForm' () => {})
+ // TODO test for AddDomainForm after it is built in a future PR
- // TODO after empty state is added in a future PR
- // it('has an empty state', () => {} )
+ // TODO test for empty state after it is built in a future PR
+ });
- it('shows an empty state when data is loading', () => {
+ it('shows a loading state when data is loading', () => {
setMockValues({ dataLoading: true });
rerender(wrapper);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
index 5eeaaaef696058c..14906378692ed9e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
@@ -9,17 +9,17 @@ import React, { useEffect } from 'react';
import { useActions, useValues } from 'kea';
-import { EuiCode, EuiPageHeader } from '@elastic/eui';
+import { EuiPageHeader } from '@elastic/eui';
import { FlashMessages } from '../../../shared/flash_messages';
-
import { Loading } from '../../../shared/loading';
+import { DomainsTable } from './components/domains_table';
import { CRAWLER_TITLE } from './constants';
import { CrawlerOverviewLogic } from './crawler_overview_logic';
export const CrawlerOverview: React.FC = () => {
- const { dataLoading, domains } = useValues(CrawlerOverviewLogic);
+ const { dataLoading } = useValues(CrawlerOverviewLogic);
const { fetchCrawlerData } = useActions(CrawlerOverviewLogic);
@@ -35,7 +35,7 @@ export const CrawlerOverview: React.FC = () => {
<>
- {JSON.stringify(domains, null, 2)}
+
>
);
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
index f23322337766a16..7ef5984960e2657 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
@@ -15,7 +15,14 @@ import '../../__mocks__/engine_logic.mock';
import { nextTick } from '@kbn/test/jest';
import { CrawlerOverviewLogic } from './crawler_overview_logic';
-import { CrawlerPolicies, CrawlerRules, CrawlRule } from './types';
+import {
+ CrawlerDataFromServer,
+ CrawlerDomain,
+ CrawlerPolicies,
+ CrawlerRules,
+ CrawlRule,
+} from './types';
+import { crawlerDataServerToClient } from './utils';
const DEFAULT_VALUES = {
dataLoading: true,
@@ -29,10 +36,26 @@ const DEFAULT_CRAWL_RULE: CrawlRule = {
pattern: '.*',
};
+const MOCK_SERVER_DATA: CrawlerDataFromServer = {
+ domains: [
+ {
+ id: '507f1f77bcf86cd799439011',
+ name: 'elastic.co',
+ created_on: 'Mon, 31 Aug 2020 17:00:00 +0000',
+ document_count: 13,
+ sitemaps: [],
+ entry_points: [],
+ crawl_rules: [],
+ },
+ ],
+};
+
+const MOCK_CLIENT_DATA = crawlerDataServerToClient(MOCK_SERVER_DATA);
+
describe('CrawlerOverviewLogic', () => {
const { mount } = new LogicMounter(CrawlerOverviewLogic);
const { http } = mockHttpValues;
- const { flashAPIErrors } = mockFlashMessageHelpers;
+ const { flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
beforeEach(() => {
jest.clearAllMocks();
@@ -44,13 +67,13 @@ describe('CrawlerOverviewLogic', () => {
});
describe('actions', () => {
- describe('onFetchCrawlerData', () => {
+ describe('onReceiveCrawlerData', () => {
const crawlerData = {
domains: [
{
id: '507f1f77bcf86cd799439011',
createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000',
- url: 'moviedatabase.com',
+ url: 'elastic.co',
documentCount: 13,
sitemaps: [],
entryPoints: [],
@@ -61,7 +84,7 @@ describe('CrawlerOverviewLogic', () => {
};
beforeEach(() => {
- CrawlerOverviewLogic.actions.onFetchCrawlerData(crawlerData);
+ CrawlerOverviewLogic.actions.onReceiveCrawlerData(crawlerData);
});
it('should set all received data as top-level values', () => {
@@ -76,41 +99,17 @@ describe('CrawlerOverviewLogic', () => {
describe('listeners', () => {
describe('fetchCrawlerData', () => {
- it('calls onFetchCrawlerData with retrieved data that has been converted from server to client', async () => {
- jest.spyOn(CrawlerOverviewLogic.actions, 'onFetchCrawlerData');
-
- http.get.mockReturnValue(
- Promise.resolve({
- domains: [
- {
- id: '507f1f77bcf86cd799439011',
- name: 'moviedatabase.com',
- created_on: 'Mon, 31 Aug 2020 17:00:00 +0000',
- document_count: 13,
- sitemaps: [],
- entry_points: [],
- crawl_rules: [],
- },
- ],
- })
- );
+ it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData');
+
+ http.get.mockReturnValue(Promise.resolve(MOCK_SERVER_DATA));
CrawlerOverviewLogic.actions.fetchCrawlerData();
await nextTick();
expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/crawler');
- expect(CrawlerOverviewLogic.actions.onFetchCrawlerData).toHaveBeenCalledWith({
- domains: [
- {
- id: '507f1f77bcf86cd799439011',
- createdOn: 'Mon, 31 Aug 2020 17:00:00 +0000',
- url: 'moviedatabase.com',
- documentCount: 13,
- sitemaps: [],
- entryPoints: [],
- crawlRules: [],
- },
- ],
- });
+ expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith(
+ MOCK_CLIENT_DATA
+ );
});
it('calls flashApiErrors when there is an error', async () => {
@@ -121,5 +120,34 @@ describe('CrawlerOverviewLogic', () => {
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
});
+
+ describe('deleteDomain', () => {
+ it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData');
+
+ http.delete.mockReturnValue(Promise.resolve(MOCK_SERVER_DATA));
+ CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
+ await nextTick();
+
+ expect(http.delete).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/crawler/domains/1234',
+ {
+ query: { respond_with: 'crawler_details' },
+ }
+ );
+ expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith(
+ MOCK_CLIENT_DATA
+ );
+ expect(setSuccessMessage).toHaveBeenCalled();
+ });
+
+ it('calls flashApiErrors when there is an error', async () => {
+ http.delete.mockReturnValue(Promise.reject('error'));
+ CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
index 6f04ade5962ebbe..dceb4e205487d83 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
@@ -7,22 +7,36 @@
import { kea, MakeLogicType } from 'kea';
-import { flashAPIErrors } from '../../../shared/flash_messages';
+import { i18n } from '@kbn/i18n';
+
+import { flashAPIErrors, setSuccessMessage } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
import { EngineLogic } from '../engine';
-import { CrawlerData, CrawlerDataFromServer, CrawlerDomain } from './types';
+import { CrawlerData, CrawlerDomain } from './types';
import { crawlerDataServerToClient } from './utils';
+export const DELETE_DOMAIN_MESSAGE = (domainUrl: string) =>
+ i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.successMessage',
+ {
+ defaultMessage: 'Successfully deleted "{domainUrl}"',
+ values: {
+ domainUrl,
+ },
+ }
+ );
+
interface CrawlerOverviewValues {
dataLoading: boolean;
domains: CrawlerDomain[];
}
interface CrawlerOverviewActions {
+ deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain };
fetchCrawlerData(): void;
- onFetchCrawlerData(data: CrawlerData): { data: CrawlerData };
+ onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData };
}
export const CrawlerOverviewLogic = kea<
@@ -30,20 +44,21 @@ export const CrawlerOverviewLogic = kea<
>({
path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'],
actions: {
+ deleteDomain: (domain) => ({ domain }),
fetchCrawlerData: true,
- onFetchCrawlerData: (data) => ({ data }),
+ onReceiveCrawlerData: (data) => ({ data }),
},
reducers: {
dataLoading: [
true,
{
- onFetchCrawlerData: () => false,
+ onReceiveCrawlerData: () => false,
},
],
domains: [
[],
{
- onFetchCrawlerData: (_, { data: { domains } }) => domains,
+ onReceiveCrawlerData: (_, { data: { domains } }) => domains,
},
],
},
@@ -54,8 +69,28 @@ export const CrawlerOverviewLogic = kea<
try {
const response = await http.get(`/api/app_search/engines/${engineName}/crawler`);
- const crawlerData = crawlerDataServerToClient(response as CrawlerDataFromServer);
- actions.onFetchCrawlerData(crawlerData);
+ const crawlerData = crawlerDataServerToClient(response);
+ actions.onReceiveCrawlerData(crawlerData);
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ deleteDomain: async ({ domain }) => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ const response = await http.delete(
+ `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}`,
+ {
+ query: {
+ respond_with: 'crawler_details',
+ },
+ }
+ );
+ const crawlerData = crawlerDataServerToClient(response);
+ actions.onReceiveCrawlerData(crawlerData);
+ setSuccessMessage(DELETE_DOMAIN_MESSAGE(domain.url));
} catch (e) {
flashAPIErrors(e);
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
index 87e6ee62460faea..870e303a2930d8b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts
@@ -16,18 +16,13 @@ import { engines } from '../../__mocks__/engines.mock';
import { nextTick } from '@kbn/test/jest';
import { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
- const {
- clearFlashMessages,
- flashAPIErrors,
- setSuccessMessage,
- setErrorMessage,
- } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors, setSuccessMessage } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(RoleMappingsLogic);
const DEFAULT_VALUES = {
attributes: [],
@@ -50,15 +45,14 @@ describe('RoleMappingsLogic', () => {
roleMappingErrors: [],
};
- const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [asRoleMapping] };
- const mappingServerProps = {
+ const mappingsServerProps = {
+ multipleAuthProvidersConfig: true,
+ roleMappings: [asRoleMapping],
attributes: ['email', 'metadata', 'username', 'role'],
authProviders: [ANY_AUTH_PROVIDER],
availableEngines: engines,
elasticsearchRoles: [],
hasAdvancedRoles: false,
- multipleAuthProvidersConfig: false,
- roleMapping: asRoleMapping,
};
beforeEach(() => {
@@ -75,48 +69,20 @@ describe('RoleMappingsLogic', () => {
it('sets data based on server response from the `mappings` (plural) endpoint', () => {
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- expect(RoleMappingsLogic.values.roleMappings).toEqual([asRoleMapping]);
- expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
- expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true);
- });
- });
-
- describe('setRoleMappingData', () => {
- it('sets state based on server response from the `mapping` (singular) endpoint', () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
-
expect(RoleMappingsLogic.values).toEqual({
...DEFAULT_VALUES,
- roleMapping: asRoleMapping,
+ roleMappings: [asRoleMapping],
dataLoading: false,
- attributes: mappingServerProps.attributes,
- availableAuthProviders: mappingServerProps.authProviders,
- availableEngines: mappingServerProps.availableEngines,
+ attributes: mappingsServerProps.attributes,
+ availableAuthProviders: mappingsServerProps.authProviders,
+ availableEngines: mappingsServerProps.availableEngines,
accessAllEngines: true,
- attributeName: 'role',
- attributeValue: 'superuser',
- elasticsearchRoles: mappingServerProps.elasticsearchRoles,
- selectedEngines: new Set(engines.map((e) => e.name)),
- selectedOptions: [
- { label: engines[0].name, value: engines[0].name },
- { label: engines[1].name, value: engines[1].name },
- ],
- });
- });
-
- it('will remove all selected engines if no roleMapping was returned from the server', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: undefined,
- });
-
- expect(RoleMappingsLogic.values).toEqual({
- ...DEFAULT_VALUES,
- dataLoading: false,
+ multipleAuthProvidersConfig: true,
+ attributeName: 'username',
+ attributeValue: '',
+ elasticsearchRoles: mappingsServerProps.elasticsearchRoles,
selectedEngines: new Set(),
- attributes: mappingServerProps.attributes,
- availableAuthProviders: mappingServerProps.authProviders,
- availableEngines: mappingServerProps.availableEngines,
+ selectedOptions: [],
});
});
});
@@ -135,11 +101,13 @@ describe('RoleMappingsLogic', () => {
const engine = engines[0];
const otherEngine = engines[1];
const mountedValues = {
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- engines: [engine, otherEngine],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ engines: [engine, otherEngine],
+ },
+ ],
selectedEngines: new Set([engine.name]),
};
@@ -153,11 +121,18 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.selectedEngines).toEqual(
new Set([engine.name, otherEngine.name])
);
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: engine.name, value: engine.name },
+ { label: otherEngine.name, value: otherEngine.name },
+ ]);
});
it('handles removing an engine from selected engines', () => {
RoleMappingsLogic.actions.handleEngineSelectionChange([engine.name]);
expect(RoleMappingsLogic.values.selectedEngines).toEqual(new Set([engine.name]));
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: engine.name, value: engine.name },
+ ]);
});
});
@@ -175,17 +150,19 @@ describe('RoleMappingsLogic', () => {
it('sets values correctly', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
elasticsearchRoles,
});
RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]);
expect(RoleMappingsLogic.values).toEqual({
...DEFAULT_VALUES,
+ multipleAuthProvidersConfig: true,
attributeValue: elasticsearchRoles[0],
- roleMapping: asRoleMapping,
- attributes: mappingServerProps.attributes,
- availableEngines: mappingServerProps.availableEngines,
+ roleMappings: [asRoleMapping],
+ roleMapping: null,
+ attributes: mappingsServerProps.attributes,
+ availableEngines: mappingsServerProps.availableEngines,
accessAllEngines: true,
attributeName: 'role',
elasticsearchRoles,
@@ -215,11 +192,13 @@ describe('RoleMappingsLogic', () => {
describe('handleAuthProviderChange', () => {
beforeEach(() => {
mount({
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- authProvider: ['foo'],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ authProvider: ['foo'],
+ },
+ ],
});
});
const providers = ['bar', 'baz'];
@@ -244,11 +223,13 @@ describe('RoleMappingsLogic', () => {
it('handles "any" auth in previous state', () => {
mount({
- ...mappingServerProps,
- roleMapping: {
- ...asRoleMapping,
- authProvider: [ANY_AUTH_PROVIDER],
- },
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...asRoleMapping,
+ authProvider: [ANY_AUTH_PROVIDER],
+ },
+ ],
});
RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
@@ -258,7 +239,6 @@ describe('RoleMappingsLogic', () => {
it('resetState', () => {
mount(mappingsServerProps);
- mount(mappingServerProps);
RoleMappingsLogic.actions.resetState();
expect(RoleMappingsLogic.values).toEqual(DEFAULT_VALUES);
@@ -266,7 +246,7 @@ describe('RoleMappingsLogic', () => {
});
it('openRoleMappingFlyout', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
RoleMappingsLogic.actions.openRoleMappingFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
@@ -275,7 +255,7 @@ describe('RoleMappingsLogic', () => {
it('closeRoleMappingFlyout', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
RoleMappingsLogic.actions.closeRoleMappingFlyout();
@@ -307,40 +287,20 @@ describe('RoleMappingsLogic', () => {
});
describe('initializeRoleMapping', () => {
- it('calls API and sets values for new mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping();
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/new');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('calls API and sets values for existing mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping('123');
-
- expect(http.get).toHaveBeenCalledWith('/api/app_search/role_mappings/123');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('handles error', async () => {
- http.get.mockReturnValue(Promise.reject('this is an error'));
- RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
+ it('sets values for existing mapping', () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeRoleMapping(asRoleMapping.id);
- expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ expect(setRoleMappingDataSpy).toHaveBeenCalledWith(asRoleMapping);
});
- it('shows error when there is a 404 status', async () => {
- http.get.mockReturnValue(Promise.reject({ status: 404 }));
+ it('does not set data for new mapping', async () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
- expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
+ expect(setRoleMappingDataSpy).not.toHaveBeenCalledWith(asRoleMapping);
});
});
@@ -362,7 +322,7 @@ describe('RoleMappingsLogic', () => {
'initializeRoleMappings'
);
- http.post.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.post).toHaveBeenCalledWith('/api/app_search/role_mappings', {
@@ -374,13 +334,16 @@ describe('RoleMappingsLogic', () => {
});
it('calls API and refreshes list when existing mapping', async () => {
- mount(mappingServerProps);
+ mount({
+ ...mappingsServerProps,
+ roleMapping: asRoleMapping,
+ });
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, {
@@ -396,12 +359,13 @@ describe('RoleMappingsLogic', () => {
const engine = engines[0];
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
accessAllEngines: false,
selectedEngines: new Set([engine.name]),
+ roleMapping: asRoleMapping,
});
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(`/api/app_search/role_mappings/${asRoleMapping.id}`, {
@@ -449,7 +413,7 @@ describe('RoleMappingsLogic', () => {
});
it('calls API and refreshes list', async () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
const initializeRoleMappingsSpy = jest.spyOn(
RoleMappingsLogic.actions,
'initializeRoleMappings'
@@ -465,7 +429,7 @@ describe('RoleMappingsLogic', () => {
});
it('handles error', async () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
http.delete.mockReturnValue(Promise.reject('this is an error'));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
await nextTick();
@@ -474,7 +438,7 @@ describe('RoleMappingsLogic', () => {
});
it('will do nothing if not confirmed', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
jest.spyOn(window, 'confirm').mockReturnValueOnce(false);
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
index 863e6746dbe958f..fc0a235b23c77cb 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.ts
@@ -13,10 +13,9 @@ import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
- setErrorMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { AttributeName } from '../../../shared/types';
import { ASRoleMapping, RoleTypes } from '../../types';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
@@ -31,17 +30,12 @@ import {
interface RoleMappingsServerDetails {
roleMappings: ASRoleMapping[];
- multipleAuthProvidersConfig: boolean;
-}
-
-interface RoleMappingServerDetails {
attributes: string[];
authProviders: string[];
availableEngines: Engine[];
elasticsearchRoles: string[];
hasAdvancedRoles: boolean;
multipleAuthProvidersConfig: boolean;
- roleMapping?: ASRoleMapping;
}
const getFirstAttributeName = (roleMapping: ASRoleMapping) =>
@@ -64,7 +58,7 @@ interface RoleMappingsActions {
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
initializeRoleMappings(): void;
resetState(): void;
- setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
+ setRoleMapping(roleMapping: ASRoleMapping): { roleMapping: ASRoleMapping };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
@@ -96,7 +90,7 @@ export const RoleMappingsLogic = kea data,
- setRoleMappingData: (data: RoleMappingServerDetails) => data,
+ setRoleMapping: (roleMapping: ASRoleMapping) => ({ roleMapping }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string) => ({ value }),
handleRoleChange: (roleType: RoleTypes) => ({ roleType }),
@@ -120,7 +114,6 @@ export const RoleMappingsLogic = kea false,
- setRoleMappingData: () => false,
resetState: () => true,
},
],
@@ -135,40 +128,39 @@ export const RoleMappingsLogic = kea multipleAuthProvidersConfig,
- setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig,
resetState: () => false,
},
],
hasAdvancedRoles: [
false,
{
- setRoleMappingData: (_, { hasAdvancedRoles }) => hasAdvancedRoles,
+ setRoleMappingsData: (_, { hasAdvancedRoles }) => hasAdvancedRoles,
},
],
availableEngines: [
[],
{
- setRoleMappingData: (_, { availableEngines }) => availableEngines,
+ setRoleMappingsData: (_, { availableEngines }) => availableEngines,
resetState: () => [],
},
],
attributes: [
[],
{
- setRoleMappingData: (_, { attributes }) => attributes,
+ setRoleMappingsData: (_, { attributes }) => attributes,
resetState: () => [],
},
],
elasticsearchRoles: [
[],
{
- setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles,
+ setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles,
},
],
roleMapping: [
null,
{
- setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
+ setRoleMapping: (_, { roleMapping }) => roleMapping,
resetState: () => null,
closeRoleMappingFlyout: () => null,
},
@@ -176,16 +168,14 @@ export const RoleMappingsLogic = kea
- roleMapping ? (roleMapping.roleType as RoleTypes) : 'owner',
+ setRoleMapping: (_, { roleMapping }) => roleMapping.roleType as RoleTypes,
handleRoleChange: (_, { roleType }) => roleType,
},
],
accessAllEngines: [
true,
{
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.accessAllEngines : true,
+ setRoleMapping: (_, { roleMapping }) => roleMapping.accessAllEngines,
handleRoleChange: (_, { roleType }) => !roleHasScopedEngines(roleType),
handleAccessAllEnginesChange: (_, { selected }) => selected,
},
@@ -193,8 +183,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeValue(roleMapping) : '',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeValue(roleMapping),
handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) =>
value === 'role' ? firstElasticsearchRole : '',
handleAttributeValueChange: (_, { value }) => value,
@@ -205,8 +194,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeName(roleMapping) : 'username',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
closeRoleMappingFlyout: () => 'username',
@@ -215,8 +203,8 @@ export const RoleMappingsLogic = kea
- roleMapping ? new Set(roleMapping.engines.map((engine) => engine.name)) : new Set(),
+ setRoleMapping: (_, { roleMapping }) =>
+ new Set(roleMapping.engines.map((engine: Engine) => engine.name)),
handleAccessAllEnginesChange: () => new Set(),
handleEngineSelectionChange: (_, { engineNames }) => {
const newSelectedEngineNames = new Set() as Set;
@@ -229,7 +217,7 @@ export const RoleMappingsLogic = kea authProviders,
+ setRoleMappingsData: (_, { authProviders }) => authProviders,
},
],
selectedAuthProviders: [
@@ -246,8 +234,7 @@ export const RoleMappingsLogic = kea v !== ANY_AUTH_PROVIDER);
return [ANY_AUTH_PROVIDER];
},
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
+ setRoleMapping: (_, { roleMapping }) => roleMapping.authProvider,
},
],
roleMappingFlyoutOpen: [
@@ -292,21 +279,8 @@ export const RoleMappingsLogic = kea {
- const { http } = HttpLogic.values;
- const route = roleMappingId
- ? `/api/app_search/role_mappings/${roleMappingId}`
- : '/api/app_search/role_mappings/new';
-
- try {
- const response = await http.get(route);
- actions.setRoleMappingData(response);
- } catch (e) {
- if (e.status === 404) {
- setErrorMessage(ROLE_MAPPING_NOT_FOUND);
- } else {
- flashAPIErrors(e);
- }
- }
+ const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId);
+ if (roleMapping) actions.setRoleMapping(roleMapping);
},
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
index c8fb009fb31da25..bd5bdb7b2f66515 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts
@@ -37,7 +37,7 @@ export const ENGINE_SCHEMA_PATH = `${ENGINE_PATH}/schema`;
export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_SCHEMA_PATH}/reindex_job/:reindexJobId`;
export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
-// TODO: Crawler sub-pages
+export const ENGINE_CRAWLER_DOMAIN_PATH = `${ENGINE_CRAWLER_PATH}/domains/:domainId`;
export const META_ENGINE_CREATION_PATH = '/meta_engine_creation';
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
index 82284be0907fba3..7696cf03ed4b15e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx
@@ -7,7 +7,7 @@
import React, { Fragment } from 'react';
-import { EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
+import { EuiIconTip, EuiTextColor, EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui';
import { ASRoleMapping } from '../../app_search/types';
import { WSRoleMapping } from '../../workplace_search/types';
@@ -84,7 +84,7 @@ export const RoleMappingsTable: React.FC = ({
const roleCol: EuiBasicTableColumn = {
field: 'roleType',
name: ROLE_LABEL,
- render: (_, { rules }: SharedRoleMapping) => getFirstAttributeValue(rules),
+ render: (_, { roleType }: SharedRoleMapping) => roleType,
};
const accessItemsCol: EuiBasicTableColumn = {
@@ -124,11 +124,16 @@ export const RoleMappingsTable: React.FC = ({
field: 'id',
name: '',
align: 'right',
- render: (_, { id }: SharedRoleMapping) => (
- initializeRoleMapping(id)}
- onDeleteClick={() => handleDeleteMapping(id)}
- />
+ render: (_, { id, toolTip }: SharedRoleMapping) => (
+ <>
+ {id && (
+ initializeRoleMapping(id)}
+ onDeleteClick={() => handleDeleteMapping(id)}
+ />
+ )}
+ {toolTip && }
+ >
),
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
index 716cb8ebb6d47d3..4ee530870284e04 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts
@@ -16,13 +16,13 @@ import { groups } from '../../__mocks__/groups.mock';
import { nextTick } from '@kbn/test/jest';
import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { RoleMappingsLogic } from './role_mappings_logic';
describe('RoleMappingsLogic', () => {
const { http } = mockHttpValues;
- const { clearFlashMessages, flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
+ const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers;
const { mount } = new LogicMounter(RoleMappingsLogic);
const defaultValues = {
attributes: [],
@@ -52,14 +52,13 @@ describe('RoleMappingsLogic', () => {
name: 'Default',
};
- const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [wsRoleMapping] };
- const mappingServerProps = {
+ const mappingsServerProps = {
+ multipleAuthProvidersConfig: true,
+ roleMappings: [wsRoleMapping],
attributes: [],
authProviders: [],
availableGroups: [roleGroup, defaultGroup],
elasticsearchRoles: [],
- multipleAuthProvidersConfig: false,
- roleMapping: wsRoleMapping,
};
beforeEach(() => {
@@ -78,36 +77,17 @@ describe('RoleMappingsLogic', () => {
expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]);
expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true);
- });
-
- describe('setRoleMappingData', () => {
- it('sets data correctly', () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
-
- expect(RoleMappingsLogic.values.roleMapping).toEqual(wsRoleMapping);
- expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
- expect(RoleMappingsLogic.values.attributes).toEqual(mappingServerProps.attributes);
- expect(RoleMappingsLogic.values.availableGroups).toEqual(
- mappingServerProps.availableGroups
- );
- expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true);
- expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual(
- mappingServerProps.elasticsearchRoles
- );
- expect(RoleMappingsLogic.values.selectedGroups).toEqual(
- new Set([wsRoleMapping.groups[0].id])
- );
- expect(RoleMappingsLogic.values.selectedOptions).toEqual([]);
- });
-
- it('sets default group with new role mapping', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: undefined,
- });
-
- expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
- });
+ expect(RoleMappingsLogic.values.dataLoading).toEqual(false);
+ expect(RoleMappingsLogic.values.attributes).toEqual(mappingsServerProps.attributes);
+ expect(RoleMappingsLogic.values.availableGroups).toEqual(mappingsServerProps.availableGroups);
+ expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(false);
+ expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual(
+ mappingsServerProps.elasticsearchRoles
+ );
+ expect(RoleMappingsLogic.values.selectedOptions).toEqual([
+ { label: defaultGroup.name, value: defaultGroup.id },
+ ]);
+ expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id]));
});
it('handleRoleChange', () => {
@@ -119,14 +99,17 @@ describe('RoleMappingsLogic', () => {
it('handleGroupSelectionChange', () => {
const group = wsRoleMapping.groups[0];
const otherGroup = groups[0];
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- groups: [group, otherGroup],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ groups: [group, otherGroup],
+ },
+ ],
});
+ RoleMappingsLogic.actions.initializeRoleMapping(wsRoleMapping.id);
RoleMappingsLogic.actions.handleGroupSelectionChange([group.id, otherGroup.id]);
expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id, otherGroup.id]));
expect(RoleMappingsLogic.values.selectedOptions).toEqual([
@@ -147,8 +130,8 @@ describe('RoleMappingsLogic', () => {
const elasticsearchRoles = ['foo', 'bar'];
it('sets values correctly', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
elasticsearchRoles,
});
RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]);
@@ -172,12 +155,14 @@ describe('RoleMappingsLogic', () => {
describe('handleAuthProviderChange', () => {
beforeEach(() => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- authProvider: ['foo'],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ authProvider: ['foo'],
+ },
+ ],
});
});
const providers = ['bar', 'baz'];
@@ -201,28 +186,23 @@ describe('RoleMappingsLogic', () => {
});
it('handles "any" auth in previous state', () => {
- RoleMappingsLogic.actions.setRoleMappingData({
- ...mappingServerProps,
- roleMapping: {
- ...wsRoleMapping,
- authProvider: [ANY_AUTH_PROVIDER],
- },
+ RoleMappingsLogic.actions.setRoleMappingsData({
+ ...mappingsServerProps,
+ roleMappings: [
+ {
+ ...wsRoleMapping,
+ authProvider: [ANY_AUTH_PROVIDER],
+ },
+ ],
});
RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[1]]);
});
-
- it('handles catch-all state', () => {
- RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny);
-
- expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]);
- });
});
it('resetState', () => {
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
RoleMappingsLogic.actions.resetState();
expect(RoleMappingsLogic.values.dataLoading).toEqual(true);
@@ -234,7 +214,7 @@ describe('RoleMappingsLogic', () => {
});
it('openRoleMappingFlyout', () => {
- mount(mappingServerProps);
+ mount(mappingsServerProps);
RoleMappingsLogic.actions.openRoleMappingFlyout();
expect(RoleMappingsLogic.values.roleMappingFlyoutOpen).toEqual(true);
@@ -243,7 +223,7 @@ describe('RoleMappingsLogic', () => {
it('closeRoleMappingFlyout', () => {
mount({
- ...mappingServerProps,
+ ...mappingsServerProps,
roleMappingFlyoutOpen: true,
});
RoleMappingsLogic.actions.closeRoleMappingFlyout();
@@ -275,40 +255,20 @@ describe('RoleMappingsLogic', () => {
});
describe('initializeRoleMapping', () => {
- it('calls API and sets values for new mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping();
-
- expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/new');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('calls API and sets values for existing mapping', async () => {
- const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData');
- http.get.mockReturnValue(Promise.resolve(mappingServerProps));
- RoleMappingsLogic.actions.initializeRoleMapping('123');
-
- expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/123');
- await nextTick();
- expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps);
- });
-
- it('handles error', async () => {
- http.get.mockReturnValue(Promise.reject('this is an error'));
- RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
+ it('sets values for existing mapping', () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
+ RoleMappingsLogic.actions.initializeRoleMapping(wsRoleMapping.id);
- expect(flashAPIErrors).toHaveBeenCalledWith('this is an error');
+ expect(setRoleMappingDataSpy).toHaveBeenCalledWith(wsRoleMapping);
});
- it('shows error when there is a 404 status', async () => {
- http.get.mockReturnValue(Promise.reject({ status: 404 }));
+ it('does not set data for new mapping', async () => {
+ const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMapping');
+ RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
RoleMappingsLogic.actions.initializeRoleMapping();
- await nextTick();
- expect(setErrorMessage).toHaveBeenCalledWith(ROLE_MAPPING_NOT_FOUND);
+ expect(setRoleMappingDataSpy).not.toHaveBeenCalledWith(wsRoleMapping);
});
});
@@ -320,7 +280,7 @@ describe('RoleMappingsLogic', () => {
);
RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps);
- http.post.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.post.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings', {
@@ -331,7 +291,7 @@ describe('RoleMappingsLogic', () => {
rules: {
username: '',
},
- groups: [],
+ groups: [defaultGroup.id],
}),
});
await nextTick();
@@ -344,9 +304,9 @@ describe('RoleMappingsLogic', () => {
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
- http.put.mockReturnValue(Promise.resolve(mappingServerProps));
+ http.put.mockReturnValue(Promise.resolve(mappingsServerProps));
RoleMappingsLogic.actions.handleSaveMapping();
expect(http.put).toHaveBeenCalledWith(
@@ -408,7 +368,7 @@ describe('RoleMappingsLogic', () => {
RoleMappingsLogic.actions,
'initializeRoleMappings'
);
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
http.delete.mockReturnValue(Promise.resolve({}));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
@@ -421,7 +381,7 @@ describe('RoleMappingsLogic', () => {
});
it('handles error', async () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
http.delete.mockReturnValue(Promise.reject('this is an error'));
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
await nextTick();
@@ -430,7 +390,7 @@ describe('RoleMappingsLogic', () => {
});
it('will do nothing if not confirmed', async () => {
- RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps);
+ RoleMappingsLogic.actions.setRoleMapping(wsRoleMapping);
window.confirm = () => false;
RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId);
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
index aee780ac18971fb..361425b7a78a121 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts
@@ -13,10 +13,9 @@ import {
clearFlashMessages,
flashAPIErrors,
setSuccessMessage,
- setErrorMessage,
} from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { ANY_AUTH_PROVIDER, ROLE_MAPPING_NOT_FOUND } from '../../../shared/role_mapping/constants';
+import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants';
import { AttributeName } from '../../../shared/types';
import { RoleGroup, WSRoleMapping, Role } from '../../types';
@@ -30,16 +29,11 @@ import {
interface RoleMappingsServerDetails {
roleMappings: WSRoleMapping[];
- multipleAuthProvidersConfig: boolean;
-}
-
-interface RoleMappingServerDetails {
attributes: string[];
authProviders: string[];
availableGroups: RoleGroup[];
elasticsearchRoles: string[];
multipleAuthProvidersConfig: boolean;
- roleMapping?: WSRoleMapping;
}
const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName =>
@@ -62,7 +56,7 @@ interface RoleMappingsActions {
initializeRoleMapping(roleMappingId?: string): { roleMappingId?: string };
initializeRoleMappings(): void;
resetState(): void;
- setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails;
+ setRoleMapping(roleMapping: WSRoleMapping): { roleMapping: WSRoleMapping };
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
openRoleMappingFlyout(): void;
closeRoleMappingFlyout(): void;
@@ -93,7 +87,7 @@ export const RoleMappingsLogic = kea data,
- setRoleMappingData: (data: RoleMappingServerDetails) => data,
+ setRoleMapping: (roleMapping: WSRoleMapping) => ({ roleMapping }),
setRoleMappingErrors: (errors: string[]) => ({ errors }),
handleAuthProviderChange: (value: string[]) => ({ value }),
handleRoleChange: (roleType: Role) => ({ roleType }),
@@ -117,7 +111,6 @@ export const RoleMappingsLogic = kea false,
- setRoleMappingData: () => false,
resetState: () => true,
},
],
@@ -132,32 +125,31 @@ export const RoleMappingsLogic = kea multipleAuthProvidersConfig,
- setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig,
resetState: () => false,
},
],
availableGroups: [
[],
{
- setRoleMappingData: (_, { availableGroups }) => availableGroups,
+ setRoleMappingsData: (_, { availableGroups }) => availableGroups,
},
],
attributes: [
[],
{
- setRoleMappingData: (_, { attributes }) => attributes,
+ setRoleMappingsData: (_, { attributes }) => attributes,
},
],
elasticsearchRoles: [
[],
{
- setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles,
+ setRoleMappingsData: (_, { elasticsearchRoles }) => elasticsearchRoles,
},
],
roleMapping: [
null,
{
- setRoleMappingData: (_, { roleMapping }) => roleMapping || null,
+ setRoleMapping: (_, { roleMapping }) => roleMapping,
resetState: () => null,
closeRoleMappingFlyout: () => null,
},
@@ -165,23 +157,21 @@ export const RoleMappingsLogic = kea
- roleMapping ? (roleMapping.roleType as Role) : 'admin',
+ setRoleMapping: (_, { roleMapping }) => roleMapping.roleType as Role,
handleRoleChange: (_, { roleType }) => roleType,
},
],
includeInAllGroups: [
false,
{
- setRoleMappingData: (_, { roleMapping }) => (roleMapping ? roleMapping.allGroups : false),
+ setRoleMapping: (_, { roleMapping }) => roleMapping.allGroups,
handleAllGroupsSelectionChange: (_, { selected }) => selected,
},
],
attributeValue: [
'',
{
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? getFirstAttributeValue(roleMapping) : '',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeValue(roleMapping),
handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) =>
value === 'role' ? firstElasticsearchRole : '',
handleAttributeValueChange: (_, { value }) => value,
@@ -192,8 +182,7 @@ export const RoleMappingsLogic = kea
- roleMapping ? getFirstAttributeName(roleMapping) : 'username',
+ setRoleMapping: (_, { roleMapping }) => getFirstAttributeName(roleMapping),
handleAttributeSelectorChange: (_, { value }) => value,
resetState: () => 'username',
closeRoleMappingFlyout: () => 'username',
@@ -202,14 +191,14 @@ export const RoleMappingsLogic = kea
- roleMapping
- ? new Set(roleMapping.groups.map((group) => group.id))
- : new Set(
- availableGroups
- .filter((group) => group.name === DEFAULT_GROUP_NAME)
- .map((group) => group.id)
- ),
+ setRoleMappingsData: (_, { availableGroups }) =>
+ new Set(
+ availableGroups
+ .filter((group) => group.name === DEFAULT_GROUP_NAME)
+ .map((group) => group.id)
+ ),
+ setRoleMapping: (_, { roleMapping }) =>
+ new Set(roleMapping.groups.map((group: RoleGroup) => group.id)),
handleGroupSelectionChange: (_, { groupIds }) => {
const newSelectedGroupNames = new Set() as Set;
groupIds.forEach((groupId) => newSelectedGroupNames.add(groupId));
@@ -221,7 +210,7 @@ export const RoleMappingsLogic = kea authProviders,
+ setRoleMappingsData: (_, { authProviders }) => authProviders,
},
],
selectedAuthProviders: [
@@ -230,15 +219,15 @@ export const RoleMappingsLogic = kea {
const previouslyContainedAny = previous.includes(ANY_AUTH_PROVIDER);
const newSelectionsContainAny = value.includes(ANY_AUTH_PROVIDER);
+ const hasItems = value.length > 0;
- if (value.length < 1) return [ANY_AUTH_PROVIDER];
if (value.length === 1) return value;
- if (!newSelectionsContainAny) return value;
- if (previouslyContainedAny) return value.filter((v) => v !== ANY_AUTH_PROVIDER);
+ if (!newSelectionsContainAny && hasItems) return value;
+ if (previouslyContainedAny && hasItems)
+ return value.filter((v) => v !== ANY_AUTH_PROVIDER);
return [ANY_AUTH_PROVIDER];
},
- setRoleMappingData: (_, { roleMapping }) =>
- roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER],
+ setRoleMapping: (_, { roleMapping }) => roleMapping.authProvider,
},
],
roleMappingFlyoutOpen: [
@@ -283,21 +272,8 @@ export const RoleMappingsLogic = kea {
- const { http } = HttpLogic.values;
- const route = roleMappingId
- ? `/api/workplace_search/org/role_mappings/${roleMappingId}`
- : '/api/workplace_search/org/role_mappings/new';
-
- try {
- const response = await http.get(route);
- actions.setRoleMappingData(response);
- } catch (e) {
- if (e.status === 404) {
- setErrorMessage(ROLE_MAPPING_NOT_FOUND);
- } else {
- flashAPIErrors(e);
- }
- }
+ const roleMapping = values.roleMappings.find(({ id }) => id === roleMappingId);
+ if (roleMapping) actions.setRoleMapping(roleMapping);
},
handleDeleteMapping: async ({ roleMappingId }) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
index 626a107b6942ba3..06a206017fbd11f 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
@@ -31,5 +31,61 @@ describe('crawler routes', () => {
path: '/api/as/v0/engines/:name/crawler',
});
});
+
+ it('validates correctly with name', () => {
+ const request = { params: { name: 'some-engine' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: {} };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('DELETE /api/app_search/engines/{name}/crawler/domains/{id}', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'delete',
+ path: '/api/app_search/engines/{name}/crawler/domains/{id}',
+ });
+
+ registerCrawlerRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:name/crawler/domains/:id',
+ });
+ });
+
+ it('validates correctly with name and id', () => {
+ const request = { params: { name: 'some-engine', id: '1234' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: { id: '1234' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('fails validation without id', () => {
+ const request = { params: { name: 'test-engine' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('accepts a query param', () => {
+ const request = {
+ params: { name: 'test-engine', id: '1234' },
+ query: { respond_with: 'crawler_details' },
+ };
+ mockRouter.shouldValidate(request);
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
index 15b8340b07d4ebc..6c8ed7a49c64a6b 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
@@ -26,4 +26,22 @@ export function registerCrawlerRoutes({
path: '/api/as/v0/engines/:name/crawler',
})
);
+
+ router.delete(
+ {
+ path: '/api/app_search/engines/{name}/crawler/domains/{id}',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ id: schema.string(),
+ }),
+ query: schema.object({
+ respond_with: schema.maybe(schema.string()),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:name/crawler/domains/:id',
+ })
+ );
}
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
index a126d06f303b4c9..718597c12e9c5bf 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts
@@ -7,11 +7,7 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import {
- registerRoleMappingsRoute,
- registerRoleMappingRoute,
- registerNewRoleMappingRoute,
-} from './role_mappings';
+import { registerRoleMappingsRoute, registerRoleMappingRoute } from './role_mappings';
const roleMappingBaseSchema = {
rules: { username: 'user' },
@@ -80,29 +76,6 @@ describe('role mappings routes', () => {
});
});
- describe('GET /api/app_search/role_mappings/{id}', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/role_mappings/{id}',
- });
-
- registerRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/:id',
- });
- });
- });
-
describe('PUT /api/app_search/role_mappings/{id}', () => {
let mockRouter: MockRouter;
@@ -160,27 +133,4 @@ describe('role mappings routes', () => {
});
});
});
-
- describe('GET /api/app_search/role_mappings/new', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/app_search/role_mappings/new',
- });
-
- registerNewRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/role_mappings/new',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
index 86e17b575e019a4..75724a3344d6deb 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts
@@ -48,20 +48,6 @@ export function registerRoleMappingRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
- router.get(
- {
- path: '/api/app_search/role_mappings/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/:id',
- })
- );
-
router.put(
{
path: '/api/app_search/role_mappings/{id}',
@@ -92,23 +78,7 @@ export function registerRoleMappingRoute({
);
}
-export function registerNewRoleMappingRoute({
- router,
- enterpriseSearchRequestHandler,
-}: RouteDependencies) {
- router.get(
- {
- path: '/api/app_search/role_mappings/new',
- validate: false,
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/role_mappings/new',
- })
- );
-}
-
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
registerRoleMappingsRoute(dependencies);
registerRoleMappingRoute(dependencies);
- registerNewRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
index 0dade134767e44c..a945866da5ef210 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts
@@ -7,11 +7,7 @@
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
-import {
- registerOrgRoleMappingsRoute,
- registerOrgRoleMappingRoute,
- registerOrgNewRoleMappingRoute,
-} from './role_mappings';
+import { registerOrgRoleMappingsRoute, registerOrgRoleMappingRoute } from './role_mappings';
describe('role mappings routes', () => {
describe('GET /api/workplace_search/org/role_mappings', () => {
@@ -60,29 +56,6 @@ describe('role mappings routes', () => {
});
});
- describe('GET /api/workplace_search/org/role_mappings/{id}', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/workplace_search/org/role_mappings/{id}',
- });
-
- registerOrgRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/ws/org/role_mappings/:id',
- });
- });
- });
-
describe('PUT /api/workplace_search/org/role_mappings/{id}', () => {
let mockRouter: MockRouter;
@@ -128,27 +101,4 @@ describe('role mappings routes', () => {
});
});
});
-
- describe('GET /api/workplace_search/org/role_mappings/new', () => {
- let mockRouter: MockRouter;
-
- beforeEach(() => {
- jest.clearAllMocks();
- mockRouter = new MockRouter({
- method: 'get',
- path: '/api/workplace_search/org/role_mappings/new',
- });
-
- registerOrgNewRoleMappingRoute({
- ...mockDependencies,
- router: mockRouter.router,
- });
- });
-
- it('creates a request handler', () => {
- expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
- path: '/ws/org/role_mappings/new',
- });
- });
- });
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
index 5a6359c1cd83691..a0fcec63cbb2729 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts
@@ -48,20 +48,6 @@ export function registerOrgRoleMappingRoute({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
- router.get(
- {
- path: '/api/workplace_search/org/role_mappings/{id}',
- validate: {
- params: schema.object({
- id: schema.string(),
- }),
- },
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/ws/org/role_mappings/:id',
- })
- );
-
router.put(
{
path: '/api/workplace_search/org/role_mappings/{id}',
@@ -92,23 +78,7 @@ export function registerOrgRoleMappingRoute({
);
}
-export function registerOrgNewRoleMappingRoute({
- router,
- enterpriseSearchRequestHandler,
-}: RouteDependencies) {
- router.get(
- {
- path: '/api/workplace_search/org/role_mappings/new',
- validate: false,
- },
- enterpriseSearchRequestHandler.createRequest({
- path: '/ws/org/role_mappings/new',
- })
- );
-}
-
export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => {
registerOrgRoleMappingsRoute(dependencies);
registerOrgRoleMappingRoute(dependencies);
- registerOrgNewRoleMappingRoute(dependencies);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
index 49836e9ed4ca68f..d707fd162ae0204 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/layouts/default.tsx
@@ -18,7 +18,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import type { Section } from '../sections';
-import { AlphaMessaging, SettingFlyout } from '../components';
+import { SettingFlyout } from '../components';
import { useLink, useConfig, useUrlModal } from '../hooks';
interface Props {
@@ -144,7 +144,6 @@ export const DefaultLayout: React.FunctionComponent = ({
) : null}
{children}
-
>
);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
index 3e4b120a28f8e76..75fc06c1a449451 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx
@@ -7,7 +7,7 @@
import type { ReactEventHandler } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
-import { useRouteMatch, useHistory } from 'react-router-dom';
+import { useRouteMatch, useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -49,7 +49,6 @@ import { CreatePackagePolicyPageLayout } from './components';
import type { CreatePackagePolicyFrom, PackagePolicyFormState } from './types';
import type { PackagePolicyValidationResults } from './services';
import { validatePackagePolicy, validationHasErrors } from './services';
-import { StepSelectPackage } from './step_select_package';
import { StepSelectAgentPolicy } from './step_select_agent_policy';
import { StepConfigurePackagePolicy } from './step_configure_package';
import { StepDefinePackagePolicy } from './step_define_package_policy';
@@ -60,9 +59,15 @@ const StepsWithLessPadding = styled(EuiSteps)`
}
`;
+const CustomEuiBottomBar = styled(EuiBottomBar)`
+ // Set a relatively _low_ z-index value here to account for EuiComboBox popover that might appear under the bottom bar
+ z-index: 50;
+`;
+
interface AddToPolicyParams {
pkgkey: string;
integration?: string;
+ policyId?: string;
}
interface AddFromPolicyParams {
@@ -81,10 +86,14 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
const routeState = useIntraAppState();
const from: CreatePackagePolicyFrom = 'policyId' in params ? 'policy' : 'package';
+ const { search } = useLocation();
+ const queryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const policyId = useMemo(() => queryParams.get('policyId') ?? undefined, [queryParams]);
+
// Agent policy and package info states
- const [agentPolicy, setAgentPolicy] = useState();
+ const [agentPolicy, setAgentPolicy] = useState();
const [packageInfo, setPackageInfo] = useState();
- const [isLoadingSecondStep, setIsLoadingSecondStep] = useState(false);
+ const [isLoadingAgentPolicyStep, setIsLoadingAgentPolicyStep] = useState(false);
// Retrieve agent count
const agentPolicyId = agentPolicy?.id;
@@ -286,6 +295,13 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
agentPolicyName: agentPolicy.name,
},
})
+ : (params as AddToPolicyParams)?.policyId && agentPolicy && agentCount === 0
+ ? i18n.translate('xpack.fleet.createPackagePolicy.addAgentNextNotification', {
+ defaultMessage: `The policy has been updated. Add an agent to the '{agentPolicyName}' policy to deploy this policy.`,
+ values: {
+ agentPolicyName: agentPolicy.name,
+ },
+ })
: undefined,
'data-test-subj': 'packagePolicyCreateSuccessToast',
});
@@ -337,32 +353,20 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
),
- [params, updatePackageInfo, agentPolicy, updateAgentPolicy]
+ [params, updatePackageInfo, agentPolicy, updateAgentPolicy, policyId]
);
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
- const stepSelectPackage = useMemo(
- () => (
-
- ),
- [params, updateAgentPolicy, packageInfo, updatePackageInfo]
- );
-
const stepConfigurePackagePolicy = useMemo(
() =>
- isLoadingSecondStep ? (
+ isLoadingAgentPolicyStep ? (
) : agentPolicy && packageInfo ? (
<>
@@ -399,7 +403,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
),
[
- isLoadingSecondStep,
+ isLoadingAgentPolicyStep,
agentPolicy,
packageInfo,
packagePolicy,
@@ -413,27 +417,20 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
);
const steps: EuiStepProps[] = [
- from === 'package'
- ? {
- title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
- defaultMessage: 'Select an agent policy',
- }),
- children: stepSelectAgentPolicy,
- }
- : {
- title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectPackageTitle', {
- defaultMessage: 'Select an integration',
- }),
- children: stepSelectPackage,
- },
{
title: i18n.translate('xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle', {
defaultMessage: 'Configure integration',
}),
- status: !packageInfo || !agentPolicy || isLoadingSecondStep ? 'disabled' : undefined,
+ status: !packageInfo || !agentPolicy || isLoadingAgentPolicyStep ? 'disabled' : undefined,
'data-test-subj': 'dataCollectionSetupStep',
children: stepConfigurePackagePolicy,
},
+ {
+ title: i18n.translate('xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle', {
+ defaultMessage: 'Apply to agent policy',
+ }),
+ children: stepSelectAgentPolicy,
+ },
];
return (
@@ -459,10 +456,10 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
)}
-
+
- {!isLoadingSecondStep && agentPolicy && packageInfo && formState === 'INVALID' ? (
+ {!isLoadingAgentPolicyStep && agentPolicy && packageInfo && formState === 'INVALID' ? (
{
-
+
);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx
index 26d47cbff5b86c1..e2f5a7249ff0ad2 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx
@@ -14,9 +14,11 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
- EuiTextColor,
EuiPortal,
EuiFormRow,
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiText,
EuiLink,
} from '@elastic/eui';
@@ -32,38 +34,32 @@ import {
} from '../../../hooks';
import { CreateAgentPolicyFlyout } from '../list_page/components';
-const AgentPolicyWrapper = styled(EuiFormRow)`
+const AgentPolicyFormRow = styled(EuiFormRow)`
.euiFormRow__label {
width: 100%;
}
`;
-// Custom styling for drop down list items due to:
-// 1) the max-width and overflow properties is added to prevent long agent policy
-// names/descriptions from overflowing the flex items
-// 2) max-width is built from the grow property on the flex items because the value
-// changes based on if Fleet is enabled/setup or not
-const AgentPolicyNameColumn = styled(EuiFlexItem)`
- max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`};
- overflow: hidden;
-`;
-const AgentPolicyDescriptionColumn = styled(EuiFlexItem)`
- max-width: ${(props) => `${((props.grow as number) / 9) * 100}%`};
- overflow: hidden;
-`;
-
export const StepSelectAgentPolicy: React.FunctionComponent<{
pkgkey: string;
updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
+ defaultAgentPolicyId?: string;
agentPolicy: AgentPolicy | undefined;
updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void;
setIsLoadingSecondStep: (isLoading: boolean) => void;
-}> = ({ pkgkey, updatePackageInfo, agentPolicy, updateAgentPolicy, setIsLoadingSecondStep }) => {
+}> = ({
+ pkgkey,
+ updatePackageInfo,
+ agentPolicy,
+ updateAgentPolicy,
+ setIsLoadingSecondStep,
+ defaultAgentPolicyId,
+}) => {
const { isReady: isFleetReady } = useFleetStatus();
// Selected agent policy state
const [selectedPolicyId, setSelectedPolicyId] = useState(
- agentPolicy ? agentPolicy.id : undefined
+ agentPolicy?.id ?? defaultAgentPolicyId
);
const [selectedAgentPolicyError, setSelectedAgentPolicyError] = useState();
@@ -223,95 +219,91 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{
) : null}
-
-
+
+
-
-
-
- setIsCreateAgentPolicyFlyoutOpen(true)}
- >
-
-
-
-
-
+
+
}
- helpText={
- isFleetReady && selectedPolicyId ? (
-
- ) : null
+ description={
+
+
+
+
+
}
>
- ) => {
- return (
-
-
- {option.label}
-
-
-
- {agentPoliciesById[option.value!].description}
-
-
- {isFleetReady ? (
-
-
-
-
-
- ) : null}
-
- );
- }}
- selectedOptions={selectedAgentPolicyOption ? [selectedAgentPolicyOption] : []}
- onChange={(options) => {
- const selectedOption = options[0] || undefined;
- if (selectedOption) {
- if (selectedOption.value !== selectedPolicyId) {
- setSelectedPolicyId(selectedOption.value);
+ label={
+
+
+
+
+
+
+ setIsCreateAgentPolicyFlyoutOpen(true)}
+ >
+
+
+
+
+
+ }
+ helpText={
+ isFleetReady && selectedPolicyId ? (
+
+ ) : null
+ }
+ >
+
-
+ )}
+ singleSelection={{ asPlainText: true }}
+ isClearable={false}
+ fullWidth={true}
+ isLoading={isAgentPoliciesLoading || isPackageInfoLoading}
+ options={agentPolicyOptions}
+ selectedOptions={selectedAgentPolicyOption ? [selectedAgentPolicyOption] : []}
+ onChange={(options) => {
+ const selectedOption = options[0] || undefined;
+ if (selectedOption) {
+ if (selectedOption.value !== selectedPolicyId) {
+ setSelectedPolicyId(selectedOption.value);
+ }
+ } else {
+ setSelectedPolicyId(undefined);
+ }
+ }}
+ />
+
+
{/* Display selected agent policy error if there is one */}
{selectedAgentPolicyError ? (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx
deleted file mode 100644
index 50c63274b5e85c5..000000000000000
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx
+++ /dev/null
@@ -1,198 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useEffect, useState, Fragment } from 'react';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui';
-
-import { Error, PackageIcon } from '../../../components';
-import type { AgentPolicy, PackageInfo, PackagePolicy, GetPackagesResponse } from '../../../types';
-import {
- useGetOneAgentPolicy,
- useGetPackages,
- useGetLimitedPackages,
- sendGetPackageInfoByKey,
-} from '../../../hooks';
-import { pkgKeyFromPackageInfo } from '../../../services';
-
-export const StepSelectPackage: React.FunctionComponent<{
- agentPolicyId: string;
- updateAgentPolicy: (agentPolicy: AgentPolicy | undefined) => void;
- packageInfo?: PackageInfo;
- updatePackageInfo: (packageInfo: PackageInfo | undefined) => void;
- setIsLoadingSecondStep: (isLoading: boolean) => void;
-}> = ({
- agentPolicyId,
- updateAgentPolicy,
- packageInfo,
- updatePackageInfo,
- setIsLoadingSecondStep,
-}) => {
- // Selected package state
- const [selectedPkgKey, setSelectedPkgKey] = useState(
- packageInfo ? pkgKeyFromPackageInfo(packageInfo) : undefined
- );
- const [selectedPkgError, setSelectedPkgError] = useState();
-
- // Fetch agent policy info
- const {
- data: agentPolicyData,
- error: agentPolicyError,
- isLoading: isAgentPoliciesLoading,
- } = useGetOneAgentPolicy(agentPolicyId);
-
- // Fetch packages info
- // Filter out limited packages already part of selected agent policy
- const [packages, setPackages] = useState([]);
- const {
- data: packagesData,
- error: packagesError,
- isLoading: isPackagesLoading,
- } = useGetPackages();
- const {
- data: limitedPackagesData,
- isLoading: isLimitedPackagesLoading,
- } = useGetLimitedPackages();
- useEffect(() => {
- if (packagesData?.response && limitedPackagesData?.response && agentPolicyData?.item) {
- const allPackages = packagesData.response;
- const limitedPackages = limitedPackagesData.response;
- const usedLimitedPackages = (agentPolicyData.item.package_policies as PackagePolicy[])
- .map((packagePolicy) => packagePolicy.package?.name || '')
- .filter((pkgName) => limitedPackages.includes(pkgName));
- setPackages(allPackages.filter((pkg) => !usedLimitedPackages.includes(pkg.name)));
- }
- }, [packagesData, limitedPackagesData, agentPolicyData]);
-
- // Update parent agent policy state
- useEffect(() => {
- if (agentPolicyData && agentPolicyData.item) {
- updateAgentPolicy(agentPolicyData.item);
- }
- }, [agentPolicyData, updateAgentPolicy]);
-
- // Update parent selected package state
- useEffect(() => {
- const fetchPackageInfo = async () => {
- if (selectedPkgKey) {
- setIsLoadingSecondStep(true);
- const { data, error } = await sendGetPackageInfoByKey(selectedPkgKey);
- if (error) {
- setSelectedPkgError(error);
- updatePackageInfo(undefined);
- } else if (data && data.response) {
- setSelectedPkgError(undefined);
- updatePackageInfo(data.response);
- }
- setIsLoadingSecondStep(false);
- } else {
- setSelectedPkgError(undefined);
- updatePackageInfo(undefined);
- }
- };
- if (!packageInfo || selectedPkgKey !== pkgKeyFromPackageInfo(packageInfo)) {
- fetchPackageInfo();
- }
- }, [selectedPkgKey, packageInfo, updatePackageInfo, setIsLoadingSecondStep]);
-
- // Display agent policy error if there is one
- if (agentPolicyError) {
- return (
-
- }
- error={agentPolicyError}
- />
- );
- }
-
- // Display packages list error if there is one
- if (packagesError) {
- return (
-
- }
- error={packagesError}
- />
- );
- }
-
- return (
-
-
- {
- const pkgkey = `${name}-${version}`;
- return {
- label: title || name,
- key: pkgkey,
- prepend: ,
- checked: selectedPkgKey === pkgkey ? 'on' : undefined,
- };
- })}
- listProps={{
- bordered: true,
- }}
- searchProps={{
- placeholder: i18n.translate(
- 'xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder',
- {
- defaultMessage: 'Search for integrations',
- }
- ),
- }}
- height={240}
- onChange={(options) => {
- const selectedOption = options.find((option) => option.checked === 'on');
- if (selectedOption) {
- if (selectedOption.key !== selectedPkgKey) {
- setSelectedPkgKey(selectedOption.key);
- }
- } else {
- setSelectedPkgKey(undefined);
- }
- }}
- >
- {(list, search) => (
-
- {search}
-
- {list}
-
- )}
-
-
- {/* Display selected package error if there is one */}
- {selectedPkgError ? (
-
-
- }
- error={selectedPkgError}
- />
-
- ) : null}
-
- );
-};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
index 2e6e7fb984ef0b0..49af14b7234fa2a 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx
@@ -18,9 +18,11 @@ import {
EuiText,
} from '@elastic/eui';
+import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common';
+import { pagePathGetters } from '../../../../../../../constants';
import type { AgentPolicy, PackagePolicy } from '../../../../../types';
import { PackageIcon, PackagePolicyActionsMenu } from '../../../../../components';
-import { useCapabilities, useLink } from '../../../../../hooks';
+import { useCapabilities, useStartServices } from '../../../../../hooks';
interface InMemoryPackagePolicy extends PackagePolicy {
packageName?: string;
@@ -49,7 +51,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
agentPolicy,
...rest
}) => {
- const { getHref } = useLink();
+ const { application } = useStartServices();
const hasWriteCapabilities = useCapabilities().write;
// With the package policies provided on input, generate the list of package policies
@@ -194,8 +196,13 @@ export const PackagePoliciesTable: React.FunctionComponent = ({
{
+ application.navigateToApp(INTEGRATIONS_PLUGIN_ID, {
+ path: `#${pagePathGetters.integrations_all()[1]}`,
+ state: { forAgentPolicyId: agentPolicy.id },
+ });
+ }}
>
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts
index 7f758e3c472c189..9e4cdde064e70d9 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts
+++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts
@@ -10,3 +10,4 @@ export { useBreadcrumbs } from './use_breadcrumbs';
export { useLinks } from './use_links';
export * from './use_local_search';
export * from './use_package_install';
+export * from './use_agent_policy_context';
diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_agent_policy_context.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_agent_policy_context.tsx
new file mode 100644
index 000000000000000..859db79ad159b99
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_agent_policy_context.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { FunctionComponent } from 'react';
+import React, { createContext, useContext, useRef, useCallback } from 'react';
+
+import type { IntegrationsAppBrowseRouteState } from '../../../types';
+import { useIntraAppState } from '../../../hooks';
+
+interface AgentPolicyContextValue {
+ getId(): string | undefined;
+}
+
+const AgentPolicyContext = createContext({ getId: () => undefined });
+
+export const AgentPolicyContextProvider: FunctionComponent = ({ children }) => {
+ const maybeState = useIntraAppState();
+ const ref = useRef(maybeState?.forAgentPolicyId);
+
+ const getId = useCallback(() => {
+ return ref.current;
+ }, []);
+ return {children} ;
+};
+
+export const useAgentPolicyContext = () => {
+ const ctx = useContext(AgentPolicyContext);
+ if (!ctx) {
+ throw new Error('useAgentPolicyContext can only be used inside of AgentPolicyContextProvider');
+ }
+ return ctx;
+};
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
index 68736455b818f62..99a29a8194f9b03 100644
--- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx
@@ -37,7 +37,12 @@ import {
INTEGRATIONS_ROUTING_PATHS,
pagePathGetters,
} from '../../../../constants';
-import { useCapabilities, useGetPackageInfoByKey, useLink } from '../../../../hooks';
+import {
+ useCapabilities,
+ useGetPackageInfoByKey,
+ useLink,
+ useAgentPolicyContext,
+} from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services';
import type {
CreatePackagePolicyRouteState,
@@ -79,6 +84,7 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
}
export function Detail() {
+ const { getId: getAgentPolicyId } = useAgentPolicyContext();
const { pkgkey, panel } = useParams();
const { getHref } = useLink();
const hasWriteCapabilites = useCapabilities().write;
@@ -87,6 +93,7 @@ export function Detail() {
const queryParams = useMemo(() => new URLSearchParams(search), [search]);
const integration = useMemo(() => queryParams.get('integration'), [queryParams]);
const services = useStartServices();
+ const agentPolicyIdFromContext = getAgentPolicyId();
// Package info state
const [packageInfo, setPackageInfo] = useState(null);
@@ -211,24 +218,42 @@ export function Detail() {
search,
hash,
});
- const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
- CreatePackagePolicyRouteState['onCancelNavigateTo'] = [
- INTEGRATIONS_PLUGIN_ID,
- {
- path: currentPath,
- },
- ];
- const redirectBackRouteState: CreatePackagePolicyRouteState = {
- onSaveNavigateTo: redirectToPath,
- onCancelNavigateTo: redirectToPath,
- onCancelUrl: currentPath,
- };
const path = pagePathGetters.add_integration_to_policy({
pkgkey,
...(integration ? { integration } : {}),
+ ...(agentPolicyIdFromContext ? { agentPolicyId: agentPolicyIdFromContext } : {}),
})[1];
+ let redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] &
+ CreatePackagePolicyRouteState['onCancelNavigateTo'];
+
+ if (agentPolicyIdFromContext) {
+ redirectToPath = [
+ PLUGIN_ID,
+ {
+ path: `#${
+ pagePathGetters.policy_details({
+ policyId: agentPolicyIdFromContext,
+ })[1]
+ }`,
+ },
+ ];
+ } else {
+ redirectToPath = [
+ INTEGRATIONS_PLUGIN_ID,
+ {
+ path: currentPath,
+ },
+ ];
+ }
+
+ const redirectBackRouteState: CreatePackagePolicyRouteState = {
+ onSaveNavigateTo: redirectToPath,
+ onCancelNavigateTo: redirectToPath,
+ onCancelUrl: currentPath,
+ };
+
services.application.navigateToApp(PLUGIN_ID, {
// Necessary because of Fleet's HashRouter. Can be changed when
// https://github.com/elastic/kibana/issues/96134 is resolved
@@ -236,7 +261,16 @@ export function Detail() {
state: redirectBackRouteState,
});
},
- [history, hash, pathname, search, pkgkey, integration, services.application]
+ [
+ history,
+ hash,
+ pathname,
+ search,
+ pkgkey,
+ integration,
+ services.application,
+ agentPolicyIdFromContext,
+ ]
);
const headerRightContent = useMemo(
@@ -284,6 +318,9 @@ export function Detail() {
href={getHref('add_integration_to_policy', {
pkgkey,
...(integration ? { integration } : {}),
+ ...(agentPolicyIdFromContext
+ ? { agentPolicyId: agentPolicyIdFromContext }
+ : {}),
})}
onClick={handleAddIntegrationPolicyClick}
data-test-subj="addIntegrationPolicyButton"
@@ -325,6 +362,7 @@ export function Detail() {
packageInstallStatus,
pkgkey,
updateAvailable,
+ agentPolicyIdFromContext,
]
);
diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts
index 4ff71cf162e22ab..3c9c0e575961515 100644
--- a/x-pack/plugins/fleet/public/constants/page_paths.ts
+++ b/x-pack/plugins/fleet/public/constants/page_paths.ts
@@ -111,9 +111,10 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
`/policies/${policyId}/add-integration`,
],
- add_integration_to_policy: ({ pkgkey, integration }) => [
+ add_integration_to_policy: ({ pkgkey, integration, agentPolicyId }) => [
FLEET_BASE_PATH,
- `/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}`,
+ // prettier-ignore
+ `/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}${agentPolicyId ? `?policyId=${agentPolicyId}` : ''}`,
],
edit_integration: ({ policyId, packagePolicyId }) => [
FLEET_BASE_PATH,
diff --git a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts
index 74921daa0d2a18d..36fd32c2a6584f7 100644
--- a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts
+++ b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts
@@ -39,6 +39,11 @@ export interface AgentDetailsReassignPolicyAction {
onDoneNavigateTo?: Parameters;
}
+export interface IntegrationsAppBrowseRouteState {
+ /** The agent policy that we are browsing integrations for */
+ forAgentPolicyId: string;
+}
+
/**
* All possible Route states.
*/
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index e43f147a65800ff..bf1a78e3cfe90fa 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -31,7 +31,7 @@ describe('Index Templates tab', () => {
server.restore();
});
- describe('when there are no index templates', () => {
+ describe('when there are no index templates of either kind', () => {
test('should display an empty prompt', async () => {
httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] });
@@ -46,6 +46,26 @@ describe('Index Templates tab', () => {
});
});
+ describe('when there are composable index templates but no legacy index templates', () => {
+ test('only the composable index templates table is visible', async () => {
+ httpRequestsMockHelpers.setLoadTemplatesResponse({
+ templates: [fixtures.getComposableTemplate()],
+ legacyTemplates: [],
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+ const { exists, component } = testBed;
+ component.update();
+
+ expect(exists('sectionLoading')).toBe(false);
+ expect(exists('emptyPrompt')).toBe(false);
+ expect(exists('templateTable')).toBe(true);
+ expect(exists('legacyTemplateTable')).toBe(false);
+ });
+ });
+
describe('when there are index templates', () => {
// Add a default loadIndexTemplate response
httpRequestsMockHelpers.setLoadTemplateResponse(fixtures.getTemplate());
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts
index 199ace6048bdec8..7d3b34a6b823877 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.helpers.ts
@@ -11,17 +11,23 @@ import { WithAppDependencies } from '../helpers';
import { formSetup, TestSubjects } from './template_form.helpers';
-const testBedConfig: TestBedConfig = {
- memoryRouter: {
- initialEntries: [`/create_template`],
- componentRoutePath: `/create_template`,
- },
- doMountAsync: true,
-};
+export const setup: any = (isLegacy: boolean = false) => {
+ const route = isLegacy
+ ? { pathname: '/create_template', search: '?legacy=true' }
+ : { pathname: '/create_template' };
+
+ const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [route],
+ componentRoutePath: route,
+ },
+ doMountAsync: true,
+ };
-const initTestBed = registerTestBed(
- WithAppDependencies(TemplateCreate),
- testBedConfig
-);
+ const initTestBed = registerTestBed(
+ WithAppDependencies(TemplateCreate),
+ testBedConfig
+ );
-export const setup: any = formSetup.bind(null, initTestBed);
+ return formSetup.call(null, initTestBed);
+};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
index 9f435fac8b3471c..77ce172f3e0dbaa 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
@@ -101,7 +101,7 @@ describe(' ', () => {
(window as any)['__react-beautiful-dnd-disable-dev-warnings'] = false;
});
- describe('on component mount', () => {
+ describe('composable index template', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup();
@@ -115,6 +115,11 @@ describe(' ', () => {
expect(find('pageTitle').text()).toEqual('Create template');
});
+ test('renders no deprecation warning', async () => {
+ const { exists } = testBed;
+ expect(exists('legacyIndexTemplateDeprecationWarning')).toBe(false);
+ });
+
test('should not let the user go to the next step with invalid fields', async () => {
const { find, actions, component } = testBed;
@@ -129,6 +134,26 @@ describe(' ', () => {
});
});
+ describe('legacy index template', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ testBed = await setup(true);
+ });
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Create legacy template');
+ });
+
+ test('renders deprecation warning', async () => {
+ const { exists } = testBed;
+ expect(exists('legacyIndexTemplateDeprecationWarning')).toBe(true);
+ });
+ });
+
describe('form validation', () => {
beforeEach(async () => {
await act(async () => {
@@ -150,6 +175,11 @@ describe(' ', () => {
expect(find('stepTitle').text()).toEqual('Component templates (optional)');
});
+ it(`doesn't render the deprecated legacy index template warning`, () => {
+ const { exists } = testBed;
+ expect(exists('legacyIndexTemplateDeprecationWarning')).toBe(false);
+ });
+
it('should list the available component templates', () => {
const {
actions: {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts
index 355bbc12f94fc89..01aeba31770db99 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_form.helpers.ts
@@ -306,6 +306,7 @@ export type TestSubjects =
| 'indexPatternsField'
| 'indexPatternsWarning'
| 'indexPatternsWarningDescription'
+ | 'legacyIndexTemplateDeprecationWarning'
| 'mappingsEditorFieldEdit'
| 'mockCodeEditor'
| 'mockComboBox'
diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/index.ts b/x-pack/plugins/index_management/public/application/components/index_templates/index.ts
index a9131bab70551e7..d460175543ac532 100644
--- a/x-pack/plugins/index_management/public/application/components/index_templates/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/index_templates/index.ts
@@ -5,4 +5,12 @@
* 2.0.
*/
-export * from './simulate_template';
+export {
+ SimulateTemplateFlyoutContent,
+ simulateTemplateFlyoutProps,
+ SimulateTemplateProps,
+ SimulateTemplate,
+ SimulateTemplateFilters,
+} from './simulate_template';
+
+export { LegacyIndexTemplatesDeprecation } from './legacy_index_template_deprecation';
diff --git a/x-pack/plugins/index_management/public/application/components/index_templates/legacy_index_template_deprecation.tsx b/x-pack/plugins/index_management/public/application/components/index_templates/legacy_index_template_deprecation.tsx
new file mode 100644
index 000000000000000..6fbea1760f3a42f
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/index_templates/legacy_index_template_deprecation.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
+import { reactRouterNavigate } from '../../../shared_imports';
+import { documentationService } from '../../services/documentation';
+
+interface Props {
+ history?: ScopedHistory;
+ showCta?: boolean;
+}
+
+export const LegacyIndexTemplatesDeprecation: React.FunctionComponent = ({
+ history,
+ showCta,
+}) => {
+ return (
+
+ {showCta && history && (
+
+
+
+
+ ),
+ learnMoreLink: (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.home.legacyIndexTemplatesDeprecation.ctaLearnMoreLinkText',
+ {
+ defaultMessage: 'learn more.',
+ }
+ )}
+
+ ),
+ }}
+ />
+
+ )}
+
+ {!showCta && (
+
+ {i18n.translate('xpack.idxMgmt.home.legacyIndexTemplatesDeprecation.learnMoreLinkText', {
+ defaultMessage: 'Learn more.',
+ })}
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index ef9cde30907f027..54160141827d0a8 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -9,24 +9,26 @@ import React, { useState, useCallback, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiSpacer, EuiButton } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
import { serializers, Forms, GlobalFlyout } from '../../../shared_imports';
+import {
+ CommonWizardSteps,
+ StepSettingsContainer,
+ StepMappingsContainer,
+ StepAliasesContainer,
+} from '../shared';
+import { documentationService } from '../../services/documentation';
import { SectionError } from '../section_error';
import {
SimulateTemplateFlyoutContent,
SimulateTemplateProps,
simulateTemplateFlyoutProps,
SimulateTemplateFilters,
+ LegacyIndexTemplatesDeprecation,
} from '../index_templates';
import { StepLogisticsContainer, StepComponentContainer, StepReviewContainer } from './steps';
-import {
- CommonWizardSteps,
- StepSettingsContainer,
- StepMappingsContainer,
- StepAliasesContainer,
-} from '../shared';
-import { documentationService } from '../../services/documentation';
const { stripEmptyFields } = serializers;
const { FormWizard, FormWizardStep } = Forms;
@@ -38,6 +40,7 @@ interface Props {
clearSaveError: () => void;
isSaving: boolean;
saveError: any;
+ history?: ScopedHistory;
isLegacy?: boolean;
defaultValue?: TemplateDeserialized;
isEditing?: boolean;
@@ -98,6 +101,7 @@ export const TemplateForm = ({
saveError,
clearSaveError,
onSave,
+ history,
}: Props) => {
const [wizardContent, setWizardContent] = useState | null>(null);
const { addContent: addContentToGlobalFlyout, closeFlyout } = useGlobalFlyout();
@@ -283,12 +287,20 @@ export const TemplateForm = ({
);
};
+ const isLegacyIndexTemplate = indexTemplate._kbnMeta.isLegacy === true;
+
return (
<>
{/* Form header */}
{title}
-
+
+
+ {isLegacyIndexTemplate && (
+
+ )}
+
+
defaultValue={wizardDefaultValue}
@@ -311,7 +323,7 @@ export const TemplateForm = ({
/>
- {indexTemplate._kbnMeta.isLegacy !== true && (
+ {!isLegacyIndexTemplate && (
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
index b820bf559fb74c1..8b756be535ed234 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export * from './template_type_indicator';
+export { TemplateTypeIndicator } from './template_type_indicator';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index ecd41455d3249b7..b8b5a8e3c7d1a42 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -24,7 +24,13 @@ import {
import { UIM_TEMPLATE_LIST_LOAD } from '../../../../../common/constants';
import { TemplateListItem } from '../../../../../common';
-import { SectionError, SectionLoading, Error } from '../../../components';
+import { attemptToURIDecode } from '../../../../shared_imports';
+import {
+ SectionError,
+ SectionLoading,
+ Error,
+ LegacyIndexTemplatesDeprecation,
+} from '../../../components';
import { useLoadIndexTemplates } from '../../../services/api';
import { documentationService } from '../../../services/documentation';
import { useServices } from '../../../app_context';
@@ -34,11 +40,10 @@ import {
getTemplateCloneLink,
} from '../../../services/routing';
import { getIsLegacyFromQueryParams } from '../../../lib/index_templates';
+import { FilterListButton, Filters } from '../components';
import { TemplateTable } from './template_table';
import { TemplateDetails } from './template_details';
import { LegacyTemplateTable } from './legacy_templates/template_table';
-import { FilterListButton, Filters } from '../components';
-import { attemptToURIDecode } from '../../../../shared_imports';
type FilterName = 'managed' | 'cloudManaged' | 'system';
interface MatchParams {
@@ -130,7 +135,7 @@ export const TemplateList: React.FunctionComponent
-
+
+
+
+
+
+
+
0 && renderLegacyTemplatesTable()}
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
index 37df44d175771c7..36bff298e345ba9 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx
@@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
import { TemplateForm, SectionLoading, SectionError, Error } from '../../components';
@@ -114,6 +115,7 @@ export const TemplateClone: React.FunctionComponent
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
index 32b6ce7181bfd51..310807aeef38fdf 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx
@@ -11,10 +11,11 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { parse } from 'query-string';
+import { ScopedHistory } from 'kibana/public';
+import { TemplateDeserialized } from '../../../../common';
import { TemplateForm } from '../../components';
import { breadcrumbService } from '../../services/breadcrumbs';
-import { TemplateDeserialized } from '../../../../common';
import { saveTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
@@ -76,6 +77,7 @@ export const TemplateCreate: React.FunctionComponent = ({ h
saveError={saveError}
clearSaveError={clearSaveError}
isLegacy={isLegacy}
+ history={history as ScopedHistory}
/>
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index 716f85e5ff1c47a..f4ffe97931a2409 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -9,14 +9,15 @@ import React, { useEffect, useState, Fragment } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiPageBody, EuiPageContent, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui';
+import { ScopedHistory } from 'kibana/public';
import { TemplateDeserialized } from '../../../../common';
+import { attemptToURIDecode } from '../../../shared_imports';
import { breadcrumbService } from '../../services/breadcrumbs';
import { useLoadIndexTemplate, updateTemplate } from '../../services/api';
import { getTemplateDetailsLink } from '../../services/routing';
import { SectionLoading, SectionError, TemplateForm, Error } from '../../components';
import { getIsLegacyFromQueryParams } from '../../lib/index_templates';
-import { attemptToURIDecode } from '../../../shared_imports';
interface MatchParams {
name: string;
@@ -154,6 +155,7 @@ export const TemplateEdit: React.FunctionComponent
);
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
index 76ebe21a5136710..78e3f2dab0d1dbb 100644
--- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts
@@ -162,7 +162,7 @@ describe(' ', () => {
const { exists, find } = testBed;
expect(exists('pipelineLoadError')).toBe(true);
- expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines.');
+ expect(find('pipelineLoadError').text()).toContain('Unable to load pipelines');
});
});
});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx
index 1cca7a0721fbc09..da8f74e1efae59d 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx
@@ -6,7 +6,7 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageContent } from '@elastic/eui';
+import { EuiPageContent, EuiEmptyPrompt } from '@elastic/eui';
import React, { FunctionComponent } from 'react';
import { Router, Switch, Route } from 'react-router-dom';
@@ -19,7 +19,6 @@ import {
useAuthorizationContext,
WithPrivileges,
SectionLoading,
- NotAuthorizedSection,
} from '../shared_imports';
import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections';
@@ -61,35 +60,42 @@ export const App: FunctionComponent = () => {
{({ isLoading, hasPrivileges, privilegesMissing }) => {
if (isLoading) {
return (
-
-
-
+
+
+
+
+
);
}
if (!hasPrivileges) {
return (
-
-
+
+
+
+
}
- message={
-
+ body={
+
+
+
}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
index c51ed94cbc11666..f68b64cc5f61360 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx
@@ -8,6 +8,7 @@
import React, { FunctionComponent, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
+import { EuiPageContent } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { SectionLoading, useKibana, attemptToURIDecode } from '../../../shared_imports';
@@ -45,12 +46,14 @@ export const PipelinesClone: FunctionComponent>
if (isLoading && isInitialRequest) {
return (
-
-
-
+
+
+
+
+
);
} else {
// We still show the create form even if we were not able to load the
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
index eefc74e2dc6fad9..5aa9205e1e1e555 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx
@@ -8,15 +8,7 @@
import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
- EuiButtonEmpty,
- EuiSpacer,
-} from '@elastic/eui';
+import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { getListPath } from '../../services/navigation';
import { Pipeline } from '../../../../common/types';
@@ -64,49 +56,43 @@ export const PipelinesCreate: React.FunctionComponent
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
+ >
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
index 6011a36d292facc..ea47f4c9a25e920 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx
@@ -9,16 +9,14 @@ import React, { useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiPageBody,
+ EuiPageHeader,
+ EuiEmptyPrompt,
EuiPageContent,
EuiSpacer,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
-import { EuiCallOut } from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
import { useKibana, SectionLoading, attemptToURIDecode } from '../../../shared_imports';
@@ -42,7 +40,9 @@ export const PipelinesEdit: React.FunctionComponent {
setIsSaving(true);
@@ -68,88 +68,92 @@ export const PipelinesEdit: React.FunctionComponent
-
-
+ return (
+
+
+
+
+
);
- } else if (error) {
- content = (
- <>
-
+
+
+
+
}
- color="danger"
- iconType="alert"
- data-test-subj="fetchPipelineError"
- >
- {error.message}
-
-
- >
+ body={{error.message}
}
+ actions={
+
+
+
+ }
+ />
+
);
- } else if (pipeline) {
- content = (
+ }
+
+ return (
+ <>
+
+
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
- );
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {content}
-
-
+ >
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
index 7d69bd3fb8cf372..9f401bca5431f25 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx
@@ -8,7 +8,7 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt, EuiLink, EuiPageBody, EuiPageContent, EuiButton } from '@elastic/eui';
+import { EuiEmptyPrompt, EuiLink, EuiPageContent, EuiButton } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { ScopedHistory } from 'kibana/public';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
@@ -20,46 +20,43 @@ export const EmptyList: FunctionComponent = () => {
const history = useHistory() as ScopedHistory;
return (
-
-
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
- defaultMessage: 'Start by creating a pipeline',
+
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', {
+ defaultMessage: 'Start by creating a pipeline',
+ })}
+
+ }
+ body={
+
+ {' '}
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', {
+ defaultMessage: 'Learn more',
})}
-
- }
- body={
-
-
-
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptDocumentionLink', {
- defaultMessage: 'Learn more',
- })}
-
-
- }
- actions={
-
- {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', {
- defaultMessage: 'Create a pipeline',
- })}
-
- }
- />
-
-
+
+
+ }
+ actions={
+
+ {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', {
+ defaultMessage: 'Create a pipeline',
+ })}
+
+ }
+ />
+
);
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
index 454747fe0870e4a..ae68cfcb399f092 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx
@@ -12,16 +12,12 @@ import { Location } from 'history';
import { parse } from 'query-string';
import {
- EuiPageBody,
- EuiPageContent,
- EuiTitle,
- EuiFlexGroup,
- EuiFlexItem,
+ EuiPageHeader,
EuiButtonEmpty,
- EuiCallOut,
- EuiLink,
+ EuiPageContent,
+ EuiEmptyPrompt,
+ EuiButton,
EuiSpacer,
- EuiText,
} from '@elastic/eui';
import { Pipeline } from '../../../../common/types';
@@ -81,33 +77,50 @@ export const PipelinesList: React.FunctionComponent = ({
history.push(getListPath());
};
- if (data && data.length === 0) {
- return ;
+ if (error) {
+ return (
+
+
+
+
+ }
+ body={{error.message}
}
+ actions={
+
+
+
+ }
+ />
+
+ );
}
- let content: React.ReactNode;
-
if (isLoading) {
- content = (
-
-
-
- );
- } else if (data?.length) {
- content = (
-
+ return (
+
+
+
+
+
);
}
+ if (data && data.length === 0) {
+ return ;
+ }
+
const renderFlyout = (): React.ReactNode => {
if (!showFlyout) {
return;
@@ -134,71 +147,47 @@ export const PipelinesList: React.FunctionComponent = ({
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Error call out for pipeline table */}
- {error ? (
-
-
-
- ),
- }}
- />
- }
+
+
- ) : (
- content
- )}
-
-
+
+ }
+ description={
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
+
+
{renderFlyout()}
{pipelinesToDelete?.length > 0 ? (
+ a
+ ,
"displayAsText": "a",
"id": "a",
},
@@ -163,7 +167,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
@@ -200,7 +208,11 @@ exports[`DatatableComponent it renders actions column when there are row actions
},
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
@@ -360,7 +372,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "a",
+ "display":
+ a
+
,
"displayAsText": "a",
"id": "a",
},
@@ -397,7 +413,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
@@ -434,7 +454,11 @@ exports[`DatatableComponent it renders the title and value 1`] = `
},
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
@@ -463,7 +487,7 @@ exports[`DatatableComponent it renders the title and value 1`] = `
`;
-exports[`DatatableComponent it should not render actions on header when it is in read only mode 1`] = `
+exports[`DatatableComponent it should render hide and reset actions on header even when it is in read only mode 1`] = `
+ a
+ ,
"displayAsText": "a",
"id": "a",
},
Object {
"actions": Object {
- "additional": Array [],
+ "additional": Array [
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableResetWidth",
+ "iconType": "empty",
+ "isDisabled": true,
+ "label": "Reset width",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ ],
"showHide": false,
"showMoveLeft": false,
"showMoveRight": false,
@@ -580,13 +646,36 @@ exports[`DatatableComponent it should not render actions on header when it is in
"showSortDesc": false,
},
"cellActions": undefined,
- "display": "b",
+ "display":
+ b
+
,
"displayAsText": "b",
"id": "b",
},
Object {
"actions": Object {
- "additional": Array [],
+ "additional": Array [
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableResetWidth",
+ "iconType": "empty",
+ "isDisabled": true,
+ "label": "Reset width",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
+ ],
"showHide": false,
"showMoveLeft": false,
"showMoveRight": false,
@@ -594,7 +683,11 @@ exports[`DatatableComponent it should not render actions on header when it is in
"showSortDesc": false,
},
"cellActions": undefined,
- "display": "c",
+ "display":
+ c
+
,
"displayAsText": "c",
"id": "c",
},
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
index 5c53d40f999b773..4372e2cd9e9640d 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
@@ -35,7 +35,8 @@ export const createGridColumns = (
visibleColumns: string[],
formatFactory: FormatFactory,
onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void,
- onColumnHide: (eventData: { columnId: string }) => void
+ onColumnHide: (eventData: { columnId: string }) => void,
+ alignments: Record
) => {
const columnsReverseLookup = table.columns.reduce<
Record
@@ -151,31 +152,33 @@ export const createGridColumns = (
const additionalActions: EuiListGroupItemProps[] = [];
- if (!isReadOnly) {
+ additionalActions.push({
+ color: 'text',
+ size: 'xs',
+ onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }),
+ iconType: 'empty',
+ label: i18n.translate('xpack.lens.table.resize.reset', {
+ defaultMessage: 'Reset width',
+ }),
+ 'data-test-subj': 'lensDatatableResetWidth',
+ isDisabled: initialWidth == null,
+ });
+ if (!isTransposed) {
additionalActions.push({
color: 'text',
size: 'xs',
- onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }),
- iconType: 'empty',
- label: i18n.translate('xpack.lens.table.resize.reset', {
- defaultMessage: 'Reset width',
+ onClick: () => onColumnHide({ columnId: originalColumnId || field }),
+ iconType: 'eyeClosed',
+ label: i18n.translate('xpack.lens.table.hide.hideLabel', {
+ defaultMessage: 'Hide',
}),
- 'data-test-subj': 'lensDatatableResetWidth',
- isDisabled: initialWidth == null,
+ 'data-test-subj': 'lensDatatableHide',
+ isDisabled: !isHidden && visibleColumns.length <= 1,
});
- if (!isTransposed) {
- additionalActions.push({
- color: 'text',
- size: 'xs',
- onClick: () => onColumnHide({ columnId: originalColumnId || field }),
- iconType: 'eyeClosed',
- label: i18n.translate('xpack.lens.table.hide.hideLabel', {
- defaultMessage: 'Hide',
- }),
- 'data-test-subj': 'lensDatatableHide',
- isDisabled: !isHidden && visibleColumns.length <= 1,
- });
- } else if (columnArgs?.bucketValues) {
+ }
+
+ if (!isReadOnly) {
+ if (isTransposed && columnArgs?.bucketValues) {
const bucketValues = columnArgs?.bucketValues;
additionalActions.push({
color: 'text',
@@ -200,11 +203,13 @@ export const createGridColumns = (
});
}
}
+ const currentAlignment = alignments && alignments[field];
+ const alignmentClassName = `lnsTableCell--${currentAlignment}`;
const columnDefinition: EuiDataGridColumn = {
id: field,
cellActions,
- display: name,
+ display: {name}
,
displayAsText: name,
actions: {
showHide: false,
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
index 49003af28f3f1ba..3479a9e964d53ef 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
-import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types';
+import { FramePublicAPI, Operation, VisualizationDimensionEditorProps } from '../../types';
import { DatatableVisualizationState } from '../visualization';
import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks';
import { mountWithIntl } from '@kbn/test/jest';
@@ -213,6 +213,22 @@ describe('data table dimension editor', () => {
expect(instance.find(PalettePanelContainer).exists()).toBe(true);
});
+ it('should not show the dynamic coloring option for a bucketed operation', () => {
+ frame.activeData!.first.columns[0].meta.type = 'number';
+ frame.datasourceLayers.first.getOperationForColumnId = jest.fn(
+ () => ({ isBucketed: true } as Operation)
+ );
+ state.columns[0].colorMode = 'cell';
+ const instance = mountWithIntl( );
+
+ expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_groups"]').exists()).toBe(
+ false
+ );
+ expect(instance.find('[data-test-subj="lnsDatatable_dynamicColoring_palette"]').exists()).toBe(
+ false
+ );
+ });
+
it('should show the summary field for non numeric columns', () => {
const instance = mountWithIntl( );
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_function"]').exists()).toBe(
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
index 6c39a04ae1504cb..cf15df07ec72cab 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
@@ -104,6 +104,11 @@ export function TableDimensionEditor(
currentData
);
+ const datasource = frame.datasourceLayers[state.layerId];
+ const showDynamicColoringFeature = Boolean(
+ isNumeric && !datasource?.getOperationForColumnId(accessor)?.isBucketed
+ );
+
const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length;
const hasTransposedColumn = state.columns.some(({ isTransposed }) => isTransposed);
@@ -260,7 +265,7 @@ export function TableDimensionEditor(
)}
>
)}
- {isNumeric && (
+ {showDynamicColoringFeature && (
<>
{
).toMatchSnapshot();
});
- test('it should not render actions on header when it is in read only mode', () => {
+ test('it should render hide and reset actions on header even when it is in read only mode', () => {
const { data, args } = sampleArgs();
expect(
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
index cd990149fdaf553..b48cb94563d3b05 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
@@ -180,34 +180,6 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[onEditAction, setColumnConfig, columnConfig]
);
- const columns: EuiDataGridColumn[] = useMemo(
- () =>
- createGridColumns(
- bucketColumns,
- firstLocalTable,
- handleFilterClick,
- handleTransposedColumnClick,
- isReadOnlySorted,
- columnConfig,
- visibleColumns,
- formatFactory,
- onColumnResize,
- onColumnHide
- ),
- [
- bucketColumns,
- firstLocalTable,
- handleFilterClick,
- handleTransposedColumnClick,
- isReadOnlySorted,
- columnConfig,
- visibleColumns,
- formatFactory,
- onColumnResize,
- onColumnHide,
- ]
- );
-
const isNumericMap: Record = useMemo(() => {
const numericMap: Record = {};
for (const column of firstLocalTable.columns) {
@@ -237,6 +209,36 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
);
}, [firstTable, isNumericMap, columnConfig]);
+ const columns: EuiDataGridColumn[] = useMemo(
+ () =>
+ createGridColumns(
+ bucketColumns,
+ firstLocalTable,
+ handleFilterClick,
+ handleTransposedColumnClick,
+ isReadOnlySorted,
+ columnConfig,
+ visibleColumns,
+ formatFactory,
+ onColumnResize,
+ onColumnHide,
+ alignments
+ ),
+ [
+ bucketColumns,
+ firstLocalTable,
+ handleFilterClick,
+ handleTransposedColumnClick,
+ isReadOnlySorted,
+ columnConfig,
+ visibleColumns,
+ formatFactory,
+ onColumnResize,
+ onColumnHide,
+ alignments,
+ ]
+ );
+
const trailingControlColumns: EuiDataGridControlColumn[] = useMemo(() => {
if (!hasAtLeastOneRowClickAction || !onRowContextMenuClick) {
return [];
@@ -278,9 +280,13 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[formatters, columnConfig, props.uiSettings]
);
- const columnVisibility = useMemo(() => ({ visibleColumns, setVisibleColumns: () => {} }), [
- visibleColumns,
- ]);
+ const columnVisibility = useMemo(
+ () => ({
+ visibleColumns,
+ setVisibleColumns: () => {},
+ }),
+ [visibleColumns]
+ );
const sorting = useMemo(
() => createGridSortingConfig(sortBy, sortDirection as LensGridDirection, onEditAction),
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
index ba0e09bdd894cb8..0c3a992e3dd7abc 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx
@@ -297,7 +297,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
),
append:
- v.selection.dataLoss !== 'nothing' || v.showBetaBadge ? (
+ v.selection.dataLoss !== 'nothing' || v.showExperimentalBadge ? (
) : null}
- {v.showBetaBadge ? (
+ {v.showExperimentalBadge ? (
diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
index 54f9c708248313b..fce5bf30f47ed5b 100644
--- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx
@@ -88,7 +88,7 @@ export const getHeatmapVisualization = ({
defaultMessage: 'Heatmap',
}),
groupLabel: groupLabelForBar,
- showBetaBadge: true,
+ showExperimentalBadge: true,
},
],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index ba3bee415f3f497..1ae2f4421a0bc9c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -1797,12 +1797,14 @@ describe('state_helpers', () => {
it('should promote the inner references when switching away from reference to field-based operation (case a2)', () => {
const expectedCol = {
- label: 'Count of records',
+ label: 'Count of records -3h',
dataType: 'number' as const,
isBucketed: false,
operationType: 'count' as const,
sourceField: 'Records',
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
};
const layer: IndexPatternLayer = {
indexPatternId: '1',
@@ -1817,6 +1819,8 @@ describe('state_helpers', () => {
// @ts-expect-error not a valid type
operationType: 'testReference',
references: ['col1'],
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
},
},
};
@@ -1845,6 +1849,8 @@ describe('state_helpers', () => {
isBucketed: false,
sourceField: 'bytes',
operationType: 'average' as const,
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
};
const layer: IndexPatternLayer = {
@@ -1858,6 +1864,8 @@ describe('state_helpers', () => {
isBucketed: false,
operationType: 'differences',
references: ['metric'],
+ filter: { language: 'kuery', query: 'bytes > 4000' },
+ timeShift: '3h',
},
},
};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index b650a2818b2d484..4e3bcec4b6ca206 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -414,11 +414,21 @@ export function replaceColumn({
indexPattern,
});
+ const column = copyCustomLabel({ ...referenceColumn }, previousColumn);
+ // do not forget to move over also any filter/shift/anything (if compatible)
+ // from the reference definition to the new operation.
+ if (referencedOperation.filterable) {
+ column.filter = (previousColumn as ReferenceBasedIndexPatternColumn).filter;
+ }
+ if (referencedOperation.shiftable) {
+ column.timeShift = (previousColumn as ReferenceBasedIndexPatternColumn).timeShift;
+ }
+
tempLayer = {
...tempLayer,
columns: {
...tempLayer.columns,
- [columnId]: copyCustomLabel({ ...referenceColumn }, previousColumn),
+ [columnId]: column,
},
};
return updateDefaultLabels(
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index b421d57dae6e1f3..7baba15f0fac6a1 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -49,6 +49,7 @@ export interface EditorFrameProps {
initialContext?: VisualizeFieldContext;
showNoDataPopover: () => void;
}
+
export interface EditorFrameInstance {
EditorFrameContainer: (props: EditorFrameProps) => React.ReactElement;
}
@@ -570,9 +571,9 @@ export interface VisualizationType {
*/
sortPriority?: number;
/**
- * Indicates if visualization is in the beta stage.
+ * Indicates if visualization is in the experimental stage.
*/
- showBetaBadge?: boolean;
+ showExperimentalBadge?: boolean;
}
export interface Visualization {
@@ -734,6 +735,7 @@ interface LensEditContextMapping {
[LENS_EDIT_RESIZE_ACTION]: LensResizeActionData;
[LENS_TOGGLE_ACTION]: LensToggleActionData;
}
+
type LensEditSupportedActions = keyof LensEditContextMapping;
export type LensEditPayload = {
@@ -746,6 +748,7 @@ export interface LensEditEvent {
name: 'edit';
data: EditPayloadContext;
}
+
export interface LensTableRowContextMenuEvent {
name: 'tableRowContextMenuClick';
data: RowClickContext['data'];
diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx
index 047c95846cd278a..2e5fdf8493e241b 100644
--- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.test.tsx
@@ -33,6 +33,7 @@ describe('Axes Settings', () => {
toggleTickLabelsVisibility: jest.fn(),
toggleGridlinesVisibility: jest.fn(),
hasBarOrAreaOnAxis: false,
+ hasPercentageAxis: false,
};
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
index 43ebc91f533a402..a0d1dae2145d5a5 100644
--- a/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/axis_settings_popover.tsx
@@ -31,6 +31,7 @@ import { ToolbarButtonProps } from '../../../../../src/plugins/kibana_react/publ
import { validateExtent } from './axes_configuration';
type AxesSettingsConfigKeys = keyof AxesSettingsConfig;
+
export interface AxisSettingsPopoverProps {
/**
* Determines the axis
@@ -93,8 +94,10 @@ export interface AxisSettingsPopoverProps {
*/
setExtent?: (extent: AxisExtentConfig | undefined) => void;
hasBarOrAreaOnAxis: boolean;
+ hasPercentageAxis: boolean;
dataBounds?: { min: number; max: number };
}
+
const popoverConfig = (
axis: AxesSettingsConfigKeys,
isHorizontal: boolean
@@ -168,6 +171,7 @@ export const AxisSettingsPopover: React.FunctionComponent {
const isHorizontal = layers?.length ? isHorizontalChart(layers) : false;
@@ -333,10 +337,13 @@ export const AxisSettingsPopover: React.FunctionComponent {
const newMode = id.replace(idPrefix, '') as AxisExtentConfig['mode'];
@@ -350,7 +357,7 @@ export const AxisSettingsPopover: React.FunctionComponent
- {localExtent.mode === 'custom' && (
+ {localExtent.mode === 'custom' && !hasPercentageAxis && (
<>
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index b3d1f8f062b7305..04533f6c914e13f 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -36,6 +36,7 @@ import {
YAxisMode,
AxesSettingsConfig,
AxisExtentConfig,
+ XYState,
} from './types';
import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
import { trackUiEvent } from '../lens_ui_telemetry';
@@ -162,6 +163,18 @@ const getDataBounds = function (
return groups;
};
+function hasPercentageAxis(axisGroups: GroupsConfiguration, groupId: string, state: XYState) {
+ return Boolean(
+ axisGroups
+ .find((group) => group.groupId === groupId)
+ ?.series.some(({ layer: layerId }) =>
+ state?.layers.find(
+ (layer) => layer.layerId === layerId && layer.seriesType.includes('percentage')
+ )
+ )
+ );
+}
+
export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProps) {
const { state, setState, frame } = props;
@@ -377,6 +390,7 @@ export const XyToolbar = memo(function XyToolbar(props: VisualizationToolbarProp
setExtent={setLeftExtent}
hasBarOrAreaOnAxis={hasBarOrAreaOnLeftAxis}
dataBounds={dataBounds.left}
+ hasPercentageAxis={hasPercentageAxis(axisGroups, 'left', state)}
/>
group.groupId === 'right') || {}).length ===
0
}
+ hasPercentageAxis={hasPercentageAxis(axisGroups, 'right', state)}
isAxisTitleVisible={axisTitlesVisibilitySettings.yRight}
toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange}
extent={state?.yRightExtent || { mode: 'full' }}
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
index 8033f8d187fd514..edadc20a595ce84 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts
@@ -374,11 +374,19 @@ export function clamp(val: number, min: number, max: number): number {
export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent {
const width = bounds.maxLon - bounds.minLon;
const height = bounds.maxLat - bounds.minLat;
+
+ const newMinLon = bounds.minLon - width * scaleFactor;
+ const nexMaxLon = bounds.maxLon + width * scaleFactor;
+
+ const lonDelta = nexMaxLon - newMinLon;
+ const left = lonDelta > 360 ? -180 : newMinLon;
+ const right = lonDelta > 360 ? 180 : nexMaxLon;
+
return {
- minLon: bounds.minLon - width * scaleFactor,
- minLat: bounds.minLat - height * scaleFactor,
- maxLon: bounds.maxLon + width * scaleFactor,
- maxLat: bounds.maxLat + height * scaleFactor,
+ minLon: left,
+ minLat: clampToLatBounds(bounds.minLat - height * scaleFactor),
+ maxLon: right,
+ maxLat: clampToLonBounds(bounds.maxLat + height * scaleFactor),
};
}
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
index d828aca4a1a0019..7aef32dfb4f8a5c 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.test.ts
@@ -23,11 +23,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -89,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-89, 39],
- bottom_right: [-83, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-89, 39],
+ bottom_right: [-83, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -39,11 +59,31 @@ describe('createExtentFilter', () => {
minLat: -100,
minLon: -190,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-180, 89],
- bottom_right: [180, -89],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-180, 89],
+ bottom_right: [180, -89],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -55,11 +95,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: 100,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [100, 39],
- bottom_right: [-160, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [100, 39],
+ bottom_right: [-160, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -71,11 +131,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -200,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [160, 39],
- bottom_right: [-100, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [160, 39],
+ bottom_right: [-100, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -87,11 +167,31 @@ describe('createExtentFilter', () => {
minLat: 35,
minLon: -191,
};
- const filter = createExtentFilter(mapExtent, [geoFieldName]);
- expect(filter.geo_bounding_box).toEqual({
- location: {
- top_left: [-180, 39],
- bottom_right: [180, 35],
+ expect(createExtentFilter(mapExtent, [geoFieldName])).toEqual({
+ meta: {
+ alias: null,
+ disabled: false,
+ key: 'location',
+ negate: false,
+ },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'location',
+ },
+ },
+ {
+ geo_bounding_box: {
+ location: {
+ top_left: [-180, 39],
+ bottom_right: [180, 35],
+ },
+ },
+ },
+ ],
+ },
},
});
});
@@ -184,23 +284,36 @@ describe('createSpatialFilterWithGeometry', () => {
negate: false,
type: 'spatial_filter',
},
- geo_shape: {
- 'geo.coordinates': {
- relation: 'INTERSECTS',
- shape: {
- coordinates: [
- [
- [-101.21639, 48.1413],
- [-101.21639, 41.84905],
- [-90.95149, 41.84905],
- [-90.95149, 48.1413],
- [-101.21639, 48.1413],
- ],
- ],
- type: 'Polygon',
- },
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'geo.coordinates',
+ },
+ },
+ {
+ geo_shape: {
+ 'geo.coordinates': {
+ relation: 'INTERSECTS',
+ shape: {
+ coordinates: [
+ [
+ [-101.21639, 48.1413],
+ [-101.21639, 41.84905],
+ [-90.95149, 41.84905],
+ [-90.95149, 48.1413],
+ [-101.21639, 48.1413],
+ ],
+ ],
+ type: 'Polygon',
+ },
+ },
+ ignore_unmapped: true,
+ },
+ },
+ ],
},
- ignore_unmapped: true,
},
});
});
@@ -318,9 +431,22 @@ describe('createDistanceFilterWithMeta', () => {
negate: false,
type: 'spatial_filter',
},
- geo_distance: {
- distance: '1000km',
- 'geo.coordinates': [120, 30],
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'geo.coordinates',
+ },
+ },
+ {
+ geo_distance: {
+ distance: '1000km',
+ 'geo.coordinates': [120, 30],
+ },
+ },
+ ],
+ },
},
});
});
diff --git a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
index 70df9e9646f50f5..9a2b2c21136dfbf 100644
--- a/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
+++ b/x-pack/plugins/maps/common/elasticsearch_util/spatial_filter_utils.ts
@@ -31,13 +31,23 @@ function createMultiGeoFieldFilter(
}
if (geoFieldNames.length === 1) {
- const geoFilter = createGeoFilter(geoFieldNames[0]);
return {
meta: {
...meta,
key: geoFieldNames[0],
},
- ...geoFilter,
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: geoFieldNames[0],
+ },
+ },
+ createGeoFilter(geoFieldNames[0]),
+ ],
+ },
+ },
};
}
@@ -201,8 +211,9 @@ export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] {
}
} else {
const geoFieldName = filter.meta.key;
- if (geoFieldName) {
- geometry = extractGeometryFromFilter(geoFieldName, filter);
+ const spatialClause = filter?.query?.bool?.must?.[1];
+ if (geoFieldName && spatialClause) {
+ geometry = extractGeometryFromFilter(geoFieldName, spatialClause);
}
}
diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js
index 77ce23594447f8b..fa69dad6167478e 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.test.js
+++ b/x-pack/plugins/maps/public/actions/map_actions.test.js
@@ -33,7 +33,9 @@ describe('map_actions', () => {
describe('store mapState is empty', () => {
beforeEach(() => {
require('../selectors/map_selectors').getDataFilters = () => {
- return {};
+ return {
+ zoom: 5,
+ };
};
require('../selectors/map_selectors').getLayerList = () => {
@@ -61,11 +63,13 @@ describe('map_actions', () => {
minLat: 5,
minLon: 95,
},
+ zoom: 5,
});
await action(dispatchMock, getStoreMock);
expect(dispatchMock).toHaveBeenCalledWith({
mapState: {
+ zoom: 5,
extent: {
maxLat: 10,
maxLon: 100,
@@ -73,10 +77,10 @@ describe('map_actions', () => {
minLon: 95,
},
buffer: {
- maxLat: 12.5,
- maxLon: 102.5,
- minLat: 2.5,
- minLon: 92.5,
+ maxLat: 21.94305,
+ maxLon: 112.5,
+ minLat: 0,
+ minLon: 90,
},
},
type: 'MAP_EXTENT_CHANGED',
@@ -154,10 +158,10 @@ describe('map_actions', () => {
minLon: 85,
},
buffer: {
- maxLat: 7.5,
- maxLon: 92.5,
- minLat: -2.5,
- minLon: 82.5,
+ maxLat: 7.71099,
+ maxLon: 92.8125,
+ minLat: -2.81137,
+ minLon: 82.26563,
},
},
type: 'MAP_EXTENT_CHANGED',
@@ -186,10 +190,10 @@ describe('map_actions', () => {
minLon: 96,
},
buffer: {
- maxLat: 13.5,
- maxLon: 103.5,
- minLat: 3.5,
- minLon: 93.5,
+ maxLat: 13.58192,
+ maxLon: 103.53516,
+ minLat: 3.33795,
+ minLon: 93.33984,
},
},
type: 'MAP_EXTENT_CHANGED',
diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts
index 42ce96d102d7e0e..3cdc5bf05ccee7d 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.ts
+++ b/x-pack/plugins/maps/public/actions/map_actions.ts
@@ -56,6 +56,7 @@ import {
import { INITIAL_LOCATION } from '../../common/constants';
import { scaleBounds } from '../../common/elasticsearch_util';
import { cleanTooltipStateForLayer } from './tooltip_actions';
+import { expandToTileBoundaries } from '../../common/geo_tile_utils';
export interface MapExtentState {
zoom: number;
@@ -158,7 +159,9 @@ export function mapExtentChanged(mapExtentState: MapExtentState) {
}
if (!doesBufferContainExtent || currentZoom !== newZoom) {
- dataFilters.buffer = scaleBounds(extent, 0.5);
+ const expandedExtent = scaleBounds(extent, 0.5);
+ // snap to the smallest tile-bounds, to avoid jitter in the bounds
+ dataFilters.buffer = expandToTileBoundaries(expandedExtent, Math.ceil(newZoom));
}
}
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
index ad413419a289b10..1043ed877830401 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts
@@ -205,13 +205,31 @@ describe('ESGeoGridSource', () => {
expect(getProperty('query')).toEqual(undefined);
expect(getProperty('filter')).toEqual([
{
- geo_bounding_box: { bar: { bottom_right: [180, -82.67628], top_left: [-180, 82.67628] } },
meta: {
alias: null,
disabled: false,
key: 'bar',
negate: false,
},
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'bar',
+ },
+ },
+ {
+ geo_bounding_box: {
+ bar: {
+ top_left: [-180, 82.67628],
+ bottom_right: [180, -82.67628],
+ },
+ },
+ },
+ ],
+ },
+ },
},
]);
expect(getProperty('aggs')).toEqual({
@@ -262,20 +280,33 @@ describe('ESGeoGridSource', () => {
});
describe('ITiledSingleLayerVectorSource', () => {
+ const mvtGeogridSource = new ESGeoGridSource(
+ {
+ id: 'foobar',
+ indexPatternId: 'fooIp',
+ geoField: geoFieldName,
+ metrics: [],
+ resolution: GRID_RESOLUTION.SUPER_FINE,
+ type: SOURCE_TYPES.ES_GEO_GRID,
+ requestType: RENDER_AS.HEATMAP,
+ },
+ {}
+ );
+
it('getLayerName', () => {
- expect(geogridSource.getLayerName()).toBe('source_layer');
+ expect(mvtGeogridSource.getLayerName()).toBe('source_layer');
});
it('getMinZoom', () => {
- expect(geogridSource.getMinZoom()).toBe(0);
+ expect(mvtGeogridSource.getMinZoom()).toBe(0);
});
it('getMaxZoom', () => {
- expect(geogridSource.getMaxZoom()).toBe(24);
+ expect(mvtGeogridSource.getMaxZoom()).toBe(24);
});
it('getUrlTemplateWithMeta', async () => {
- const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta(
+ const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta(
vectorSourceRequestMeta
);
@@ -283,19 +314,19 @@ describe('ESGeoGridSource', () => {
expect(urlTemplateWithMeta.minSourceZoom).toBe(0);
expect(urlTemplateWithMeta.maxSourceZoom).toBe(24);
expect(urlTemplateWithMeta.urlTemplate).toEqual(
- "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
+ "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
- const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta({
+ const urlTemplateWithMeta = await mvtGeogridSource.getUrlTemplateWithMeta({
...vectorSourceRequestMeta,
searchSessionId: '1',
});
expect(
urlTemplateWithMeta.urlTemplate.startsWith(
- "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
+ "rootdir/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1"
)
).toBe(true);
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
index 7ed24b4805997a6..86dd63be4b67b82 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
@@ -142,6 +142,20 @@ export class ESPewPewSource extends AbstractESAggSource {
},
});
+ // pewpew source is often used with security solution index-pattern
+ // Some underlying indices may not contain geo fields
+ // Filter out documents without geo fields to avoid shard failures for those indices
+ searchSource.setField('filter', [
+ ...searchSource.getField('filter'),
+ // destGeoField exists ensured by buffer filter
+ // so only need additional check for sourceGeoField
+ {
+ exists: {
+ field: this._descriptor.sourceGeoField,
+ },
+ },
+ ]);
+
const esResponse = await this._runEsQuery({
requestId: this.getId(),
requestName: layerName,
diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts
index 776d316440a5617..82d2162986503bc 100644
--- a/x-pack/plugins/maps/server/mvt/get_tile.ts
+++ b/x-pack/plugins/maps/server/mvt/get_tile.ts
@@ -24,6 +24,7 @@ import {
} from '../../common/constants';
import {
+ createExtentFilter,
convertRegularRespToGeoJson,
hitsToGeoJson,
isTotalHitsGreaterThan,
@@ -254,21 +255,14 @@ export async function getTile({
}
function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown {
- return {
- geo_shape: {
- [geometryFieldName]: {
- shape: {
- type: 'envelope',
- // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]]
- coordinates: [
- [tileBounds.top_left.lon, tileBounds.top_left.lat],
- [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat],
- ],
- },
- relation: 'INTERSECTS',
- },
- },
+ const tileExtent = {
+ minLon: tileBounds.top_left.lon,
+ minLat: tileBounds.bottom_right.lat,
+ maxLon: tileBounds.bottom_right.lon,
+ maxLat: tileBounds.top_left.lat,
};
+ const tileExtentFilter = createExtentFilter(tileExtent, [geometryFieldName]);
+ return tileExtentFilter.query;
}
function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon {
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/logo.json
new file mode 100755
index 000000000000000..862f970b7405dbc
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/logo.json
@@ -0,0 +1,3 @@
+{
+ "icon": "logoSecurity"
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json
new file mode 100755
index 000000000000000..480f49f3f2b1981
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json
@@ -0,0 +1,77 @@
+{
+ "id": "security_auth",
+ "title": "Security: Authentication",
+ "description": "Detect anomalous activity in your ECS-compatible authentication logs.",
+ "type": "auth data",
+ "logoFile": "logo.json",
+ "defaultIndexPattern": "auditbeat-*,logs-*,filebeat-*,winlogbeat-*",
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ }
+ ]
+ }
+ },
+ "jobs": [
+ {
+ "id": "auth_high_count_logon_events_for_a_source_ip",
+ "file": "auth_high_count_logon_events_for_a_source_ip.json"
+ },
+ {
+ "id": "auth_high_count_logon_fails",
+ "file": "auth_high_count_logon_fails.json"
+ },
+ {
+ "id": "auth_high_count_logon_events",
+ "file": "auth_high_count_logon_events.json"
+ },
+ {
+ "id": "auth_rare_hour_for_a_user",
+ "file": "auth_rare_hour_for_a_user.json"
+ },
+ {
+ "id": "auth_rare_source_ip_for_a_user",
+ "file": "auth_rare_source_ip_for_a_user.json"
+ },
+ {
+ "id": "auth_rare_user",
+ "file": "auth_rare_user.json"
+ }
+ ],
+ "datafeeds": [
+ {
+ "id": "datafeed-auth_high_count_logon_events_for_a_source_ip",
+ "file": "datafeed_auth_high_count_logon_events_for_a_source_ip.json",
+ "job_id": "auth_high_count_logon_events_for_a_source_ip"
+ },
+ {
+ "id": "datafeed-auth_high_count_logon_fails",
+ "file": "datafeed_auth_high_count_logon_fails.json",
+ "job_id": "auth_high_count_logon_fails"
+ },
+ {
+ "id": "datafeed-auth_high_count_logon_events",
+ "file": "datafeed_auth_high_count_logon_events.json",
+ "job_id": "auth_high_count_logon_events"
+ },
+ {
+ "id": "datafeed-auth_rare_hour_for_a_user",
+ "file": "datafeed_auth_rare_hour_for_a_user.json",
+ "job_id": "auth_rare_hour_for_a_user"
+ },
+ {
+ "id": "datafeed-auth_rare_source_ip_for_a_user",
+ "file": "datafeed_auth_rare_source_ip_for_a_user.json",
+ "job_id": "auth_rare_source_ip_for_a_user"
+ },
+ {
+ "id": "datafeed-auth_rare_user",
+ "file": "datafeed_auth_rare_user.json",
+ "job_id": "auth_rare_user"
+ }
+ ]
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
new file mode 100644
index 000000000000000..ee84fb222bb5c7a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json
@@ -0,0 +1,29 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration or brute force activity.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high count of logon events",
+ "function": "high_non_zero_count",
+ "detector_index": 0
+ }
+ ],
+ "influencers": []
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
new file mode 100644
index 000000000000000..7bbbc81b6de7ab5
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusually large spike in successful authentication events events from a particular source IP address. This can be due to password spraying, user enumeration or brute force activity.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high count of auth events for a source IP",
+ "function": "high_non_zero_count",
+ "by_field_name": "source.ip",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "winlog.event_data.LogonType",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
new file mode 100644
index 000000000000000..4b7094e92c6ecf6
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json
@@ -0,0 +1,29 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration or brute force activity and may be a precursor to account takeover or credentialed access.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high count of logon fails",
+ "function": "high_non_zero_count",
+ "detector_index": 0
+ }
+ ],
+ "influencers": []
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json
new file mode 100644
index 000000000000000..bb86d256e59df04
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json
@@ -0,0 +1,33 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "rare hour for a user",
+ "function": "time_of_day",
+ "by_field_name": "user.name",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json
new file mode 100644
index 000000000000000..6f72e148fa38ed6
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json
@@ -0,0 +1,34 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "rare source IP for a user",
+ "function": "rare",
+ "by_field_name": "source.ip",
+ "partition_field_name": "user.name",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json
new file mode 100644
index 000000000000000..5cb9c7112b29d3f
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json
@@ -0,0 +1,33 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Security: Authentication - looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.",
+ "groups": [
+ "security",
+ "authentication"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "rare user",
+ "function": "rare",
+ "by_field_name": "user.name",
+ "detector_index": 0
+ }
+ ],
+ "influencers": [
+ "source.ip",
+ "user.name"
+ ]
+ },
+ "allow_lazy_open": true,
+ "analysis_limits": {
+ "model_memory_limit": "128mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-security-auth"
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events.json
new file mode 100644
index 000000000000000..eb81179e443637d
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_high_count_logon_events",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events_for_a_source_ip.json
new file mode 100644
index 000000000000000..dfed3ada1fe0bea
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_events_for_a_source_ip.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_high_count_logon_events_for_a_source_ip",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_fails.json
new file mode 100644
index 000000000000000..431c115b34d6047
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_high_count_logon_fails.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_high_count_logon_fails",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "failure"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_hour_for_a_user.json
new file mode 100644
index 000000000000000..377197231f28c10
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_hour_for_a_user.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_rare_hour_for_a_user",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_source_ip_for_a_user.json
new file mode 100644
index 000000000000000..dfa2ad7ab397c9a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_source_ip_for_a_user.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_rare_source_ip_for_a_user",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_user.json
new file mode 100644
index 000000000000000..f7de5d3aee71a7a
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_auth_rare_user.json
@@ -0,0 +1,26 @@
+{
+ "job_id": "auth_rare_user",
+ "indices": [
+ "auditbeat-*",
+ "logs-*",
+ "filebeat-*",
+ "winlogbeat-*"
+ ],
+ "max_empty_searches": 10,
+ "query": {
+ "bool": {
+ "filter": [
+ {
+ "term": {
+ "event.category": "authentication"
+ }
+ },
+ {
+ "term": {
+ "event.outcome": "success"
+ }
+ }
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts
index 6b24ef000b69518..48ceaefb2393949 100644
--- a/x-pack/plugins/ml/server/saved_objects/checks.ts
+++ b/x-pack/plugins/ml/server/saved_objects/checks.ts
@@ -24,7 +24,7 @@ interface JobSavedObjectStatus {
};
}
-interface JobStatus {
+export interface JobStatus {
jobId: string;
datafeedId?: string | null;
checks: {
@@ -68,7 +68,9 @@ export function checksFactory(
if (type === 'anomaly-detector') {
jobExists = adJobs.jobs.some((j) => j.job_id === jobId);
- datafeedExists = datafeeds.datafeeds.some((d) => d.job_id === jobId);
+ datafeedExists = datafeeds.datafeeds.some(
+ (d) => d.datafeed_id === datafeedId && d.job_id === jobId
+ );
} else {
jobExists = dfaJobs.data_frame_analytics.some((j) => j.id === jobId);
}
diff --git a/x-pack/plugins/ml/server/saved_objects/sync.ts b/x-pack/plugins/ml/server/saved_objects/sync.ts
index a59687b9c3cbf49..d6fa887d1d68be8 100644
--- a/x-pack/plugins/ml/server/saved_objects/sync.ts
+++ b/x-pack/plugins/ml/server/saved_objects/sync.ts
@@ -14,6 +14,7 @@ import {
InitializeSavedObjectResponse,
} from '../../common/types/saved_objects';
import { checksFactory } from './checks';
+import type { JobStatus } from './checks';
import { getSavedObjectClientError } from './util';
import { Datafeed } from '../../common/types/anomaly_detection_jobs';
@@ -45,6 +46,12 @@ export function syncSavedObjectsFactory(
const tasks: Array<() => Promise> = [];
const status = await checkStatus();
+
+ const adJobsById = status.jobs['anomaly-detector'].reduce((acc, j) => {
+ acc[j.jobId] = j;
+ return acc;
+ }, {} as Record);
+
for (const job of status.jobs['anomaly-detector']) {
if (job.checks.savedObjectExits === false) {
if (simulate === true) {
@@ -141,8 +148,16 @@ export function syncSavedObjectsFactory(
}
for (const job of status.savedObjects['anomaly-detector']) {
- if (job.checks.datafeedExists === true && job.datafeedId === null) {
+ if (
+ (job.checks.datafeedExists === true && job.datafeedId === null) ||
+ (job.checks.datafeedExists === false &&
+ job.datafeedId === null &&
+ job.checks.datafeedExists === false &&
+ adJobsById[job.jobId] &&
+ adJobsById[job.jobId].datafeedId !== job.datafeedId)
+ ) {
// add datafeed id for jobs where the datafeed exists but the id is missing from the saved object
+ // or if the datafeed id in the saved object is not the same as the one attached to the job in es
if (simulate === true) {
results.datafeedsAdded[job.jobId] = { success: true };
} else {
diff --git a/x-pack/plugins/observability/public/components/app/section/index.tsx b/x-pack/plugins/observability/public/components/app/section/index.tsx
index ddfd8ebed4f8f75..191a1b2890ada30 100644
--- a/x-pack/plugins/observability/public/components/app/section/index.tsx
+++ b/x-pack/plugins/observability/public/components/app/section/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiAccordion, EuiLink, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { EuiAccordion, EuiPanel, EuiSpacer, EuiTitle, EuiButton } from '@elastic/eui';
import React from 'react';
import { ErrorPanel } from './error_panel';
import { usePluginContext } from '../../../hooks/use_plugin_context';
@@ -25,36 +25,29 @@ interface Props {
export function SectionContainer({ title, appLink, children, hasError }: Props) {
const { core } = usePluginContext();
return (
-
- {title}
-
- }
- extraAction={
- appLink?.href && (
-
- {appLink.label}
-
- )
- }
- >
- <>
-
-
- {hasError ? (
-
- ) : (
- <>
-
- {children}
- >
- )}
-
- >
-
+
+
+ {title}
+
+ }
+ extraAction={
+ appLink?.href && (
+ {appLink.label}
+ )
+ }
+ >
+ <>
+
+
+ {hasError ? : <>{children}>}
+
+ >
+
+
);
}
diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
index c91732019f79fe1..209c224618f78e0 100644
--- a/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
+++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/list/remote_clusters_list.test.js
@@ -181,6 +181,10 @@ describe(' ', () => {
expect(exists('remoteClusterCreateButton')).toBe(true);
});
+ test('should have link to documentation', () => {
+ expect(exists('documentationLink')).toBe(true);
+ });
+
test('should list the remote clusters in the table', () => {
expect(tableCellsValues.length).toEqual(remoteClusters.length);
expect(tableCellsValues).toEqual([
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
index 1706be8cfbe2f6f..2b2e7338fb6b053 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_page_title/remote_cluster_page_title.js
@@ -5,62 +5,38 @@
* 2.0.
*/
-import React, { Fragment } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { remoteClustersUrl } from '../../../services/documentation';
-import {
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiSpacer,
- EuiText,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiPageHeader, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
export const RemoteClusterPageTitle = ({ title, description }) => (
-
-
-
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
- {description ? (
- <>
-
-
-
- {description}
-
- >
- ) : null}
-
-
-
+ <>
+ {title}}
+ rightSideItems={[
+
+
+ ,
+ ]}
+ description={description}
+ />
+
+
+ >
);
RemoteClusterPageTitle.propTypes = {
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
index 124d2d42afb7892..f62550ca5aa1073 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_add/remote_cluster_add.js
@@ -9,8 +9,6 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiPageContent } from '@elastic/eui';
-
import { extractQueryParams } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
@@ -58,11 +56,7 @@ export class RemoteClusterAdd extends PureComponent {
const { isAddingCluster, addClusterError } = this.props;
return (
-
+ <>
-
+ >
);
}
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
index 18ee2e2b3875dd5..1f3388d06e54c6c 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js
@@ -5,27 +5,17 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiButtonEmpty,
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPageContent,
- EuiSpacer,
- EuiText,
- EuiTextColor,
-} from '@elastic/eui';
+import { EuiButton, EuiCallOut, EuiEmptyPrompt, EuiPageContent, EuiSpacer } from '@elastic/eui';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { getRouter, redirect } from '../../services';
import { setBreadcrumbs } from '../../services/breadcrumb';
-import { RemoteClusterPageTitle, RemoteClusterForm, ConfiguredByNodeWarning } from '../components';
+import { RemoteClusterPageTitle, RemoteClusterForm } from '../components';
export class RemoteClusterEdit extends Component {
static propTypes = {
@@ -92,56 +82,50 @@ export class RemoteClusterEdit extends Component {
}
};
- renderContent() {
+ render() {
const { clusterName } = this.state;
const { isLoading, cluster, isEditingCluster, getEditClusterError } = this.props;
if (isLoading) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
if (!cluster) {
return (
-
-
+
+
+
+
}
- color="danger"
- iconType="alert"
- >
-
-
-
-
-
-
+
+
+ }
+ actions={
+
@@ -149,10 +133,10 @@ export class RemoteClusterEdit extends Component {
id="xpack.remoteClusters.edit.viewRemoteClustersButtonLabel"
defaultMessage="View remote clusters"
/>
-
-
-
-
+
+ }
+ />
+
);
}
@@ -160,23 +144,50 @@ export class RemoteClusterEdit extends Component {
if (isConfiguredByNode) {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
return (
<>
+
+ }
+ />
+
{hasDeprecatedProxySetting ? (
<>
>
) : null}
+
);
}
-
- render() {
- return (
-
-
- }
- />
-
- {this.renderContent()}
-
- );
- }
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
index ccf4c7568f7ad97..b94ae8f7edbc040 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_list.js
@@ -5,32 +5,24 @@
* 2.0.
*/
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
import PropTypes from 'prop-types';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
+ EuiButtonEmpty,
EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
EuiLoadingKibana,
- EuiLoadingSpinner,
EuiOverlayMask,
- EuiPageBody,
EuiPageContent,
- EuiPageContentHeader,
- EuiPageContentHeaderSection,
EuiSpacer,
- EuiText,
- EuiTextColor,
- EuiTitle,
- EuiCallOut,
+ EuiPageHeader,
} from '@elastic/eui';
+import { remoteClustersUrl } from '../../services/documentation';
import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public';
-import { extractQueryParams } from '../../../shared_imports';
+import { extractQueryParams, SectionLoading } from '../../../shared_imports';
import { setBreadcrumbs } from '../../services/breadcrumb';
import { RemoteClusterTable } from './remote_cluster_table';
@@ -82,41 +74,6 @@ export class RemoteClusterList extends Component {
clearInterval(this.interval);
}
- getHeaderSection(isAuthorized) {
- return (
-
-
-
-
-
-
-
-
-
-
- {isAuthorized && (
-
-
-
-
-
- )}
-
-
-
- );
- }
-
renderBlockingAction() {
const { isCopyingCluster, isRemovingCluster } = this.props;
@@ -132,16 +89,28 @@ export class RemoteClusterList extends Component {
}
renderNoPermission() {
- const title = i18n.translate('xpack.remoteClusters.remoteClusterList.noPermissionTitle', {
- defaultMessage: 'Permission error',
- });
return (
-
-
+
+
+
+ }
+ body={
+
+
+
+ }
/>
-
+
);
}
@@ -150,80 +119,84 @@ export class RemoteClusterList extends Component {
// handle unexpected error shapes in the API action.
const { statusCode, error: errorString } = error.body;
- const title = i18n.translate('xpack.remoteClusters.remoteClusterList.loadingErrorTitle', {
- defaultMessage: 'Error loading remote clusters',
- });
return (
-
- {statusCode} {errorString}
-
+
+
+
+
+ }
+ body={
+
+ {statusCode} {errorString}
+
+ }
+ />
+
);
}
renderEmpty() {
return (
-
-
-
- }
- body={
-
+
+
+
+
+ }
+ body={
-
- }
- actions={
-
-
-
- }
- />
+ }
+ actions={
+
+
+
+ }
+ />
+
);
}
renderLoading() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
}
@@ -231,10 +204,35 @@ export class RemoteClusterList extends Component {
const { clusters } = this.props;
return (
-
+ <>
+
+ }
+ rightSideItems={[
+
+
+ ,
+ ]}
+ />
+
+
+
-
+ >
);
}
@@ -242,7 +240,6 @@ export class RemoteClusterList extends Component {
const { isLoading, clusters, clusterLoadError } = this.props;
const isEmpty = !isLoading && !clusters.length;
const isAuthorized = !clusterLoadError || clusterLoadError.status !== 403;
- const isHeaderVisible = clusterLoadError || !isEmpty;
let content;
@@ -261,13 +258,10 @@ export class RemoteClusterList extends Component {
}
return (
-
-
- {isHeaderVisible && this.getHeaderSection(isAuthorized)}
- {content}
- {this.renderBlockingAction()}
-
-
+ <>
+ {content}
+ {this.renderBlockingAction()}
+ >
);
}
}
diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
index 3da8bb505fc543a..1404e51d98a6d43 100644
--- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
+++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js
@@ -330,6 +330,19 @@ export class RemoteClusterTable extends Component {
)}
) : undefined,
+ toolsRight: (
+
+
+
+ ),
onChange: this.onSearch,
box: {
incremental: true,
diff --git a/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js b/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
index 343237e70a120ef..1a6459627c9a130 100644
--- a/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
+++ b/x-pack/plugins/remote_clusters/public/application/store/actions/load_clusters.js
@@ -5,9 +5,7 @@
* 2.0.
*/
-import { i18n } from '@kbn/i18n';
-
-import { loadClusters as sendLoadClustersRequest, showApiError } from '../../services';
+import { loadClusters as sendLoadClustersRequest } from '../../services';
import { LOAD_CLUSTERS_START, LOAD_CLUSTERS_SUCCESS, LOAD_CLUSTERS_FAILURE } from '../action_types';
@@ -20,17 +18,10 @@ export const loadClusters = () => async (dispatch) => {
try {
clusters = await sendLoadClustersRequest();
} catch (error) {
- dispatch({
+ return dispatch({
type: LOAD_CLUSTERS_FAILURE,
payload: { error },
});
-
- return showApiError(
- error,
- i18n.translate('xpack.remoteClusters.loadAction.errorTitle', {
- defaultMessage: 'Error loading remote clusters',
- })
- );
}
dispatch({
diff --git a/x-pack/plugins/remote_clusters/public/shared_imports.ts b/x-pack/plugins/remote_clusters/public/shared_imports.ts
index fd281753186665d..c8d7f1d9f13f3d6 100644
--- a/x-pack/plugins/remote_clusters/public/shared_imports.ts
+++ b/x-pack/plugins/remote_clusters/public/shared_imports.ts
@@ -5,4 +5,8 @@
* 2.0.
*/
-export { extractQueryParams, indices } from '../../../../src/plugins/es_ui_shared/public';
+export {
+ extractQueryParams,
+ indices,
+ SectionLoading,
+} from '../../../../src/plugins/es_ui_shared/public';
diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx
index 6442c81a4718450..618c91fba071576 100644
--- a/x-pack/plugins/reporting/public/components/report_listing.tsx
+++ b/x-pack/plugins/reporting/public/components/report_listing.tsx
@@ -9,11 +9,10 @@ import {
EuiBasicTable,
EuiFlexGroup,
EuiFlexItem,
- EuiPageContent,
+ EuiPageHeader,
EuiSpacer,
EuiText,
EuiTextColor,
- EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
@@ -139,38 +138,29 @@ class ReportListingUi extends Component {
public render() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {this.renderTable()}
-
+ <>
+
+ }
+ description={
+
+ }
+ />
+
+
+ {this.renderTable()}
+
-
+ >
);
}
diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json
index 725b563c3674f33..10541d9a4ebddc7 100644
--- a/x-pack/plugins/rollup/kibana.json
+++ b/x-pack/plugins/rollup/kibana.json
@@ -5,7 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": [
- "indexPatternManagement",
"management",
"licensing",
"features"
diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts
index 17e352e1a447290..0d345e326193c70 100644
--- a/x-pack/plugins/rollup/public/plugin.ts
+++ b/x-pack/plugins/rollup/public/plugin.ts
@@ -12,14 +12,13 @@ import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_mana
import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config';
// @ts-ignore
import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config';
-import { CONFIG_ROLLUPS, UIM_APP_NAME } from '../common';
+import { UIM_APP_NAME } from '../common';
import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
} from '../../../../src/plugins/home/public';
import { ManagementSetup } from '../../../../src/plugins/management/public';
import { IndexManagementPluginSetup } from '../../index_management/public';
-import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public';
// @ts-ignore
import { setHttp, init as initDocumentation } from './crud_app/services/index';
import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services';
@@ -29,20 +28,13 @@ export interface RollupPluginSetupDependencies {
home?: HomePublicPluginSetup;
management: ManagementSetup;
indexManagement?: IndexManagementPluginSetup;
- indexPatternManagement: IndexPatternManagementSetup;
usageCollection?: UsageCollectionSetup;
}
export class RollupPlugin implements Plugin {
setup(
core: CoreSetup,
- {
- home,
- management,
- indexManagement,
- indexPatternManagement,
- usageCollection,
- }: RollupPluginSetupDependencies
+ { home, management, indexManagement, usageCollection }: RollupPluginSetupDependencies
) {
setFatalErrors(core.fatalErrors);
if (usageCollection) {
@@ -54,13 +46,6 @@ export class RollupPlugin implements Plugin {
indexManagement.extensionsService.addToggle(rollupToggleExtension);
}
- const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS);
-
- if (isRollupIndexPatternsEnabled) {
- indexPatternManagement.creation.addCreationConfig(RollupIndexPatternCreationConfig);
- indexPatternManagement.list.addListConfig(RollupIndexPatternListConfig);
- }
-
if (home) {
home.featureCatalogue.register({
id: 'rollup_jobs',
diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json
index 9b994d1710ffc26..6885081ce4bdd1a 100644
--- a/x-pack/plugins/rollup/tsconfig.json
+++ b/x-pack/plugins/rollup/tsconfig.json
@@ -16,7 +16,6 @@
"references": [
{ "path": "../../../src/core/tsconfig.json" },
// required plugins
- { "path": "../../../src/plugins/index_pattern_management/tsconfig.json" },
{ "path": "../../../src/plugins/management/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
index 04190fbf5eacddd..b1bc14cc79e6414 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: expected value of type [boolean] but got [string]"`;
+exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [2]: expected value of type [boolean] but got [string]"`;
exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`;
diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts
index d941b2d777c4fca..223aee37a904715 100644
--- a/x-pack/plugins/security/server/authorization/validate_es_response.ts
+++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts
@@ -9,6 +9,11 @@ import { schema } from '@kbn/config-schema';
import type { HasPrivilegesResponse } from './types';
+const anyBoolean = schema.boolean();
+const anyBooleanArray = schema.arrayOf(anyBoolean);
+const anyString = schema.string();
+const anyObject = schema.object({}, { unknowns: 'allow' });
+
/**
* Validates an Elasticsearch "Has privileges" response against the expected application, actions, and resources.
*
@@ -31,7 +36,6 @@ export function validateEsPrivilegeResponse(
}
function buildValidationSchema(application: string, actions: string[], resources: string[]) {
- const actionValidationSchema = schema.boolean();
const actionsValidationSchema = schema.object(
{},
{
@@ -45,9 +49,7 @@ function buildValidationSchema(application: string, actions: string[], resources
throw new Error('Payload did not match expected actions');
}
- Object.values(value).forEach((actionResult) => {
- actionValidationSchema.validate(actionResult);
- });
+ anyBooleanArray.validate(Object.values(value));
},
}
);
@@ -73,12 +75,12 @@ function buildValidationSchema(application: string, actions: string[], resources
);
return schema.object({
- username: schema.string(),
- has_all_requested: schema.boolean(),
- cluster: schema.object({}, { unknowns: 'allow' }),
+ username: anyString,
+ has_all_requested: anyBoolean,
+ cluster: anyObject,
application: schema.object({
[application]: resourcesValidationSchema,
}),
- index: schema.object({}, { unknowns: 'allow' }),
+ index: anyObject,
});
}
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
index 740437646f61a41..2fdb7e99d860e84 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/formatted_date_time.tsx
@@ -8,14 +8,10 @@
import React from 'react';
import { FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n/react';
-export const FormattedDateAndTime: React.FC<{ date: Date; showRelativeTime?: boolean }> = ({
- date,
- showRelativeTime = false,
-}) => {
+export const FormattedDateAndTime: React.FC<{ date: Date }> = ({ date }) => {
// If date is greater than or equal to 1h (ago), then show it as a date
- // and if showRelativeTime is false
// else, show it as relative to "now"
- return Date.now() - date.getTime() >= 3.6e6 && !showRelativeTime ? (
+ return Date.now() - date.getTime() >= 3.6e6 ? (
<>
{' @'}
diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx
index 8dac6234f19a865..e7199f6df2b1f55 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_modules.tsx
@@ -17,6 +17,7 @@ export const mlModules: string[] = [
'siem_packetbeat',
'siem_winlogbeat',
'siem_winlogbeat_auth',
+ 'security_auth',
'security_linux',
'security_network',
'security_windows',
diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
index fc5c19e95fb770e..d677a4a9fd662e7 100644
--- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.test.tsx
@@ -140,6 +140,22 @@ describe('when using PaginatedContent', () => {
});
});
+ it('should call onChange when page is empty', () => {
+ render({
+ pagination: {
+ pageIndex: 1,
+ pageSizeOptions: [5, 10, 20],
+ pageSize: 10,
+ totalItemCount: 10,
+ },
+ });
+ expect(onChangeHandler).toHaveBeenCalledWith({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+ expect(onChangeHandler).toHaveBeenCalledTimes(1);
+ });
+
it('should ignore items, error, noItemsMessage when `children` is used', () => {
render({ children: {'children being used here'}
});
expect(renderResult.getByTestId('custom-content')).not.toBeNull();
diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
index 890b21624eaf67a..a6b2683316efe5e 100644
--- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx
@@ -16,6 +16,7 @@ import React, {
useCallback,
useMemo,
useState,
+ useEffect,
} from 'react';
import {
CommonProps,
@@ -152,6 +153,12 @@ export const PaginatedContent = memo(
[pagination?.pageSize, pagination?.totalItemCount]
);
+ useEffect(() => {
+ if (pageCount > 0 && pageCount < (pagination?.pageIndex || 0) + 1) {
+ onChange({ pageIndex: pageCount - 1, pageSize: pagination?.pageSize || 0 });
+ }
+ }, [pageCount, onChange, pagination]);
+
const handleItemsPerPageChange: EuiTablePaginationProps['onChangeItemsPerPage'] = useCallback(
(pageSize) => {
onChange({ pageSize, pageIndex: pagination?.pageIndex || 0 });
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
index 7ecbad54dbbece0..356d44a81052871 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx
@@ -70,6 +70,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
['View host details', 'hostLink'],
['View agent policy', 'agentPolicyLink'],
['View agent details', 'agentDetailsLink'],
+ ['Reassign agent policy', 'agentPolicyReassignLink'],
])('should display %s action', async (_, dataTestSubj) => {
await render();
expect(renderResult.getByTestId(dataTestSubj)).not.toBeNull();
@@ -80,6 +81,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
['View host details', 'hostLink'],
['View agent policy', 'agentPolicyLink'],
['View agent details', 'agentDetailsLink'],
+ ['Reassign agent policy', 'agentPolicyReassignLink'],
])(
'should navigate via kibana `navigateToApp()` when %s is clicked',
async (_, dataTestSubj) => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
index f8574da7f0a03df..c431cd682d25ba6 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx
@@ -10,7 +10,7 @@ import styled from 'styled-components';
import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui';
import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types';
-import { FormattedDateAndTime } from '../../../../../../common/components/endpoint/formatted_date_time';
+import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date';
import { LogEntryTimelineIcon } from './log_entry_timeline_icon';
import * as i18 from '../../translations';
@@ -144,9 +144,8 @@ export const LogEntry = memo(({ logEntry }: { logEntry: Immutable{displayResponseEvent ? responseEventTitle : actionEventTitle}}
timelineIcon={
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
index 16cae79d42c0f27..38404a5c6c11ff9 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx
@@ -8,10 +8,8 @@
import styled from 'styled-components';
import {
EuiDescriptionList,
- EuiHorizontalRule,
EuiListGroup,
EuiListGroupItem,
- EuiIcon,
EuiText,
EuiFlexGroup,
EuiFlexItem,
@@ -23,17 +21,14 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { isPolicyOutOfDate } from '../../utils';
import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types';
-import { useEndpointSelector, useAgentDetailsIngestUrl } from '../hooks';
-import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
+import { useEndpointSelector } from '../hooks';
import { policyResponseStatus, uiQueryParams } from '../../store/selectors';
import { POLICY_STATUS_TO_BADGE_COLOR, HOST_STATUS_TO_BADGE_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
-import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
import { getEndpointDetailsPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
-import { AgentDetailsReassignPolicyAction } from '../../../../../../../fleet/public';
import { EndpointPolicyLink } from '../components/endpoint_policy_link';
import { OutOfDate } from '../components/out_of_date';
@@ -44,8 +39,6 @@ const HostIds = styled(EuiListGroupItem)`
}
`;
-const openReassignFlyoutSearch = '?openReassignFlyout=true';
-
export const EndpointDetails = memo(
({
details,
@@ -56,19 +49,34 @@ export const EndpointDetails = memo(
policyInfo?: HostInfo['policy_info'];
hostStatus: HostStatus;
}) => {
- const agentId = details.elastic.agent.id;
- const {
- url: agentDetailsUrl,
- appId: ingestAppId,
- appPath: agentDetailsAppPath,
- } = useAgentDetailsIngestUrl(agentId);
const queryParams = useEndpointSelector(uiQueryParams);
const policyStatus = useEndpointSelector(
policyResponseStatus
) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR;
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
- const detailsResultsUpper = useMemo(() => {
+ const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { selected_endpoint, show, ...currentUrlParams } = queryParams;
+ return [
+ formatUrl(
+ getEndpointDetailsPath({
+ name: 'endpointPolicyResponse',
+ ...currentUrlParams,
+ selected_endpoint: details.agent.id,
+ })
+ ),
+ getEndpointDetailsPath({
+ name: 'endpointPolicyResponse',
+ ...currentUrlParams,
+ selected_endpoint: details.agent.id,
+ }),
+ ];
+ }, [details.agent.id, formatUrl, queryParams]);
+
+ const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
+
+ const detailsResults = useMemo(() => {
return [
{
title: i18n.translate('xpack.securitySolution.endpoint.details.os', {
@@ -106,55 +114,9 @@ export const EndpointDetails = memo(
),
},
- ];
- }, [details, hostStatus]);
-
- const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { selected_endpoint, show, ...currentUrlParams } = queryParams;
- return [
- formatUrl(
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
- ...currentUrlParams,
- selected_endpoint: details.agent.id,
- })
- ),
- getEndpointDetailsPath({
- name: 'endpointPolicyResponse',
- ...currentUrlParams,
- selected_endpoint: details.agent.id,
- }),
- ];
- }, [details.agent.id, formatUrl, queryParams]);
-
- const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`;
- const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`;
- const handleReassignEndpointsClick = useNavigateToAppEventHandler(
- ingestAppId,
- {
- path: agentDetailsWithFlyoutPath,
- state: {
- onDoneNavigateTo: [
- 'securitySolution:administration',
- {
- path: getEndpointDetailsPath({
- name: 'endpointDetails',
- selected_endpoint: details.agent.id,
- }),
- },
- ],
- },
- }
- );
-
- const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
-
- const detailsResultsPolicy = useMemo(() => {
- return [
{
title: i18n.translate('xpack.securitySolution.endpoint.details.policy', {
- defaultMessage: 'Integration Policy',
+ defaultMessage: 'Policy',
}),
description: (
@@ -198,7 +160,7 @@ export const EndpointDetails = memo(
},
{
title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', {
- defaultMessage: 'Policy Response',
+ defaultMessage: 'Policy Status',
}),
description: (
// https://github.com/elastic/eui/issues/4530
@@ -210,7 +172,7 @@ export const EndpointDetails = memo(
onClick={policyStatusClickHandler}
onClickAriaLabel={i18n.translate(
'xpack.securitySolution.endpoint.details.policyStatus',
- { defaultMessage: 'Policy Response' }
+ { defaultMessage: 'Policy Status' }
)}
>
@@ -223,10 +185,12 @@ export const EndpointDetails = memo(
),
},
- ];
- }, [details, policyResponseUri, policyStatus, policyStatusClickHandler, policyInfo]);
- const detailsResultsLower = useMemo(() => {
- return [
+ {
+ title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
+ defaultMessage: 'Endpoint Version',
+ }),
+ description: {details.agent.version} ,
+ },
{
title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', {
defaultMessage: 'IP Address',
@@ -241,70 +205,23 @@ export const EndpointDetails = memo(
),
},
- {
- title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', {
- defaultMessage: 'Hostname',
- }),
- description: {details.host.hostname} ,
- },
- {
- title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
- defaultMessage: 'Endpoint Version',
- }),
- description: {details.agent.version} ,
- },
];
- }, [details.agent.version, details.host.hostname, details.host.ip]);
+ }, [
+ details,
+ hostStatus,
+ policyResponseUri,
+ policyStatus,
+ policyStatusClickHandler,
+ policyInfo,
+ ]);
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
>
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
index c39a17e98c76ab7..7892c56fef80692 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx
@@ -166,14 +166,14 @@ export const EndpointDetailsFlyout = memo(() => {
style={{ zIndex: 4001 }}
data-test-subj="endpointDetailsFlyout"
size="m"
- paddingSize="m"
+ paddingSize="l"
>
-
+
{hostDetailsLoading ? (
) : (
-
+
),
},
+ {
+ icon: 'gear',
+ key: 'agentPolicyReassignLink',
+ 'data-test-subj': 'agentPolicyReassignLink',
+ navigateAppId: 'fleet',
+ navigateOptions: {
+ path: `#${
+ pagePathGetters.fleet_agent_details({
+ agentId: fleetAgentId,
+ })[1]
+ }/activity?openReassignFlyout=true`,
+ },
+ href: `${getUrlForApp('fleet')}#${
+ pagePathGetters.fleet_agent_details({
+ agentId: fleetAgentId,
+ })[1]
+ }/activity?openReassignFlyout=true`,
+ children: (
+
+ ),
+ },
];
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index aa1c47a3102d907..86f1e32e751eeb7 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -516,7 +516,6 @@ describe('when on the endpoint list page', () => {
describe('when there is a selected host in the url', () => {
let hostDetails: HostInfo;
- let elasticAgentId: string;
let renderAndWaitForData: () => Promise>;
const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => {
const {
@@ -546,8 +545,6 @@ describe('when on the endpoint list page', () => {
query_strategy_version,
};
- elasticAgentId = hostDetails.metadata.elastic.agent.id;
-
const policy = docGenerator.generatePolicyPackagePolicy();
policy.id = hostDetails.metadata.Endpoint.policy.applied.id;
@@ -738,37 +735,11 @@ describe('when on the endpoint list page', () => {
);
});
- it('should include the link to reassignment in Ingest', async () => {
- coreStart.application.getUrlForApp.mockReturnValue('/app/fleet');
- const renderResult = await renderAndWaitForData();
- const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest');
- expect(linkToReassign).not.toBeNull();
- expect(linkToReassign.textContent).toEqual('Reassign Policy');
- expect(linkToReassign.getAttribute('href')).toEqual(
- `/app/fleet#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true`
- );
- });
-
it('should show the Take Action button', async () => {
const renderResult = await renderAndWaitForData();
expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
});
- describe('when link to reassignment in Ingest is clicked', () => {
- beforeEach(async () => {
- coreStart.application.getUrlForApp.mockReturnValue('/app/fleet');
- const renderResult = await renderAndWaitForData();
- const linkToReassign = await renderResult.findByTestId('endpointDetailsLinkToIngest');
- reactTestingLibrary.act(() => {
- reactTestingLibrary.fireEvent.click(linkToReassign);
- });
- });
-
- it('should navigate to Ingest without full page refresh', () => {
- expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1);
- });
- });
-
describe('when showing host Policy Response panel', () => {
let renderResult: ReturnType;
beforeEach(async () => {
@@ -1139,5 +1110,12 @@ describe('when on the endpoint list page', () => {
const agentDetailsLink = await renderResult.findByTestId('agentDetailsLink');
expect(agentDetailsLink.getAttribute('href')).toEqual(`/app/fleet#/fleet/agents/${agentId}`);
});
+
+ it('navigates to the Ingest Agent Details page with policy reassign', async () => {
+ const agentPolicyReassignLink = await renderResult.findByTestId('agentPolicyReassignLink');
+ expect(agentPolicyReassignLink.getAttribute('href')).toEqual(
+ `/app/fleet#/fleet/agents/${agentId}/activity?openReassignFlyout=true`
+ );
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
index d4e81fd81266873..fef6ccb99a17a2e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts
@@ -59,6 +59,13 @@ export const getListItems: EventFiltersSelector<
return apiResponseData?.data || [];
});
+export const getTotalCountListItems: EventFiltersSelector> = createSelector(
+ getListApiSuccessResponse,
+ (apiResponseData) => {
+ return apiResponseData?.total || 0;
+ }
+);
+
/**
* Will return the query that was used with the currently displayed list of content. If a new page
* of content is being loaded, this selector will then attempt to use the previousState to return
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
index ac2b16e51603c9f..9d2d3c394c4166e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts
@@ -17,6 +17,7 @@ import {
getCurrentListPageDataState,
getListApiSuccessResponse,
getListItems,
+ getTotalCountListItems,
getCurrentListItemsQuery,
getListPagination,
getListFetchError,
@@ -120,6 +121,19 @@ describe('event filters selectors', () => {
});
});
+ describe('getTotalCountListItems()', () => {
+ it('should return the list items from api response', () => {
+ setToLoadedState();
+ expect(getTotalCountListItems(initialState)).toEqual(
+ getLastLoadedResourceState(initialState.listPage.data)?.data.content.total
+ );
+ });
+
+ it('should return empty array if no api response', () => {
+ expect(getTotalCountListItems(initialState)).toEqual(0);
+ });
+ });
+
describe('getCurrentListItemsQuery()', () => {
it('should return empty object if Uninitialized', () => {
expect(getCurrentListItemsQuery(initialState)).toEqual({});
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
index 00ee80c5d70223e..0975104f02297f2 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
@@ -32,6 +32,7 @@ import {
getActionError,
getFormEntry,
showDeleteModal,
+ getTotalCountListItems,
} from '../store/selector';
import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content';
import { Immutable, ListPageRouteState } from '../../../../../common/endpoint/types';
@@ -66,6 +67,7 @@ export const EventFiltersListPage = memo(() => {
const isActionError = useEventFiltersSelector(getActionError);
const formEntry = useEventFiltersSelector(getFormEntry);
const listItems = useEventFiltersSelector(getListItems);
+ const totalCountListItems = useEventFiltersSelector(getTotalCountListItems);
const pagination = useEventFiltersSelector(getListPagination);
const isLoading = useEventFiltersSelector(getListIsLoading);
const fetchError = useEventFiltersSelector(getListFetchError);
@@ -235,7 +237,7 @@ export const EventFiltersListPage = memo(() => {
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap
new file mode 100644
index 000000000000000..d9dd6ec4a0be53e
--- /dev/null
+++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/__snapshots__/entity_by_expression.test.tsx.snap
@@ -0,0 +1,171 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render entity by expression with aggregatable field options for entity 1`] = `
+
+
+
+
+ }
+ value="FlightNum"
+ >
+
+ }
+ closePopover={[Function]}
+ display="block"
+ hasArrow={true}
+ id="popoverForExpression"
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="m"
+ zIndex={8000}
+ >
+
+
+
+
+
+ by
+
+
+
+ FlightNum
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.test.tsx
new file mode 100644
index 000000000000000..31b89873922c9c6
--- /dev/null
+++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.test.tsx
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { EntityByExpression, getValidIndexPatternFields } from './entity_by_expression';
+
+const defaultProps = {
+ errors: {
+ index: [],
+ indexId: [],
+ geoField: [],
+ entity: [],
+ dateField: [],
+ boundaryType: [],
+ boundaryIndexTitle: [],
+ boundaryIndexId: [],
+ boundaryGeoField: [],
+ name: ['Name is required.'],
+ interval: [],
+ alertTypeId: [],
+ actionConnectors: [],
+ },
+ entity: 'FlightNum',
+ setAlertParamsEntity: (arg: string) => {},
+ indexFields: [
+ {
+ count: 0,
+ name: 'DestLocation',
+ type: 'geo_point',
+ esTypes: ['geo_point'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'FlightNum',
+ type: 'string',
+ esTypes: ['keyword'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'OriginLocation',
+ type: 'geo_point',
+ esTypes: ['geo_point'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ {
+ count: 0,
+ name: 'timestamp',
+ type: 'date',
+ esTypes: ['date'],
+ scripted: false,
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ },
+ ],
+ isInvalid: false,
+};
+
+test('should render entity by expression with aggregatable field options for entity', async () => {
+ const component = mount( );
+ expect(component).toMatchSnapshot();
+});
+//
+
+test('should only use valid index fields', async () => {
+ // Only the string index field should match
+ const indexFields = getValidIndexPatternFields(defaultProps.indexFields);
+ expect(indexFields.length).toEqual(1);
+
+ // Set all agg fields to false, invalidating them for use
+ const invalidIndexFields = defaultProps.indexFields.map((field) => ({
+ ...field,
+ aggregatable: false,
+ }));
+
+ const noIndexFields = getValidIndexPatternFields(invalidIndexFields);
+ expect(noIndexFields.length).toEqual(0);
+});
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.tsx
index 2df6439ad56f050..a194bd40d9931f3 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/expressions/entity_by_expression.tsx
@@ -22,6 +22,16 @@ interface Props {
isInvalid: boolean;
}
+const ENTITY_TYPES = ['string', 'number', 'ip'];
+export function getValidIndexPatternFields(fields: IFieldType[]): IFieldType[] {
+ return fields.filter((field) => {
+ const isSpecifiedSupportedField = ENTITY_TYPES.includes(field.type);
+ const hasLeadingUnderscore = field.name.startsWith('_');
+ const isAggregatable = !!field.aggregatable;
+ return isSpecifiedSupportedField && isAggregatable && !hasLeadingUnderscore;
+ });
+}
+
export const EntityByExpression: FunctionComponent = ({
errors,
entity,
@@ -29,9 +39,6 @@ export const EntityByExpression: FunctionComponent = ({
indexFields,
isInvalid,
}) => {
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const ENTITY_TYPES = ['string', 'number', 'ip'];
-
const usePrevious = (value: T): T | undefined => {
const ref = useRef();
useEffect(() => {
@@ -48,14 +55,12 @@ export const EntityByExpression: FunctionComponent = ({
});
useEffect(() => {
if (!_.isEqual(oldIndexFields, indexFields)) {
- fields.current.indexFields = indexFields.filter(
- (field: IFieldType) => ENTITY_TYPES.includes(field.type) && !field.name.startsWith('_')
- );
+ fields.current.indexFields = getValidIndexPatternFields(indexFields);
if (!entity && fields.current.indexFields.length) {
setAlertParamsEntity(fields.current.indexFields[0].name);
}
}
- }, [ENTITY_TYPES, indexFields, oldIndexFields, setAlertParamsEntity, entity]);
+ }, [indexFields, oldIndexFields, setAlertParamsEntity, entity]);
const indexPopover = (
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 9be5957cd4718a2..676d6d6b7e35119 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -3028,7 +3028,6 @@
"indexPatternManagement.editIndexPattern.timeFilterHeader": "時刻フィールド:「{timeFieldName}」",
"indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "フィールドマッピング",
"indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "{indexPatternTitle}でフィールドを表示して編集します。型や検索可否などのフィールド属性はElasticsearchで{mappingAPILink}に基づきます。",
- "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "インデックスパターン",
"indexPatternManagement.emptyIndexPatternPrompt.documentation": "ドキュメンテーションを表示",
"indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibanaでは、検索するインデックスを特定するためにインデックスパターンが必要です。インデックスパターンは、昨日のログデータなど特定のインデックス、またはログデータを含むすべてのインデックスを参照できます。",
"indexPatternManagement.emptyIndexPatternPrompt.learnMore": "詳細について",
@@ -8924,15 +8923,6 @@
"xpack.fleet.agentStatus.offlineLabel": "オフライン",
"xpack.fleet.agentStatus.unhealthyLabel": "異常",
"xpack.fleet.agentStatus.updatingLabel": "更新中",
- "xpack.fleet.alphaMessageDescription": "Fleet は本番環境用ではありません。",
- "xpack.fleet.alphaMessageLinkText": "詳細を参照してください。",
- "xpack.fleet.alphaMessageTitle": "ベータリリース",
- "xpack.fleet.alphaMessaging.docsLink": "ドキュメンテーション",
- "xpack.fleet.alphaMessaging.feedbackText": "{docsLink}をご覧ください。質問やフィードバックについては、{forumLink}にアクセスしてください。",
- "xpack.fleet.alphaMessaging.flyoutTitle": "このリリースについて",
- "xpack.fleet.alphaMessaging.forumLink": "ディスカッションフォーラム",
- "xpack.fleet.alphaMessaging.introText": "Fleet は開発中であり、本番環境用ではありません。このベータリリースは、ユーザーが Fleet と新しい Elastic エージェントをテストしてフィードバックを提供することを目的としています。このプラグインには、サポート SLA が適用されません。",
- "xpack.fleet.alphaMessging.closeFlyoutLabel": "閉じる",
"xpack.fleet.appNavigation.agentsLinkText": "エージェント",
"xpack.fleet.appNavigation.dataStreamsLinkText": "データストリーム",
"xpack.fleet.appNavigation.overviewLinkText": "概要",
@@ -8996,11 +8986,6 @@
"xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション",
"xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle": "統合の構成",
"xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle": "エージェントポリシーを選択",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "エージェントポリシー情報の読み込みエラー",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索",
- "xpack.fleet.createPackagePolicy.stepSelectPackageTitle": "統合を選択",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.addButton": "エージェントポリシーを作成",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyLabel": "エージェントポリシー",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText": "この統合を追加するエージェントポリシーを選択",
@@ -11962,7 +11947,6 @@
"xpack.ingestPipelines.deleteModal.multipleErrorsNotificationMessageText": "{count}件のパイプラインの削除エラー",
"xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText": "パイプライン'{pipelineName}'を削除しました",
"xpack.ingestPipelines.edit.docsButtonLabel": "パイプラインドキュメントを編集",
- "xpack.ingestPipelines.edit.fetchPipelineError": "パイプラインの読み込みエラー",
"xpack.ingestPipelines.edit.loadingPipelinesDescription": "パイプラインを読み込んでいます…",
"xpack.ingestPipelines.edit.pageTitle": "パイプライン'{name}'を編集",
"xpack.ingestPipelines.form.cancelButtonLabel": "キャンセル",
@@ -11985,8 +11969,6 @@
"xpack.ingestPipelines.form.versionFieldLabel": "バージョン (任意) ",
"xpack.ingestPipelines.form.versionToggleDescription": "バージョン番号を追加",
"xpack.ingestPipelines.list.listTitle": "Ingestノードパイプライン",
- "xpack.ingestPipelines.list.loadErrorReloadLinkLabel": "再試行してください。",
- "xpack.ingestPipelines.list.loadErrorTitle": "パイプラインを読み込めません。{reloadLink}",
"xpack.ingestPipelines.list.loadingMessage": "パイプラインを読み込み中...",
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "パイプラインが見つかりません",
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "クローンを作成",
@@ -17675,7 +17657,6 @@
"xpack.remoteClusters.form.errors.serverNameMissing": "サーバー名が必要です。",
"xpack.remoteClusters.licenseCheckErrorMessage": "ライセンス確認失敗",
"xpack.remoteClusters.listBreadcrumbTitle": "リモートクラスター",
- "xpack.remoteClusters.loadAction.errorTitle": "リモートクラスターの読み込み中にエラーが発生",
"xpack.remoteClusters.readDocsButtonLabel": "リモートクラスタードキュメント",
"xpack.remoteClusters.refreshAction.errorTitle": "リモートクラスターの更新中にエラーが発生",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "保存中",
@@ -18035,15 +18016,15 @@
"xpack.rollupJobs.detailPanel.jobActionMenu.buttonLabel": "管理",
"xpack.rollupJobs.detailPanel.loadingLabel": "ロールアップジョブを読み込み中...",
"xpack.rollupJobs.detailPanel.notFoundLabel": "ロールアップジョブが見つかりません",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonDescription": "要約データに制限された集約を実行します。",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonText": "ロールアップインデックスパターン",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName": "ロールアップインデックスパターン",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.indexLabel": "ロールアップ",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.noMatchError": "ロールアップインデックスパターンエラー:ロールアップインデックスの 1 つと一致している必要があります",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.tooManyMatchesError": "ロールアップインデックスパターンエラー:一致できるロールアップインデックスは 1 つだけです",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.uncaughtError": "ロールアップインデックスパターンエラー:{error}",
- "xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text": "ロールアップインデックスパターンのKibanaのサポートはベータ版です。保存された検索、可視化、ダッシュボードでこれらのパターンを使用すると問題が発生する場合があります。Timelionや機械学習などの一部の高度な機能ではサポートされていません。",
- "xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text": "ロールアップインデックスパターンは、1つのロールアップインデックスとゼロ以上の標準インデックスと一致させることができます。ロールアップインデックスパターンでは、メトリック、フィールド、間隔、アグリゲーションが制限されています。ロールアップインデックスは、1つのジョブ構成があるインデックス、または複数のジョブと互換する構成があるインデックスに制限されています。",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription": "要約データに制限された集約を実行します。",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText": "ロールアップインデックスパターン",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName": "ロールアップインデックスパターン",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel": "ロールアップ",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError": "ロールアップインデックスパターンエラー:ロールアップインデックスの 1 つと一致している必要があります",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError": "ロールアップインデックスパターンエラー:一致できるロールアップインデックスは 1 つだけです",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError": "ロールアップインデックスパターンエラー:{error}",
+ "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text": "ロールアップインデックスパターンのKibanaのサポートはベータ版です。保存された検索、可視化、ダッシュボードでこれらのパターンを使用すると問題が発生する場合があります。Timelionや機械学習などの一部の高度な機能ではサポートされていません。",
+ "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text": "ロールアップインデックスパターンは、1つのロールアップインデックスとゼロ以上の標準インデックスと一致させることができます。ロールアップインデックスパターンでは、メトリック、フィールド、間隔、アグリゲーションが制限されています。ロールアップインデックスは、1つのジョブ構成があるインデックス、または複数のジョブと互換する構成があるインデックスに制限されています。",
"xpack.rollupJobs.featureCatalogueDescription": "今後の分析用に履歴データを小さなインデックスに要約して格納します。",
"xpack.rollupJobs.indexMgmtBadge.rollupLabel": "ロールアップ",
"xpack.rollupJobs.indexMgmtToggle.toggleLabel": "ロールアップインデックスを含める",
@@ -20131,10 +20112,8 @@
"xpack.securitySolution.endpoint.details.endpointVersion": "エンドポイントバージョン",
"xpack.securitySolution.endpoint.details.errorBody": "フライアウトを終了して、利用可能なホストを選択してください。",
"xpack.securitySolution.endpoint.details.errorTitle": "ホストが見つかりませんでした",
- "xpack.securitySolution.endpoint.details.hostname": "ホスト名",
"xpack.securitySolution.endpoint.details.ipAddress": "IPアドレス",
"xpack.securitySolution.endpoint.details.lastSeen": "前回の認識",
- "xpack.securitySolution.endpoint.details.linkToIngestTitle": "ポリシーの再割り当て",
"xpack.securitySolution.endpoint.details.noPolicyResponse": "ポリシー応答がありません",
"xpack.securitySolution.endpoint.details.os": "OS",
"xpack.securitySolution.endpoint.details.policy": "統合ポリシー",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index d6ebf8a32bcc4e1..8b558620aa897e2 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -3049,7 +3049,6 @@
"indexPatternManagement.editIndexPattern.timeFilterHeader": "时间字段:“{timeFieldName}”",
"indexPatternManagement.editIndexPattern.timeFilterLabel.mappingAPILink": "字段映射",
"indexPatternManagement.editIndexPattern.timeFilterLabel.timeFilterDetail": "查看和编辑 {indexPatternTitle} 中的字段。字段属性,如类型和可搜索性,基于 Elasticsearch 中的 {mappingAPILink}。",
- "indexPatternManagement.editIndexPatternLiveRegionAriaLabel": "索引模式",
"indexPatternManagement.emptyIndexPatternPrompt.documentation": "阅读文档",
"indexPatternManagement.emptyIndexPatternPrompt.indexPatternExplanation": "Kibana 需要索引模式,以识别您要浏览的索引。索引模式可以指向特定索引 (例如昨天的日志数据) ,或包含日志数据的所有索引。",
"indexPatternManagement.emptyIndexPatternPrompt.learnMore": "希望了解详情?",
@@ -9004,15 +9003,6 @@
"xpack.fleet.agentStatus.offlineLabel": "脱机",
"xpack.fleet.agentStatus.unhealthyLabel": "运行不正常",
"xpack.fleet.agentStatus.updatingLabel": "正在更新",
- "xpack.fleet.alphaMessageDescription": "不推荐在生产环境中使用 Fleet。",
- "xpack.fleet.alphaMessageLinkText": "查看更多详情。",
- "xpack.fleet.alphaMessageTitle": "公测版",
- "xpack.fleet.alphaMessaging.docsLink": "文档",
- "xpack.fleet.alphaMessaging.feedbackText": "阅读我们的{docsLink}或前往我们的{forumLink},以了解问题或提供反馈。",
- "xpack.fleet.alphaMessaging.flyoutTitle": "关于本版本",
- "xpack.fleet.alphaMessaging.forumLink": "讨论论坛",
- "xpack.fleet.alphaMessaging.introText": "Fleet 仍处于开发状态,不适用于生产环境。此公测版用于用户测试 Fleet 和新 Elastic 代理并提供相关反馈。此插件不受支持 SLA 的约束。",
- "xpack.fleet.alphaMessging.closeFlyoutLabel": "关闭",
"xpack.fleet.appNavigation.agentsLinkText": "代理",
"xpack.fleet.appNavigation.dataStreamsLinkText": "数据流",
"xpack.fleet.appNavigation.overviewLinkText": "概览",
@@ -9077,13 +9067,7 @@
"xpack.fleet.createPackagePolicy.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项",
"xpack.fleet.createPackagePolicy.stepConfigurePackagePolicyTitle": "配置集成",
"xpack.fleet.createPackagePolicy.stepSelectAgentPolicyTitle": "选择代理策略",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingPolicyTitle": "加载代理策略信息时出错",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错",
- "xpack.fleet.createPackagePolicy.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成",
- "xpack.fleet.createPackagePolicy.stepSelectPackageTitle": "选择集成",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.addButton": "创建代理策略",
- "xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsCountText": "{count, plural, other {# 个代理}}已注册",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "{count, plural, other {# 个代理}}已注册到选定代理策略。",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyLabel": "代理策略",
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyPlaceholderText": "选择要将此集成添加到的代理策略",
@@ -12124,7 +12108,6 @@
"xpack.ingestPipelines.deleteModal.successDeleteMultipleNotificationMessageText": "已删除 {numSuccesses, plural, other {# 个管道}}",
"xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText": "已删除管道“{pipelineName}”",
"xpack.ingestPipelines.edit.docsButtonLabel": "编辑管道文档",
- "xpack.ingestPipelines.edit.fetchPipelineError": "加载管道时出错",
"xpack.ingestPipelines.edit.loadingPipelinesDescription": "正在加载管道……",
"xpack.ingestPipelines.edit.pageTitle": "编辑管道“{name}”",
"xpack.ingestPipelines.form.cancelButtonLabel": "取消",
@@ -12149,8 +12132,6 @@
"xpack.ingestPipelines.form.versionFieldLabel": "版本 (可选) ",
"xpack.ingestPipelines.form.versionToggleDescription": "添加版本号",
"xpack.ingestPipelines.list.listTitle": "采集节点管道",
- "xpack.ingestPipelines.list.loadErrorReloadLinkLabel": "请重试。",
- "xpack.ingestPipelines.list.loadErrorTitle": "无法加载管道。{reloadLink}",
"xpack.ingestPipelines.list.loadingMessage": "正在加载管道……",
"xpack.ingestPipelines.list.notFoundFlyoutMessage": "未找到管道",
"xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel": "克隆",
@@ -17915,7 +17896,6 @@
"xpack.remoteClusters.form.errors.serverNameMissing": "服务器名必填。",
"xpack.remoteClusters.licenseCheckErrorMessage": "许可证检查失败",
"xpack.remoteClusters.listBreadcrumbTitle": "远程集群",
- "xpack.remoteClusters.loadAction.errorTitle": "加载远程集群时出错",
"xpack.remoteClusters.readDocsButtonLabel": "远程集群文档",
"xpack.remoteClusters.refreshAction.errorTitle": "刷新远程集群时出错",
"xpack.remoteClusters.remoteClusterForm.actions.savingText": "正在保存",
@@ -18276,15 +18256,15 @@
"xpack.rollupJobs.detailPanel.jobActionMenu.buttonLabel": "管理",
"xpack.rollupJobs.detailPanel.loadingLabel": "正在加载汇总/打包作业……",
"xpack.rollupJobs.detailPanel.notFoundLabel": "未找到汇总/打包作业",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonDescription": "针对汇总数据执行有限聚合",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultButtonText": "汇总/打包索引模式",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.defaultTypeName": "汇总/打包索引模式",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.indexLabel": "汇总/打包",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.noMatchError": "汇总/打包索引模式错误:必须匹配一个汇总/打包索引",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.tooManyMatchesError": "汇总/打包索引模式错误:只能匹配一个汇总/打包索引",
- "xpack.rollupJobs.editRollupIndexPattern.createIndex.uncaughtError": "汇总索引模式错误:{error}",
- "xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text": "Kibana 对汇总/打包索引模式的支持处于公测版状态。将这些模式用于已保存搜索、可视化以及仪表板可能会遇到问题。某些高级功能,如 Timelion 和 Machine Learning,不支持这些模式。",
- "xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text": "可以根据一个汇总/打包索引和零个或更多常规索引匹配汇总/打包索引模式。汇总/打包索引模式的指标、字段、时间间隔和聚合有限。汇总/打包索引仅限于具有一个作业配置或多个作业配置兼容的索引。",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonDescription": "针对汇总数据执行有限聚合",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.defaultButtonText": "汇总/打包索引模式",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.defaultTypeName": "汇总/打包索引模式",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.indexLabel": "汇总/打包",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.noMatchError": "汇总/打包索引模式错误:必须匹配一个汇总/打包索引",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.tooManyMatchesError": "汇总/打包索引模式错误:只能匹配一个汇总/打包索引",
+ "indexPatternManagement.editRollupIndexPattern.createIndex.uncaughtError": "汇总索引模式错误:{error}",
+ "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text": "Kibana 对汇总/打包索引模式的支持处于公测版状态。将这些模式用于已保存搜索、可视化以及仪表板可能会遇到问题。某些高级功能,如 Timelion 和 Machine Learning,不支持这些模式。",
+ "indexPatternManagement.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text": "可以根据一个汇总/打包索引和零个或更多常规索引匹配汇总/打包索引模式。汇总/打包索引模式的指标、字段、时间间隔和聚合有限。汇总/打包索引仅限于具有一个作业配置或多个作业配置兼容的索引。",
"xpack.rollupJobs.featureCatalogueDescription": "汇总历史数据并将其存储在较小的索引中以供将来分析。",
"xpack.rollupJobs.indexMgmtBadge.rollupLabel": "汇总/打包",
"xpack.rollupJobs.indexMgmtToggle.toggleLabel": "包括汇总索引",
@@ -20430,10 +20410,8 @@
"xpack.securitySolution.endpoint.details.endpointVersion": "终端版本",
"xpack.securitySolution.endpoint.details.errorBody": "请退出浮出控件并选择可用主机。",
"xpack.securitySolution.endpoint.details.errorTitle": "找不到主机",
- "xpack.securitySolution.endpoint.details.hostname": "主机名",
"xpack.securitySolution.endpoint.details.ipAddress": "IP 地址",
"xpack.securitySolution.endpoint.details.lastSeen": "最后看到时间",
- "xpack.securitySolution.endpoint.details.linkToIngestTitle": "重新分配策略",
"xpack.securitySolution.endpoint.details.noPolicyResponse": "没有可用策略响应",
"xpack.securitySolution.endpoint.details.os": "OS",
"xpack.securitySolution.endpoint.details.policy": "集成策略",
diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts
index c34da1bc097bc0b..01fa320bbc789cd 100644
--- a/x-pack/plugins/uptime/public/apps/plugin.ts
+++ b/x-pack/plugins/uptime/public/apps/plugin.ts
@@ -111,7 +111,7 @@ export class UptimePlugin
return [
{
label: 'Uptime',
- sortKey: 200,
+ sortKey: 500,
entries: [
{
label: i18n.translate('xpack.uptime.overview.heading', {
diff --git a/x-pack/test/api_integration/apis/ml/modules/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts
index 4fa79b915cc5dff..0a3e2dbed570bf1 100644
--- a/x-pack/test/api_integration/apis/ml/modules/get_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts
@@ -30,6 +30,7 @@ const moduleIds = [
'nginx_ecs',
'sample_data_ecommerce',
'sample_data_weblogs',
+ 'security_auth',
'security_linux',
'security_network',
'security_windows',
diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
index 2181bea8b40407b..2742fbff294c0d3 100644
--- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts
@@ -84,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => {
user: USER.ML_POWERUSER,
expected: {
responseCode: 200,
- moduleIds: ['siem_auditbeat', 'siem_auditbeat_auth'],
+ moduleIds: ['security_auth', 'siem_auditbeat', 'siem_auditbeat_auth'],
},
},
{
@@ -105,6 +105,7 @@ export default ({ getService }: FtrProviderContext) => {
expected: {
responseCode: 200,
moduleIds: [
+ 'security_auth',
'security_network',
'security_windows',
'siem_winlogbeat',
@@ -148,7 +149,7 @@ export default ({ getService }: FtrProviderContext) => {
user: USER.ML_POWERUSER,
expected: {
responseCode: 200,
- moduleIds: ['security_linux', 'security_network', 'security_windows'],
+ moduleIds: ['security_auth', 'security_linux', 'security_network', 'security_windows'],
},
},
{
diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts b/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts
index e861dc528b2375e..e8c940d6b29b63d 100644
--- a/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts
+++ b/x-pack/test/api_integration/apis/ml/saved_objects/sync.ts
@@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
+import { cloneDeep } from 'lodash';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { USER } from '../../../../functional/services/ml/security_common';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
@@ -23,7 +24,7 @@ export default ({ getService }: FtrProviderContext) => {
async function runRequest(user: USER, expectedStatusCode: number) {
const { body } = await supertest
- .get(`/s/space1/api/ml/saved_objects/sync`)
+ .get(`/s/${idSpace1}/api/ml/saved_objects/sync`)
.auth(user, ml.securityCommon.getPasswordForUser(user))
.set(COMMON_REQUEST_HEADERS)
.expect(expectedStatusCode);
@@ -32,9 +33,19 @@ export default ({ getService }: FtrProviderContext) => {
}
describe('GET saved_objects/sync', () => {
- before(async () => {
+ beforeEach(async () => {
await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] });
+ await ml.testResources.setKibanaTimeZoneToUTC();
+ });
+ afterEach(async () => {
+ await spacesService.delete(idSpace1);
+ await ml.api.cleanMlIndices();
+ await ml.testResources.cleanMLSavedObjects();
+ });
+
+ it('should sync datafeeds and saved objects', async () => {
+ // prepare test data
await ml.api.createAnomalyDetectionJob(
ml.commonConfig.getADFqSingleMetricJobConfig(adJobId1),
idSpace1
@@ -51,18 +62,6 @@ export default ({ getService }: FtrProviderContext) => {
ml.commonConfig.getADFqSingleMetricJobConfig(adJobIdES)
);
- await ml.testResources.setKibanaTimeZoneToUTC();
- });
-
- after(async () => {
- await spacesService.delete(idSpace1);
- await ml.api.cleanMlIndices();
- await ml.testResources.cleanMLSavedObjects();
- });
-
- it('should sync datafeeds and saved objects', async () => {
- // prepare test data
-
// datafeed should be added with the request
const datafeedConfig2 = ml.commonConfig.getADFqDatafeedConfig(adJobId2);
await ml.api.createDatafeedES(datafeedConfig2);
@@ -89,7 +88,73 @@ export default ({ getService }: FtrProviderContext) => {
});
});
+ it('should sync datafeeds after recreation in ES with different name', async () => {
+ // prepare test data
+ const jobConfig1 = ml.commonConfig.getADFqSingleMetricJobConfig(adJobId1);
+ await ml.api.createAnomalyDetectionJob(jobConfig1, idSpace1);
+
+ // datafeed should be added with the request
+ const datafeedConfig1 = ml.commonConfig.getADFqDatafeedConfig(adJobId1);
+ await ml.api.createDatafeedES(datafeedConfig1);
+
+ // run the sync request and verify the response
+ const body = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
+
+ // expect datafeed to be added
+ expect(body).to.eql({
+ datafeedsAdded: { [adJobId1]: { success: true } },
+ datafeedsRemoved: {},
+ savedObjectsCreated: {},
+ savedObjectsDeleted: {},
+ });
+
+ // delete the datafeed but do not sync
+ await ml.api.deleteDatafeedES(datafeedConfig1.datafeed_id);
+
+ // create a new datafeed with a different id
+ const datafeedConfig2 = cloneDeep(datafeedConfig1);
+ datafeedConfig2.datafeed_id = `different_${datafeedConfig2.datafeed_id}`;
+ await ml.api.createDatafeedES(datafeedConfig2);
+
+ const body2 = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
+
+ // previous datafeed should be removed on first sync
+ expect(body2).to.eql({
+ datafeedsAdded: {},
+ datafeedsRemoved: { [adJobId1]: { success: true } },
+ savedObjectsCreated: {},
+ savedObjectsDeleted: {},
+ });
+
+ const body3 = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
+
+ // new datafeed will be added on second sync
+ expect(body3).to.eql({
+ datafeedsAdded: { [adJobId1]: { success: true } },
+ datafeedsRemoved: {},
+ savedObjectsCreated: {},
+ savedObjectsDeleted: {},
+ });
+ });
+
it('should not sync anything if all objects are already synced', async () => {
+ await ml.api.createAnomalyDetectionJob(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobId1),
+ idSpace1
+ );
+ await ml.api.createAnomalyDetectionJob(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobId2),
+ idSpace1
+ );
+ await ml.api.createAnomalyDetectionJob(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobId3),
+ idSpace1
+ );
+ await ml.api.createAnomalyDetectionJobES(
+ ml.commonConfig.getADFqSingleMetricJobConfig(adJobIdES)
+ );
+
+ await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
const body = await runRequest(USER.ML_POWERUSER_ALL_SPACES, 200);
expect(body).to.eql({
diff --git a/x-pack/test/functional/apps/maps/blended_vector_layer.js b/x-pack/test/functional/apps/maps/blended_vector_layer.js
index e207410eb22818a..3cc7d8e07d623f4 100644
--- a/x-pack/test/functional/apps/maps/blended_vector_layer.js
+++ b/x-pack/test/functional/apps/maps/blended_vector_layer.js
@@ -29,7 +29,7 @@ export default function ({ getPageObjects, getService }) {
it('should request documents when zoomed to smaller regions showing less data', async () => {
const { rawResponse: response } = await PageObjects.maps.getResponse();
// Allow a range of hits to account for variances in browser window size.
- expect(response.hits.hits.length).to.be.within(30, 40);
+ expect(response.hits.hits.length).to.be.within(35, 45);
});
it('should request clusters when zoomed to larger regions showing lots of data', async () => {
diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts
index 52ec81b8bf7dbef..0872abfcaa4f86b 100644
--- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts
+++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts
@@ -253,7 +253,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
]);
});
- it('allows configuring http advanced options', async () => {
+ it.skip('allows configuring http advanced options', async () => {
// This test ensures that updates made to the Synthetics Policy are carried all the way through
// to the generated Agent Policy that is dispatch down to the Elastic Agent.
const config = generateHTTPConfig('http://elastic.co');
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index b2790d0f7a08ef6..332a40795bee9b8 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -43,7 +43,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
},
async isLensPageOrFail() {
- return await testSubjects.existOrFail('lnsApp');
+ return await testSubjects.existOrFail('lnsApp', { timeout: 1000 });
},
/**
diff --git a/x-pack/test/functional/page_objects/synthetics_integration_page.ts b/x-pack/test/functional/page_objects/synthetics_integration_page.ts
index 69ae3f43d26f2dd..56a67d9e6fbd43f 100644
--- a/x-pack/test/functional/page_objects/synthetics_integration_page.ts
+++ b/x-pack/test/functional/page_objects/synthetics_integration_page.ts
@@ -16,6 +16,8 @@ export function SyntheticsIntegrationPageProvider({
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
+ const fixedFooterHeight = 72; // Size of EuiBottomBar more or less
+
return {
/**
* Navigates to the Synthetics Integration page
@@ -85,6 +87,7 @@ export function SyntheticsIntegrationPageProvider({
*/
async fillTextInputByTestSubj(testSubj: string, value: string) {
const field = await testSubjects.find(testSubj, 5000);
+ await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight });
await field.click();
await field.clearValue();
await field.type(value);
@@ -96,6 +99,7 @@ export function SyntheticsIntegrationPageProvider({
* @params {value} the value of the input
*/
async fillTextInput(field: WebElementWrapper, value: string) {
+ await field.scrollIntoViewIfNecessary({ bottomOffset: fixedFooterHeight });
await field.click();
await field.clearValue();
await field.type(value);
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts
index e55307ed5ef66f2..1a5158adbd69511 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts
@@ -124,8 +124,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
describe('when the hostname is clicked on,', () => {
it('display the details flyout', async () => {
await (await testSubjects.find('hostnameCellLink')).click();
- await testSubjects.existOrFail('endpointDetailsUpperList');
- await testSubjects.existOrFail('endpointDetailsLowerList');
+ await testSubjects.existOrFail('endpointDetailsList');
});
it('updates the details flyout when a new hostname is selected from the list', async () => {
diff --git a/x-pack/test/stack_functional_integration/apps/alerts/alerts_encryption_keys.js b/x-pack/test/stack_functional_integration/apps/alerts/alerts_encryption_keys.js
index f11aa7e09635bfc..e4ac00661c078b3 100644
--- a/x-pack/test/stack_functional_integration/apps/alerts/alerts_encryption_keys.js
+++ b/x-pack/test/stack_functional_integration/apps/alerts/alerts_encryption_keys.js
@@ -6,8 +6,14 @@
*/
import expect from '@kbn/expect';
+import { resolve } from 'path';
+import { REPO_ROOT } from '@kbn/dev-utils';
-const ARCHIVE = '../integration-test/test/es_archives/email_connectors_with_encryption_rotation';
+const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test');
+const ARCHIVE = resolve(
+ INTEGRATION_TEST_ROOT,
+ 'test/es_archives/email_connectors_with_encryption_rotation'
+);
export default ({ getPageObjects, getService }) => {
const esArchiver = getService('esArchiver');
diff --git a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js
index 00fce236b5d1724..a22e4438c7dbdd9 100644
--- a/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js
+++ b/x-pack/test/stack_functional_integration/apps/ccs/ccs_discover.js
@@ -6,11 +6,15 @@
*/
import fs from 'fs';
+import { resolve } from 'path';
import expect from '@kbn/expect';
import { Client as EsClient } from '@elastic/elasticsearch';
import { KbnClient } from '@kbn/test';
import { EsArchiver } from '@kbn/es-archiver';
-import { CA_CERT_PATH } from '@kbn/dev-utils';
+import { CA_CERT_PATH, REPO_ROOT } from '@kbn/dev-utils';
+
+const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test');
+const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat');
export default ({ getService, getPageObjects }) => {
describe('Cross cluster search test in discover', async () => {
@@ -261,7 +265,7 @@ export default ({ getService, getPageObjects }) => {
before('Prepare data:metricbeat-*', async function () {
log.info('Create index');
- await esArchiver.load('../integration-test/test/es_archives/metricbeat');
+ await esArchiver.load(ARCHIVE);
log.info('Create index pattern');
dataId = await supertest
@@ -321,7 +325,7 @@ export default ({ getService, getPageObjects }) => {
}
log.info('Delete index');
- await esArchiver.unload('../integration-test/test/es_archives/metricbeat');
+ await esArchiver.unload(ARCHIVE);
});
after('Clean up .siem-signal-*', async function () {
diff --git a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
index 2b25d5ffea6e1ab..b678e88bcf0dfff 100644
--- a/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
+++ b/x-pack/test/stack_functional_integration/apps/metricbeat/_metricbeat_dashboard.js
@@ -6,6 +6,11 @@
*/
import expect from '@kbn/expect';
+import { resolve } from 'path';
+import { REPO_ROOT } from '@kbn/dev-utils';
+
+const INTEGRATION_TEST_ROOT = process.env.WORKSPACE || resolve(REPO_ROOT, '../integration-test');
+const ARCHIVE = resolve(INTEGRATION_TEST_ROOT, 'test/es_archives/metricbeat');
export default function ({ getService, getPageObjects, updateBaselines }) {
const screenshot = getService('screenshots');
@@ -15,7 +20,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
describe('check metricbeat Dashboard', function () {
before(async function () {
- await esArchiver.load('../integration-test/test/es_archives/metricbeat');
+ await esArchiver.load(ARCHIVE);
// this navigateToActualURL takes the place of navigating to the dashboard landing page,
// filtering on the dashboard name, selecting it, setting the timepicker, and going to full screen
@@ -45,7 +50,7 @@ export default function ({ getService, getPageObjects, updateBaselines }) {
});
after(async function () {
- await esArchiver.unload('../integration-test/test/es_archives/metricbeat');
+ await esArchiver.unload(ARCHIVE);
});
it('[Metricbeat System] Overview ECS should match snapshot', async function () {
diff --git a/yarn.lock b/yarn.lock
index 0e2b953389c10a6..a12920f72ba82ec 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10870,7 +10870,7 @@ d3-array@>=2.5, d3-array@^2.3.0, d3-array@^2.7.1:
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.8.0.tgz#f76e10ad47f1f4f75f33db5fc322eb9ffde5ef23"
integrity sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==
-d3-cloud@1.2.5, d3-cloud@^1.2.5:
+d3-cloud@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/d3-cloud/-/d3-cloud-1.2.5.tgz#3e91564f2d27fba47fcc7d812eb5081ea24c603d"
integrity sha512-4s2hXZgvs0CoUIw31oBAGrHt9Kt/7P9Ik5HIVzISFiWkD0Ga2VLAuO/emO/z1tYIpE7KG2smB4PhMPfFMJpahw==