diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage
index f2a58e7b6a7ac5..c474998e6fd3db 100644
--- a/.ci/Jenkinsfile_coverage
+++ b/.ci/Jenkinsfile_coverage
@@ -3,7 +3,7 @@
library 'kibana-pipeline-library'
kibanaLibrary.load() // load from the Jenkins instance
-kibanaPipeline(timeoutMinutes: 180) {
+kibanaPipeline(timeoutMinutes: 240) {
catchErrors {
withEnv([
'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc.
diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md
new file mode 100644
index 00000000000000..51492756ef2327
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md)
+
+## AppBase.defaultPath property
+
+Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the `path` option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar.
+
+Signature:
+
+```typescript
+defaultPath?: string;
+```
diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.md b/docs/development/core/public/kibana-plugin-core-public.appbase.md
index b73785647f23cc..7b624f12ac1df0 100644
--- a/docs/development/core/public/kibana-plugin-core-public.appbase.md
+++ b/docs/development/core/public/kibana-plugin-core-public.appbase.md
@@ -18,6 +18,7 @@ export interface AppBase
| [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) | Partial<Capabilities>
| Custom capabilities defined by the app. |
| [category](./kibana-plugin-core-public.appbase.category.md) | AppCategory
| The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference |
| [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) | boolean
| Hide the UI chrome when the application is mounted. Defaults to false
. Takes precedence over chrome service visibility settings. |
+| [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) | string
| Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path
option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. |
| [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) | string
| A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon
property. |
| [icon](./kibana-plugin-core-public.appbase.icon.md) | string
| A URL to an image file used as an icon. Used as a fallback if euiIconType
is not provided. |
| [id](./kibana-plugin-core-public.appbase.id.md) | string
| The unique identifier of the application |
diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md
index cdf9171a46aed0..3d8b5d115c8a27 100644
--- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md
+++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md
@@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug
Signature:
```typescript
-export declare type AppUpdatableFields = Pick;
+export declare type AppUpdatableFields = Pick;
```
diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md
index 1cc1a1194a5379..a9fabb38df8696 100644
--- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md
+++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md
@@ -29,5 +29,5 @@ export interface ChromeNavLink
| [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) | string
| A url base that legacy apps can set to match deep URLs to an application. |
| [title](./kibana-plugin-core-public.chromenavlink.title.md) | string
| The title of the application. |
| [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string
| A tooltip shown when hovering over an app link. |
-| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string
| A url that legacy apps can set to deep link into their applications. |
+| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string
| The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, baseUrl
will be used instead. |
diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md
index 0c415ed1a7fadc..1e0b8900159930 100644
--- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md
+++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md
@@ -4,11 +4,7 @@
## ChromeNavLink.url property
-> Warning: This API is now obsolete.
->
->
-
-A url that legacy apps can set to deep link into their applications.
+The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, `baseUrl` will be used instead.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md
index a502c40db0cd8c..a3294fb0a087ae 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md
@@ -9,22 +9,36 @@ A migration function for a [saved object type](./kibana-plugin-core-server.saved
Signature:
```typescript
-export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc;
+export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc;
```
## Example
```typescript
-const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => {
- if(doc.attributes.someProp === null) {
- log.warn('Skipping migration');
- } else {
- doc.attributes.someProp = migrateProperty(doc.attributes.someProp);
- }
-
- return doc;
+interface TypeV1Attributes {
+ someKey: string;
+ obsoleteProperty: number;
}
+interface TypeV2Attributes {
+ someKey: string;
+ newProperty: string;
+}
+
+const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => {
+ const { obsoleteProperty, ...otherAttributes } = doc.attributes;
+ // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input
+ // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete
+ // attributes are not present on the returned doc.
+ return {
+ ...doc,
+ attributes: {
+ ...otherAttributes,
+ newProperty: migrate(obsoleteProperty),
+ },
+ };
+};
+
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md
index 6d4e252fe7532e..3f4090619edbfb 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md
@@ -9,5 +9,5 @@ Describes Saved Object documents that have passed through the migration framewor
Signature:
```typescript
-export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable;
+export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable;
```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md
index be51400addbbc5..8e2395ee6310d8 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md
@@ -9,5 +9,5 @@ Describes Saved Object documents from Kibana < 7.0.0 which don't have a `refe
Signature:
```typescript
-export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
+export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md
index 04a0d871cab2dc..3969a97fa7789f 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md
@@ -7,7 +7,10 @@
Signature:
```typescript
-export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined;
+export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: {
+ forceNow?: Date;
+ fieldName?: string;
+}): import("../..").RangeFilter | undefined;
```
## Parameters
@@ -16,7 +19,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan
| --- | --- | --- |
| indexPattern | IIndexPattern | undefined
| |
| timeRange | TimeRange
| |
-| forceNow | Date
| |
+| options | {
forceNow?: Date;
fieldName?: string;
}
| |
Returns:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md
new file mode 100644
index 00000000000000..c3998876c97121
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md)
+
+## IIndexPattern.getTimeField() method
+
+Signature:
+
+```typescript
+getTimeField?(): IFieldType | undefined;
+```
+Returns:
+
+`IFieldType | undefined`
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
index 1bbd6cf67f0ce3..1cb89822eb605d 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md
@@ -21,3 +21,9 @@ export interface IIndexPattern
| [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string
| |
| [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string
| |
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [getTimeField()](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) | |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index 4d63d1c3501973..13e38ba5e6e5dc 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -41,7 +41,7 @@
| [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | |
| [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | |
| [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | |
-| [getTime(indexPattern, timeRange, forceNow)](./kibana-plugin-plugins-data-public.gettime.md) | |
+| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | |
## Interfaces
diff --git a/renovate.json5 b/renovate.json5
index ffa006264873d3..c0ddcaf4f23c8f 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -846,6 +846,14 @@
'@types/semver',
],
},
+ {
+ groupSlug: 'set-value',
+ groupName: 'set-value related packages',
+ packageNames: [
+ 'set-value',
+ '@types/set-value',
+ ],
+ },
{
groupSlug: 'sinon',
groupName: 'sinon related packages',
diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md
index 8c5fe4875aaea2..c91c00bc1aa021 100644
--- a/src/core/MIGRATION_EXAMPLES.md
+++ b/src/core/MIGRATION_EXAMPLES.md
@@ -957,7 +957,7 @@ const migration = (doc, log) => {...}
Would be converted to:
```typescript
-const migration: SavedObjectMigrationFn = (doc, { log }) => {...}
+const migration: SavedObjectMigrationFn = (doc, { log }) => {...}
```
### Remarks
diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts
index c25918c6b7328c..e29837aecb1253 100644
--- a/src/core/public/application/application_service.test.ts
+++ b/src/core/public/application/application_service.test.ts
@@ -87,7 +87,7 @@ describe('#setup()', () => {
).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
});
- it('allows to register a statusUpdater for the application', async () => {
+ it('allows to register an AppUpdater for the application', async () => {
const setup = service.setup(setupDeps);
const pluginId = Symbol('plugin');
@@ -118,6 +118,7 @@ describe('#setup()', () => {
updater$.next(app => ({
status: AppStatus.inaccessible,
tooltip: 'App inaccessible due to reason',
+ defaultPath: 'foo/bar',
}));
applications = await applications$.pipe(take(1)).toPromise();
@@ -128,6 +129,7 @@ describe('#setup()', () => {
legacy: false,
navLinkStatus: AppNavLinkStatus.default,
status: AppStatus.inaccessible,
+ defaultPath: 'foo/bar',
tooltip: 'App inaccessible due to reason',
})
);
@@ -209,7 +211,7 @@ describe('#setup()', () => {
});
});
- describe('registerAppStatusUpdater', () => {
+ describe('registerAppUpdater', () => {
it('updates status fields', async () => {
const setup = service.setup(setupDeps);
@@ -413,6 +415,36 @@ describe('#setup()', () => {
})
);
});
+
+ it('allows to update the basePath', async () => {
+ const setup = service.setup(setupDeps);
+
+ const pluginId = Symbol('plugin');
+ setup.register(pluginId, createApp({ id: 'app1' }));
+
+ const updater = new BehaviorSubject(app => ({}));
+ setup.registerAppUpdater(updater);
+
+ const start = await service.start(startDeps);
+ await start.navigateToApp('app1');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined);
+ MockHistory.push.mockClear();
+
+ updater.next(app => ({ defaultPath: 'default-path' }));
+ await start.navigateToApp('app1');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default-path', undefined);
+ MockHistory.push.mockClear();
+
+ updater.next(app => ({ defaultPath: 'another-path' }));
+ await start.navigateToApp('app1');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/another-path', undefined);
+ MockHistory.push.mockClear();
+
+ updater.next(app => ({}));
+ await start.navigateToApp('app1');
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined);
+ MockHistory.push.mockClear();
+ });
});
it("`registerMountContext` calls context container's registerContext", () => {
@@ -676,6 +708,57 @@ describe('#start()', () => {
expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#/hash/router/path', undefined);
});
+ it('preserves trailing slash when path contains a hash', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/app-path' }));
+
+ const { navigateToApp } = await service.start(startDeps);
+ await navigateToApp('app2', { path: '#/' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/', undefined);
+ MockHistory.push.mockClear();
+
+ await navigateToApp('app2', { path: '#/foo/bar/' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/foo/bar/', undefined);
+ MockHistory.push.mockClear();
+
+ await navigateToApp('app2', { path: '/path#/' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/', undefined);
+ MockHistory.push.mockClear();
+
+ await navigateToApp('app2', { path: '/path#/hash/' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/hash/', undefined);
+ MockHistory.push.mockClear();
+
+ await navigateToApp('app2', { path: '/path/' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path', undefined);
+ MockHistory.push.mockClear();
+ });
+
+ it('appends the defaultPath when the path parameter is not specified', async () => {
+ const { register } = service.setup(setupDeps);
+
+ register(Symbol(), createApp({ id: 'app1', defaultPath: 'default/path' }));
+ register(
+ Symbol(),
+ createApp({ id: 'app2', appRoute: '/custom-app-path', defaultPath: '/my-base' })
+ );
+
+ const { navigateToApp } = await service.start(startDeps);
+
+ await navigateToApp('app1', { path: 'defined-path' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/defined-path', undefined);
+
+ await navigateToApp('app1', {});
+ expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default/path', undefined);
+
+ await navigateToApp('app2', { path: 'defined-path' });
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/defined-path', undefined);
+
+ await navigateToApp('app2', {});
+ expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/my-base', undefined);
+ });
+
it('includes state if specified', async () => {
const { register } = service.setup(setupDeps);
diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx
index 1c9492d81c7f68..bafa1932e5e927 100644
--- a/src/core/public/application/application_service.tsx
+++ b/src/core/public/application/application_service.tsx
@@ -46,6 +46,7 @@ import {
Mounter,
} from './types';
import { getLeaveAction, isConfirmAction } from './application_leave';
+import { appendAppPath } from './utils';
interface SetupDeps {
context: ContextSetup;
@@ -81,13 +82,7 @@ const getAppUrl = (mounters: Map, appId: string, path: string =
const appBasePath = mounters.get(appId)?.appRoute
? `/${mounters.get(appId)!.appRoute}`
: `/app/${appId}`;
-
- // Only preppend slash if not a hash or query path
- path = path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
-
- return `${appBasePath}${path}`
- .replace(/\/{2,}/g, '/') // Remove duplicate slashes
- .replace(/\/$/, ''); // Remove trailing slash
+ return appendAppPath(appBasePath, path);
};
const allApplicationsFilter = '__ALL__';
@@ -290,6 +285,9 @@ export class ApplicationService {
},
navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => {
if (await this.shouldNavigate(overlays)) {
+ if (path === undefined) {
+ path = applications$.value.get(appId)?.defaultPath;
+ }
this.appLeaveHandlers.delete(this.currentAppId$.value!);
this.navigate!(getAppUrl(availableMounters, appId, path), state);
this.currentAppId$.next(appId);
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index 318afb652999ef..0734e178033e24 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -66,6 +66,13 @@ export interface AppBase {
*/
navLinkStatus?: AppNavLinkStatus;
+ /**
+ * Allow to define the default path a user should be directed to when navigating to the app.
+ * When defined, this value will be used as a default for the `path` option when calling {@link ApplicationStart.navigateToApp | navigateToApp}`,
+ * and will also be appended to the {@link ChromeNavLink | application navLink} in the navigation bar.
+ */
+ defaultPath?: string;
+
/**
* An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime.
*
@@ -187,7 +194,10 @@ export enum AppNavLinkStatus {
* Defines the list of fields that can be updated via an {@link AppUpdater}.
* @public
*/
-export type AppUpdatableFields = Pick;
+export type AppUpdatableFields = Pick<
+ AppBase,
+ 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath'
+>;
/**
* Updater for applications.
@@ -642,7 +652,8 @@ export interface ApplicationStart {
* Navigate to a given app
*
* @param appId
- * @param options.path - optional path inside application to deep link to
+ * @param options.path - optional path inside application to deep link to.
+ * If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default.
* @param options.state - optional state to forward to the application
*/
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise;
diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts
new file mode 100644
index 00000000000000..7ed0919f88c614
--- /dev/null
+++ b/src/core/public/application/utils.test.ts
@@ -0,0 +1,71 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { removeSlashes, appendAppPath } from './utils';
+
+describe('removeSlashes', () => {
+ it('only removes duplicates by default', () => {
+ expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/');
+ expect(removeSlashes('some/////other//url')).toEqual('some/other/url');
+ });
+
+ it('remove trailing slash when `trailing` is true', () => {
+ expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to');
+ });
+
+ it('remove leading slash when `leading` is true', () => {
+ expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/');
+ });
+
+ it('does not removes duplicates when `duplicates` is false', () => {
+ expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual(
+ 'some//url//to/'
+ );
+ expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual(
+ '/some//url//to'
+ );
+ });
+
+ it('accept mixed options', () => {
+ expect(
+ removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true })
+ ).toEqual('some//url//to');
+ expect(
+ removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true })
+ ).toEqual('some/url/to');
+ });
+});
+
+describe('appendAppPath', () => {
+ it('appends the appBasePath with given path', () => {
+ expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app');
+ });
+
+ it('preserves the trailing slash only if included in the hash', () => {
+ expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/');
+ expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual(
+ '/app/my-app/some-path#/hash/'
+ );
+ expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash');
+ });
+});
diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts
new file mode 100644
index 00000000000000..048f195fe12230
--- /dev/null
+++ b/src/core/public/application/utils.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Utility to remove trailing, leading or duplicate slashes.
+ * By default will only remove duplicates.
+ */
+export const removeSlashes = (
+ url: string,
+ {
+ trailing = false,
+ leading = false,
+ duplicates = true,
+ }: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {}
+): string => {
+ if (duplicates) {
+ url = url.replace(/\/{2,}/g, '/');
+ }
+ if (trailing) {
+ url = url.replace(/\/$/, '');
+ }
+ if (leading) {
+ url = url.replace(/^\//, '');
+ }
+ return url;
+};
+
+export const appendAppPath = (appBasePath: string, path: string = '') => {
+ // Only prepend slash if not a hash or query path
+ path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
+ // Do not remove trailing slash when in hashbang
+ const removeTrailing = path.indexOf('#') === -1;
+ return removeSlashes(`${appBasePath}${path}`, {
+ trailing: removeTrailing,
+ duplicates: true,
+ leading: false,
+ });
+};
diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts
index d0ef2aeb265feb..fb2972735c2b70 100644
--- a/src/core/public/chrome/nav_links/nav_link.ts
+++ b/src/core/public/chrome/nav_links/nav_link.ts
@@ -44,6 +44,12 @@ export interface ChromeNavLink {
*/
readonly baseUrl: string;
+ /**
+ * The route used to open the {@link AppBase.defaultPath | default path } of an application.
+ * If unset, `baseUrl` will be used instead.
+ */
+ readonly url?: string;
+
/**
* An ordinal used to sort nav links relative to one another for display.
*/
@@ -99,18 +105,6 @@ export interface ChromeNavLink {
*/
readonly linkToLastSubUrl?: boolean;
- /**
- * A url that legacy apps can set to deep link into their applications.
- *
- * @internalRemarks
- * Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should
- * be removed once the ApplicationService is implemented and mounting apps. At that
- * time, each app can handle opening to the previous location when they are mounted.
- *
- * @deprecated
- */
- readonly url?: string;
-
/**
* Indicates whether or not this app is currently on the screen.
*
diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts
index 23fdabe0f34301..4c319873af804e 100644
--- a/src/core/public/chrome/nav_links/to_nav_link.test.ts
+++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts
@@ -85,6 +85,38 @@ describe('toNavLink', () => {
expect(link.properties.baseUrl).toEqual('http://localhost/base-path/my-route/my-path');
});
+ it('generates the `url` property', () => {
+ let link = toNavLink(
+ app({
+ appRoute: '/my-route/my-path',
+ }),
+ basePath
+ );
+ expect(link.properties.url).toEqual('http://localhost/base-path/my-route/my-path');
+
+ link = toNavLink(
+ app({
+ appRoute: '/my-route/my-path',
+ defaultPath: 'some/default/path',
+ }),
+ basePath
+ );
+ expect(link.properties.url).toEqual(
+ 'http://localhost/base-path/my-route/my-path/some/default/path'
+ );
+ });
+
+ it('does not generate `url` for legacy app', () => {
+ const link = toNavLink(
+ legacyApp({
+ appUrl: '/my-legacy-app/#foo',
+ defaultPath: '/some/default/path',
+ }),
+ basePath
+ );
+ expect(link.properties.url).toBeUndefined();
+ });
+
it('uses appUrl when converting legacy applications', () => {
expect(
toNavLink(
diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts
index 18e4b7b26b6ba1..f79b1df77f8e1c 100644
--- a/src/core/public/chrome/nav_links/to_nav_link.ts
+++ b/src/core/public/chrome/nav_links/to_nav_link.ts
@@ -20,9 +20,11 @@
import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application';
import { IBasePath } from '../../http';
import { NavLinkWrapper } from './nav_link';
+import { appendAppPath } from '../../application/utils';
export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper {
const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default;
+ const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!);
return new NavLinkWrapper({
...app,
hidden: useAppStatus
@@ -30,9 +32,12 @@ export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWra
: app.navLinkStatus === AppNavLinkStatus.hidden,
disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled,
legacy: isLegacyApp(app),
- baseUrl: isLegacyApp(app)
- ? relativeToAbsolute(basePath.prepend(app.appUrl))
- : relativeToAbsolute(basePath.prepend(app.appRoute!)),
+ baseUrl: relativeToAbsolute(baseUrl),
+ ...(isLegacyApp(app)
+ ? {}
+ : {
+ url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)),
+ }),
});
}
diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx
index 52b59c53b658c0..d97ef477c2ee0c 100644
--- a/src/core/public/chrome/ui/header/nav_link.tsx
+++ b/src/core/public/chrome/ui/header/nav_link.tsx
@@ -53,7 +53,7 @@ export function euiNavLink(
order,
tooltip,
} = navLink;
- let href = navLink.baseUrl;
+ let href = navLink.url ?? navLink.baseUrl;
if (legacy) {
href = url && !active ? url : baseUrl;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index b92bb209d26073..af06b207889c22 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -36,6 +36,7 @@ export interface AppBase {
capabilities?: Partial;
category?: AppCategory;
chromeless?: boolean;
+ defaultPath?: string;
euiIconType?: string;
icon?: string;
id: string;
@@ -168,7 +169,7 @@ export enum AppStatus {
export type AppUnmount = () => void;
// @public
-export type AppUpdatableFields = Pick;
+export type AppUpdatableFields = Pick;
// @public
export type AppUpdater = (app: AppBase) => Partial | undefined;
@@ -290,7 +291,6 @@ export interface ChromeNavLink {
readonly subUrlBase?: string;
readonly title: string;
readonly tooltip?: string;
- // @deprecated
readonly url?: string;
}
diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts
index 466d399f653cd6..f7274740ea5fe3 100644
--- a/src/core/server/saved_objects/migrations/core/index.ts
+++ b/src/core/server/saved_objects/migrations/core/index.ts
@@ -21,5 +21,5 @@ export { DocumentMigrator } from './document_migrator';
export { IndexMigrator } from './index_migrator';
export { buildActiveMappings } from './build_active_mappings';
export { CallCluster } from './call_cluster';
-export { LogFn } from './migration_logger';
+export { LogFn, SavedObjectsMigrationLogger } from './migration_logger';
export { MigrationResult, MigrationStatus } from './migration_coordinator';
diff --git a/src/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts
new file mode 100644
index 00000000000000..76a890d26bfa0c
--- /dev/null
+++ b/src/core/server/saved_objects/migrations/mocks.ts
@@ -0,0 +1,43 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SavedObjectMigrationContext } from './types';
+import { SavedObjectsMigrationLogger } from './core';
+
+const createLoggerMock = (): jest.Mocked => {
+ const mock = {
+ debug: jest.fn(),
+ info: jest.fn(),
+ warning: jest.fn(),
+ warn: jest.fn(),
+ };
+
+ return mock;
+};
+
+const createContextMock = (): jest.Mocked => {
+ const mock = {
+ log: createLoggerMock(),
+ };
+ return mock;
+};
+
+export const migrationMocks = {
+ createContext: createContextMock,
+};
diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts
index 6bc085dde872e3..85f15b4c18b66a 100644
--- a/src/core/server/saved_objects/migrations/types.ts
+++ b/src/core/server/saved_objects/migrations/types.ts
@@ -26,23 +26,37 @@ import { SavedObjectsMigrationLogger } from './core/migration_logger';
*
* @example
* ```typescript
- * const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => {
- * if(doc.attributes.someProp === null) {
- * log.warn('Skipping migration');
- * } else {
- * doc.attributes.someProp = migrateProperty(doc.attributes.someProp);
- * }
+ * interface TypeV1Attributes {
+ * someKey: string;
+ * obsoleteProperty: number;
+ * }
*
- * return doc;
+ * interface TypeV2Attributes {
+ * someKey: string;
+ * newProperty: string;
* }
+ *
+ * const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => {
+ * const { obsoleteProperty, ...otherAttributes } = doc.attributes;
+ * // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input
+ * // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete
+ * // attributes are not present on the returned doc.
+ * return {
+ * ...doc,
+ * attributes: {
+ * ...otherAttributes,
+ * newProperty: migrate(obsoleteProperty),
+ * },
+ * };
+ * };
* ```
*
* @public
*/
-export type SavedObjectMigrationFn = (
- doc: SavedObjectUnsanitizedDoc,
+export type SavedObjectMigrationFn = (
+ doc: SavedObjectUnsanitizedDoc,
context: SavedObjectMigrationContext
-) => SavedObjectUnsanitizedDoc;
+) => SavedObjectUnsanitizedDoc;
/**
* Migration context provided when invoking a {@link SavedObjectMigrationFn | migration handler}
diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts
index 7ba4613c857d7b..4e1f5981d6a410 100644
--- a/src/core/server/saved_objects/saved_objects_service.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_service.mock.ts
@@ -31,6 +31,7 @@ import { savedObjectsClientProviderMock } from './service/lib/scoped_client_prov
import { savedObjectsRepositoryMock } from './service/lib/repository.mock';
import { savedObjectsClientMock } from './service/saved_objects_client.mock';
import { typeRegistryMock } from './saved_objects_type_registry.mock';
+import { migrationMocks } from './migrations/mocks';
import { ServiceStatusLevels } from '../status';
type SavedObjectsServiceContract = PublicMethodsOf;
@@ -105,4 +106,5 @@ export const savedObjectsServiceMock = {
createSetupContract: createSetupContractMock,
createInternalStartContract: createInternalStartContractMock,
createStartContract: createStartContractMock,
+ createMigrationContext: migrationMocks.createContext,
};
diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts
index a33e16895078ed..acd2c7b5284aa2 100644
--- a/src/core/server/saved_objects/serialization/types.ts
+++ b/src/core/server/saved_objects/serialization/types.ts
@@ -47,8 +47,8 @@ export interface SavedObjectsRawDocSource {
/**
* Saved Object base document
*/
-interface SavedObjectDoc {
- attributes: any;
+interface SavedObjectDoc {
+ attributes: T;
id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional
type: string;
namespace?: string;
@@ -69,7 +69,7 @@ interface Referencable {
*
* @public
*/
-export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
+export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
/**
* Describes Saved Object documents that have passed through the migration
@@ -77,4 +77,4 @@ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
*
* @public
*/
-export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable;
+export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable;
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index dc1c9d379d508a..e8b77a8570291e 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1680,7 +1680,7 @@ export interface SavedObjectMigrationContext {
}
// @public
-export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc;
+export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc;
// @public
export interface SavedObjectMigrationMap {
@@ -1708,7 +1708,7 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti
// Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts
//
// @public
-export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable;
+export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable;
// @public (undocumented)
export interface SavedObjectsBaseOptions {
@@ -2311,7 +2311,7 @@ export class SavedObjectTypeRegistry {
}
// @public
-export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
+export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial;
// @public
export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest;
diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js
index 43a2cbd78c5023..c5387590fcf661 100644
--- a/src/dev/jest/config.js
+++ b/src/dev/jest/config.js
@@ -40,6 +40,7 @@ export default {
],
collectCoverageFrom: [
'src/plugins/**/*.{ts,tsx}',
+ '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*',
'!src/plugins/**/*.d.ts',
'packages/kbn-ui-framework/src/components/**/*.js',
'!packages/kbn-ui-framework/src/components/index.js',
diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 5f6b67ee6ad202..7de054f2eaa9c8 100644
--- a/src/plugins/dashboard/public/plugin.tsx
+++ b/src/plugins/dashboard/public/plugin.tsx
@@ -141,10 +141,14 @@ export class DashboardPlugin
if (share) {
share.urlGenerators.registerUrlGenerator(
- createDirectAccessDashboardLinkGenerator(async () => ({
- appBasePath: (await startServices)[0].application.getUrlForApp('dashboard'),
- useHashedUrl: (await startServices)[0].uiSettings.get('state:storeInSessionStorage'),
- }))
+ createDirectAccessDashboardLinkGenerator(async () => {
+ const [coreStart, , selfStart] = await startServices;
+ return {
+ appBasePath: coreStart.application.getUrlForApp('dashboard'),
+ useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'),
+ savedDashboardLoader: selfStart.getSavedDashboardLoader(),
+ };
+ })
);
}
diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts
index d48aacc1d8c1e4..248a3f991d6cbf 100644
--- a/src/plugins/dashboard/public/url_generator.test.ts
+++ b/src/plugins/dashboard/public/url_generator.test.ts
@@ -21,10 +21,33 @@ import { createDirectAccessDashboardLinkGenerator } from './url_generator';
import { hashedItemStore } from '../../kibana_utils/public';
// eslint-disable-next-line
import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock';
-import { esFilters } from '../../data/public';
+import { esFilters, Filter } from '../../data/public';
+import { SavedObjectLoader } from '../../saved_objects/public';
const APP_BASE_PATH: string = 'xyz/app/kibana';
+const createMockDashboardLoader = (
+ dashboardToFilters: {
+ [dashboardId: string]: () => Filter[];
+ } = {}
+) => {
+ return {
+ get: async (dashboardId: string) => {
+ return {
+ searchSource: {
+ getField: (field: string) => {
+ if (field === 'filter')
+ return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : [];
+ throw new Error(
+ `createMockDashboardLoader > searchSource > getField > ${field} is not mocked`
+ );
+ },
+ },
+ };
+ },
+ } as SavedObjectLoader;
+};
+
describe('dashboard url generator', () => {
beforeEach(() => {
// @ts-ignore
@@ -33,7 +56,11 @@ describe('dashboard url generator', () => {
test('creates a link to a saved dashboard', async () => {
const generator = createDirectAccessDashboardLinkGenerator(() =>
- Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader(),
+ })
);
const url = await generator.createUrl!({});
expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard?_a=()&_g=()"`);
@@ -41,7 +68,11 @@ describe('dashboard url generator', () => {
test('creates a link with global time range set up', async () => {
const generator = createDirectAccessDashboardLinkGenerator(() =>
- Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader(),
+ })
);
const url = await generator.createUrl!({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
@@ -53,7 +84,11 @@ describe('dashboard url generator', () => {
test('creates a link with filters, time range, refresh interval and query to a saved object', async () => {
const generator = createDirectAccessDashboardLinkGenerator(() =>
- Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader(),
+ })
);
const url = await generator.createUrl!({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
@@ -89,7 +124,11 @@ describe('dashboard url generator', () => {
test('if no useHash setting is given, uses the one was start services', async () => {
const generator = createDirectAccessDashboardLinkGenerator(() =>
- Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true })
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: true,
+ savedDashboardLoader: createMockDashboardLoader(),
+ })
);
const url = await generator.createUrl!({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
@@ -99,7 +138,11 @@ describe('dashboard url generator', () => {
test('can override a false useHash ui setting', async () => {
const generator = createDirectAccessDashboardLinkGenerator(() =>
- Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false })
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader(),
+ })
);
const url = await generator.createUrl!({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
@@ -110,7 +153,11 @@ describe('dashboard url generator', () => {
test('can override a true useHash ui setting', async () => {
const generator = createDirectAccessDashboardLinkGenerator(() =>
- Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true })
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: true,
+ savedDashboardLoader: createMockDashboardLoader(),
+ })
);
const url = await generator.createUrl!({
timeRange: { to: 'now', from: 'now-15m', mode: 'relative' },
@@ -118,4 +165,150 @@ describe('dashboard url generator', () => {
});
expect(url.indexOf('relative')).toBeGreaterThan(1);
});
+
+ describe('preserving saved filters', () => {
+ const savedFilter1 = {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'savedfilter1' },
+ };
+
+ const savedFilter2 = {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'savedfilter2' },
+ };
+
+ const appliedFilter = {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'appliedfilter' },
+ };
+
+ test('attaches filters from destination dashboard', async () => {
+ const generator = createDirectAccessDashboardLinkGenerator(() =>
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader({
+ ['dashboard1']: () => [savedFilter1],
+ ['dashboard2']: () => [savedFilter2],
+ }),
+ })
+ );
+
+ const urlToDashboard1 = await generator.createUrl!({
+ dashboardId: 'dashboard1',
+ filters: [appliedFilter],
+ });
+
+ expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter'));
+
+ const urlToDashboard2 = await generator.createUrl!({
+ dashboardId: 'dashboard2',
+ filters: [appliedFilter],
+ });
+
+ expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2'));
+ expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter'));
+ });
+
+ test("doesn't fail if can't retrieve filters from destination dashboard", async () => {
+ const generator = createDirectAccessDashboardLinkGenerator(() =>
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader({
+ ['dashboard1']: () => {
+ throw new Error('Not found');
+ },
+ }),
+ })
+ );
+
+ const url = await generator.createUrl!({
+ dashboardId: 'dashboard1',
+ filters: [appliedFilter],
+ });
+
+ expect(url).not.toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(url).toEqual(expect.stringContaining('query:appliedfilter'));
+ });
+
+ test('can enforce empty filters', async () => {
+ const generator = createDirectAccessDashboardLinkGenerator(() =>
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader({
+ ['dashboard1']: () => [savedFilter1],
+ }),
+ })
+ );
+
+ const url = await generator.createUrl!({
+ dashboardId: 'dashboard1',
+ filters: [],
+ preserveSavedFilters: false,
+ });
+
+ expect(url).not.toEqual(expect.stringContaining('query:savedfilter1'));
+ expect(url).not.toEqual(expect.stringContaining('query:appliedfilter'));
+ expect(url).toMatchInlineSnapshot(
+ `"xyz/app/kibana#/dashboard/dashboard1?_a=(filters:!())&_g=(filters:!())"`
+ );
+ });
+
+ test('no filters in result url if no filters applied', async () => {
+ const generator = createDirectAccessDashboardLinkGenerator(() =>
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader({
+ ['dashboard1']: () => [savedFilter1],
+ }),
+ })
+ );
+
+ const url = await generator.createUrl!({
+ dashboardId: 'dashboard1',
+ });
+ expect(url).not.toEqual(expect.stringContaining('filters'));
+ expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard/dashboard1?_a=()&_g=()"`);
+ });
+
+ test('can turn off preserving filters', async () => {
+ const generator = createDirectAccessDashboardLinkGenerator(() =>
+ Promise.resolve({
+ appBasePath: APP_BASE_PATH,
+ useHashedUrl: false,
+ savedDashboardLoader: createMockDashboardLoader({
+ ['dashboard1']: () => [savedFilter1],
+ }),
+ })
+ );
+ const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({
+ dashboardId: 'dashboard1',
+ filters: [appliedFilter],
+ preserveSavedFilters: false,
+ });
+
+ expect(urlWithPreservedFiltersTurnedOff).not.toEqual(
+ expect.stringContaining('query:savedfilter1')
+ );
+ expect(urlWithPreservedFiltersTurnedOff).toEqual(
+ expect.stringContaining('query:appliedfilter')
+ );
+ });
+ });
});
diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts
index 0fdf395e75bcaa..6f121ceb2d3731 100644
--- a/src/plugins/dashboard/public/url_generator.ts
+++ b/src/plugins/dashboard/public/url_generator.ts
@@ -27,6 +27,7 @@ import {
} from '../../data/public';
import { setStateToKbnUrl } from '../../kibana_utils/public';
import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public';
+import { SavedObjectLoader } from '../../saved_objects/public';
export const STATE_STORAGE_KEY = '_a';
export const GLOBAL_STATE_STORAGE_KEY = '_g';
@@ -64,10 +65,22 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{
* whether to hash the data in the url to avoid url length issues.
*/
useHash?: boolean;
+
+ /**
+ * When `true` filters from saved filters from destination dashboard as merged with applied filters
+ * When `false` applied filters take precedence and override saved filters
+ *
+ * true is default
+ */
+ preserveSavedFilters?: boolean;
}>;
export const createDirectAccessDashboardLinkGenerator = (
- getStartServices: () => Promise<{ appBasePath: string; useHashedUrl: boolean }>
+ getStartServices: () => Promise<{
+ appBasePath: string;
+ useHashedUrl: boolean;
+ savedDashboardLoader: SavedObjectLoader;
+ }>
): UrlGeneratorsDefinition => ({
id: DASHBOARD_APP_URL_GENERATOR,
createUrl: async state => {
@@ -76,6 +89,19 @@ export const createDirectAccessDashboardLinkGenerator = (
const appBasePath = startServices.appBasePath;
const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`;
+ const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => {
+ if (state.preserveSavedFilters === false) return [];
+ if (!state.dashboardId) return [];
+ try {
+ const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId);
+ return dashboard?.searchSource?.getField('filter') ?? [];
+ } catch (e) {
+ // in case dashboard is missing, built the url without those filters
+ // dashboard app will handle redirect to landing page with toast message
+ return [];
+ }
+ };
+
const cleanEmptyKeys = (stateObj: Record) => {
Object.keys(stateObj).forEach(key => {
if (stateObj[key] === undefined) {
@@ -85,11 +111,18 @@ export const createDirectAccessDashboardLinkGenerator = (
return stateObj;
};
+ // leave filters `undefined` if no filters was applied
+ // in this case dashboard will restore saved filters on its own
+ const filters = state.filters && [
+ ...(await getSavedFiltersFromDestinationDashboardIfNeeded()),
+ ...state.filters,
+ ];
+
const appStateUrl = setStateToKbnUrl(
STATE_STORAGE_KEY,
cleanEmptyKeys({
query: state.query,
- filters: state.filters?.filter(f => !esFilters.isFilterPinned(f)),
+ filters: filters?.filter(f => !esFilters.isFilterPinned(f)),
}),
{ useHash },
`${appBasePath}#/${hash}`
@@ -99,7 +132,7 @@ export const createDirectAccessDashboardLinkGenerator = (
GLOBAL_STATE_STORAGE_KEY,
cleanEmptyKeys({
time: state.timeRange,
- filters: state.filters?.filter(f => esFilters.isFilterPinned(f)),
+ filters: filters?.filter(f => esFilters.isFilterPinned(f)),
refreshInterval: state.refreshInterval,
}),
{ useHash },
diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts
index 9829498118cc0a..22ed18f75c652a 100644
--- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts
+++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts
@@ -18,14 +18,17 @@
*/
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { savedObjectsServiceMock } from '../../../../core/server/mocks';
import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations';
+const contextMock = savedObjectsServiceMock.createMigrationContext();
+
describe('dashboard', () => {
describe('7.0.0', () => {
const migration = migrations['7.0.0'];
test('skips error on empty object', () => {
- expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(`
+ expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(`
Object {
"references": Array [],
}
@@ -44,7 +47,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- const migratedDoc = migration(doc);
+ const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
@@ -83,7 +86,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- const migratedDoc = migration(doc);
+ const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
@@ -122,7 +125,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
@@ -160,7 +163,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"kibanaSavedObjectMeta": Object {
@@ -198,7 +201,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- const migratedDoc = migration(doc);
+ const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
@@ -237,7 +240,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- const migratedDoc = migration(doc);
+ const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
@@ -291,7 +294,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
- const migratedDoc = migration(doc);
+ const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
@@ -331,7 +334,7 @@ Object {
panelsJSON: 123,
},
} as SavedObjectUnsanitizedDoc;
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": 123,
@@ -349,7 +352,7 @@ Object {
panelsJSON: '{123abc}',
},
} as SavedObjectUnsanitizedDoc;
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "{123abc}",
@@ -367,7 +370,7 @@ Object {
panelsJSON: '{}',
},
} as SavedObjectUnsanitizedDoc;
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "{}",
@@ -385,7 +388,7 @@ Object {
panelsJSON: '[{"id":"123"}]',
},
} as SavedObjectUnsanitizedDoc;
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "[{\\"id\\":\\"123\\"}]",
@@ -403,7 +406,7 @@ Object {
panelsJSON: '[{"type":"visualization"}]',
},
} as SavedObjectUnsanitizedDoc;
- expect(migration(doc)).toMatchInlineSnapshot(`
+ expect(migration(doc, contextMock)).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"panelsJSON": "[{\\"type\\":\\"visualization\\"}]",
@@ -422,7 +425,7 @@ Object {
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
} as SavedObjectUnsanitizedDoc;
- const migratedDoc = migration(doc);
+ const migratedDoc = migration(doc, contextMock);
expect(migratedDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts
index 7c1d0568cd3d71..4f7945d6dd6017 100644
--- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts
+++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts
@@ -19,7 +19,7 @@
import { get, flow } from 'lodash';
-import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server';
+import { SavedObjectMigrationFn } from 'kibana/server';
import { migrations730 } from './migrations_730';
import { migrateMatchAllQuery } from './migrate_match_all_query';
import { DashboardDoc700To720 } from '../../common';
@@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) {
doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource);
}
-const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => {
+const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => {
// Set new "references" attribute
doc.references = doc.references || [];
@@ -111,7 +111,7 @@ export const dashboardSavedObjectTypeMigrations = {
* in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7
* only contained the 6.7.2 migration and not the 7.0.1 migration.
*/
- '6.7.2': flow(migrateMatchAllQuery),
- '7.0.0': flow<(doc: SavedObjectUnsanitizedDoc) => DashboardDoc700To720>(migrations700),
- '7.3.0': flow(migrations730),
+ '6.7.2': flow>(migrateMatchAllQuery),
+ '7.0.0': flow>(migrations700),
+ '7.3.0': flow>(migrations730),
};
diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts
index 5b8582bf821ef5..db2fbeb2788023 100644
--- a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts
+++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts
@@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server';
import { get } from 'lodash';
import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common';
-export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
+export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
if (searchSourceJSON) {
diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts
index aa744324428a41..a58df547fa5224 100644
--- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts
+++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts
@@ -17,19 +17,13 @@
* under the License.
*/
+import { savedObjectsServiceMock } from '../../../../core/server/mocks';
import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations';
import { migrations730 } from './migrations_730';
import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common';
import { RawSavedDashboardPanel730ToLatest } from '../../common';
-const mockContext = {
- log: {
- warning: () => {},
- warn: () => {},
- debug: () => {},
- info: () => {},
- },
-};
+const mockContext = savedObjectsServiceMock.createMigrationContext();
test('dashboard migration 7.3.0 migrates filters to query on search source', () => {
const doc: DashboardDoc700To720 = {
@@ -95,7 +89,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when
},
};
- const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc);
+ const doc700 = migrations['7.0.0'](doc, mockContext);
const newDoc = migrations['7.3.0'](doc700, mockContext);
const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON);
@@ -127,7 +121,7 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => {
},
};
- const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc);
+ const doc700 = migrations['7.0.0'](doc, mockContext);
const newDoc = migrations['7.3.0'](doc700, mockContext);
const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON);
diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts
index 698edbf9cd6a8e..e21d27a70e02ad 100644
--- a/src/plugins/data/common/index_patterns/types.ts
+++ b/src/plugins/data/common/index_patterns/types.ts
@@ -26,6 +26,7 @@ export interface IIndexPattern {
id?: string;
type?: string;
timeFieldName?: string;
+ getTimeField?(): IFieldType | undefined;
fieldFormatMap?: Record<
string,
{
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index bbf7d2a8eabebc..b0bb911dca9e51 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -676,7 +676,10 @@ export function getSearchErrorType({ message }: Pick): "
// Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined;
+export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: {
+ forceNow?: Date;
+ fieldName?: string;
+}): import("../..").RangeFilter | undefined;
// Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -814,6 +817,8 @@ export interface IIndexPattern {
// (undocumented)
fields: IFieldType[];
// (undocumented)
+ getTimeField?(): IFieldType | undefined;
+ // (undocumented)
id?: string;
// (undocumented)
timeFieldName?: string;
diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts
index a8eb3a3fe81023..4dba157a6f5546 100644
--- a/src/plugins/data/public/query/timefilter/get_time.test.ts
+++ b/src/plugins/data/public/query/timefilter/get_time.test.ts
@@ -51,5 +51,43 @@ describe('get_time', () => {
});
clock.restore();
});
+
+ test('build range filter for non-primary field', () => {
+ const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf());
+
+ const filter = getTime(
+ {
+ id: 'test',
+ title: 'test',
+ timeFieldName: 'date',
+ fields: [
+ {
+ name: 'date',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ filterable: true,
+ },
+ {
+ name: 'myCustomDate',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ filterable: true,
+ },
+ ],
+ } as any,
+ { from: 'now-60y', to: 'now' },
+ { fieldName: 'myCustomDate' }
+ );
+ expect(filter!.range.myCustomDate).toEqual({
+ gte: '1940-02-01T00:00:00.000Z',
+ lte: '2000-02-01T00:00:00.000Z',
+ format: 'strict_date_optional_time',
+ });
+ clock.restore();
+ });
});
});
diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts
index fa154061890412..9cdd25d3213cee 100644
--- a/src/plugins/data/public/query/timefilter/get_time.ts
+++ b/src/plugins/data/public/query/timefilter/get_time.ts
@@ -19,7 +19,7 @@
import dateMath from '@elastic/datemath';
import { IIndexPattern } from '../..';
-import { TimeRange, IFieldType, buildRangeFilter } from '../../../common';
+import { TimeRange, buildRangeFilter } from '../../../common';
interface CalculateBoundsOptions {
forceNow?: Date;
@@ -35,18 +35,27 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp
export function getTime(
indexPattern: IIndexPattern | undefined,
timeRange: TimeRange,
+ options?: { forceNow?: Date; fieldName?: string }
+) {
+ return createTimeRangeFilter(
+ indexPattern,
+ timeRange,
+ options?.fieldName || indexPattern?.timeFieldName,
+ options?.forceNow
+ );
+}
+
+function createTimeRangeFilter(
+ indexPattern: IIndexPattern | undefined,
+ timeRange: TimeRange,
+ fieldName?: string,
forceNow?: Date
) {
if (!indexPattern) {
- // in CI, we sometimes seem to fail here.
return;
}
-
- const timefield: IFieldType | undefined = indexPattern.fields.find(
- field => field.name === indexPattern.timeFieldName
- );
-
- if (!timefield) {
+ const field = indexPattern.fields.find(f => f.name === (fieldName || indexPattern.timeFieldName));
+ if (!field) {
return;
}
@@ -55,7 +64,7 @@ export function getTime(
return;
}
return buildRangeFilter(
- timefield,
+ field,
{
...(bounds.min && { gte: bounds.min.toISOString() }),
...(bounds.max && { lte: bounds.max.toISOString() }),
diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts
index a6260e782c12ff..034af03842ab8d 100644
--- a/src/plugins/data/public/query/timefilter/index.ts
+++ b/src/plugins/data/public/query/timefilter/index.ts
@@ -22,6 +22,6 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service';
export * from './types';
export { Timefilter, TimefilterContract } from './timefilter';
export { TimeHistory, TimeHistoryContract } from './time_history';
-export { getTime } from './get_time';
+export { getTime, calculateBounds } from './get_time';
export { changeTimeFilter } from './lib/change_time_filter';
export { extractTimeFilter } from './lib/extract_time_filter';
diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts
index 4fbdac47fb3b02..86ef69be572a9c 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.ts
@@ -164,7 +164,9 @@ export class Timefilter {
};
public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => {
- return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow());
+ return getTime(indexPattern, timeRange ? timeRange : this._time, {
+ forceNow: this.getForceNow(),
+ });
};
public getBounds(): TimeRangeBounds {
diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts
index 57f3aa85ad9443..3ecdc17cb57f3f 100644
--- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts
+++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts
@@ -45,7 +45,7 @@ const updateTimeBuckets = (
customBuckets?: IBucketDateHistogramAggConfig['buckets']
) => {
const bounds =
- agg.params.timeRange && agg.fieldIsTimeField()
+ agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto')
? timefilter.calculateBounds(agg.params.timeRange)
: undefined;
const buckets = customBuckets || agg.buckets;
diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts
index 087b83127079f9..eec75b08411330 100644
--- a/src/plugins/data/public/search/expressions/esaggs.ts
+++ b/src/plugins/data/public/search/expressions/esaggs.ts
@@ -32,8 +32,15 @@ import { Adapters } from '../../../../../plugins/inspector/public';
import { IAggConfigs } from '../aggs';
import { ISearchSource } from '../search_source';
import { tabifyAggResponse } from '../tabify';
-import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common';
-import { FilterManager, getTime } from '../../query';
+import {
+ Filter,
+ Query,
+ serializeFieldFormat,
+ TimeRange,
+ IIndexPattern,
+ isRangeFilter,
+} from '../../../common';
+import { FilterManager, calculateBounds, getTime } from '../../query';
import { getSearchService, getQueryService, getIndexPatterns } from '../../services';
import { buildTabularInspectorData } from './build_tabular_inspector_data';
import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils';
@@ -42,6 +49,8 @@ export interface RequestHandlerParams {
searchSource: ISearchSource;
aggs: IAggConfigs;
timeRange?: TimeRange;
+ timeFields?: string[];
+ indexPattern?: IIndexPattern;
query?: Query;
filters?: Filter[];
forceFetch: boolean;
@@ -65,12 +74,15 @@ interface Arguments {
partialRows: boolean;
includeFormatHints: boolean;
aggConfigs: string;
+ timeFields?: string[];
}
const handleCourierRequest = async ({
searchSource,
aggs,
timeRange,
+ timeFields,
+ indexPattern,
query,
filters,
forceFetch,
@@ -111,9 +123,19 @@ const handleCourierRequest = async ({
return aggs.onSearchRequestStart(paramSearchSource, options);
});
- if (timeRange) {
+ // If timeFields have been specified, use the specified ones, otherwise use primary time field of index
+ // pattern if it's available.
+ const defaultTimeField = indexPattern?.getTimeField?.();
+ const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : [];
+ const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields;
+
+ // If a timeRange has been specified and we had at least one timeField available, create range
+ // filters for that those time fields
+ if (timeRange && allTimeFields.length > 0) {
timeFilterSearchSource.setField('filter', () => {
- return getTime(searchSource.getField('index'), timeRange);
+ return allTimeFields
+ .map(fieldName => getTime(indexPattern, timeRange, { fieldName }))
+ .filter(isRangeFilter);
});
}
@@ -181,11 +203,13 @@ const handleCourierRequest = async ({
(searchSource as any).finalResponse = resp;
- const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null;
+ const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null;
const tabifyParams = {
metricsAtAllLevels,
partialRows,
- timeRange: parsedTimeRange ? parsedTimeRange.range : undefined,
+ timeRange: parsedTimeRange
+ ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields }
+ : undefined,
};
const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams });
@@ -242,6 +266,11 @@ export const esaggs = (): ExpressionFunctionDefinition {
const check = (aggResp: any, count: number, keys: string[]) => {
@@ -187,9 +192,9 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 150,
- lte: 350,
- name: 'date',
+ from: moment(150),
+ to: moment(350),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
@@ -204,9 +209,9 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 150,
- lte: 350,
- name: 'date',
+ from: moment(150),
+ to: moment(350),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
@@ -221,9 +226,9 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 100,
- lte: 400,
- name: 'date',
+ from: moment(100),
+ to: moment(400),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
@@ -238,13 +243,47 @@ describe('Buckets wrapper', () => {
},
};
const timeRange = {
- gte: 150,
- lte: 350,
- name: 'date',
+ from: moment(150),
+ to: moment(350),
+ timeFields: ['date'],
};
const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
expect(buckets).toHaveLength(4);
});
+
+ test('does drop bucket when multiple time fields specified', () => {
+ const aggParams = {
+ drop_partials: true,
+ field: {
+ name: 'date',
+ },
+ };
+ const timeRange = {
+ from: moment(100),
+ to: moment(350),
+ timeFields: ['date', 'other_datefield'],
+ };
+ const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
+
+ expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([100, 200]);
+ });
+
+ test('does not drop bucket when no timeFields have been specified', () => {
+ const aggParams = {
+ drop_partials: true,
+ field: {
+ name: 'date',
+ },
+ };
+ const timeRange = {
+ from: moment(100),
+ to: moment(350),
+ timeFields: [],
+ };
+ const buckets = new TabifyBuckets(aggResp, aggParams, timeRange);
+
+ expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([0, 100, 200, 300]);
+ });
});
});
diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/public/search/tabify/buckets.ts
index 971e820ac6ddf6..cd52a09caeaade 100644
--- a/src/plugins/data/public/search/tabify/buckets.ts
+++ b/src/plugins/data/public/search/tabify/buckets.ts
@@ -20,7 +20,7 @@
import { get, isPlainObject, keys, findKey } from 'lodash';
import moment from 'moment';
import { IAggConfig } from '../aggs';
-import { AggResponseBucket, TabbedRangeFilterParams } from './types';
+import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types';
type AggParams = IAggConfig['params'] & {
drop_partials: boolean;
@@ -36,7 +36,7 @@ export class TabifyBuckets {
buckets: any;
_keys: any[] = [];
- constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) {
+ constructor(aggResp: any, aggParams?: AggParams, timeRange?: TimeRangeInformation) {
if (aggResp && aggResp.buckets) {
this.buckets = aggResp.buckets;
} else if (aggResp) {
@@ -107,12 +107,12 @@ export class TabifyBuckets {
// dropPartials should only be called if the aggParam setting is enabled,
// and the agg field is the same as the Time Range.
- private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) {
+ private dropPartials(params: AggParams, timeRange?: TimeRangeInformation) {
if (
!timeRange ||
this.buckets.length <= 1 ||
this.objectMode ||
- params.field.name !== timeRange.name
+ !timeRange.timeFields.includes(params.field.name)
) {
return;
}
@@ -120,10 +120,10 @@ export class TabifyBuckets {
const interval = this.buckets[1].key - this.buckets[0].key;
this.buckets = this.buckets.filter((bucket: AggResponseBucket) => {
- if (moment(bucket.key).isBefore(timeRange.gte)) {
+ if (moment(bucket.key).isBefore(timeRange.from)) {
return false;
}
- if (moment(bucket.key + interval).isAfter(timeRange.lte)) {
+ if (moment(bucket.key + interval).isAfter(timeRange.to)) {
return false;
}
return true;
diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/public/search/tabify/tabify.ts
index e93e9890342529..9cb55f94537c56 100644
--- a/src/plugins/data/public/search/tabify/tabify.ts
+++ b/src/plugins/data/public/search/tabify/tabify.ts
@@ -20,7 +20,7 @@
import { get } from 'lodash';
import { TabbedAggResponseWriter } from './response_writer';
import { TabifyBuckets } from './buckets';
-import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types';
+import { TabbedResponseWriterOptions } from './types';
import { AggResponseBucket } from './types';
import { AggGroupNames, IAggConfigs } from '../aggs';
@@ -54,7 +54,7 @@ export function tabifyAggResponse(
switch (agg.type.type) {
case AggGroupNames.Buckets:
const aggBucket = get(bucket, agg.id);
- const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange);
+ const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange);
if (tabifyBuckets.length) {
tabifyBuckets.forEach((subBucket, tabifyBucketKey) => {
@@ -153,20 +153,6 @@ export function tabifyAggResponse(
doc_count: esResponse.hits.total,
};
- let timeRange: TabbedRangeFilterParams | undefined;
-
- // Extract the time range object if provided
- if (respOpts && respOpts.timeRange) {
- const [timeRangeKey] = Object.keys(respOpts.timeRange);
-
- if (timeRangeKey) {
- timeRange = {
- name: timeRangeKey,
- ...respOpts.timeRange[timeRangeKey],
- };
- }
- }
-
collectBucket(aggConfigs, write, topLevelBucket, '', 1);
return write.response();
diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/public/search/tabify/types.ts
index 1e051880d3f19f..72e91eb58c8a97 100644
--- a/src/plugins/data/public/search/tabify/types.ts
+++ b/src/plugins/data/public/search/tabify/types.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { Moment } from 'moment';
import { RangeFilterParams } from '../../../common';
import { IAggConfig } from '../aggs';
@@ -25,11 +26,18 @@ export interface TabbedRangeFilterParams extends RangeFilterParams {
name: string;
}
+/** @internal */
+export interface TimeRangeInformation {
+ from?: Moment;
+ to?: Moment;
+ timeFields: string[];
+}
+
/** @internal **/
export interface TabbedResponseWriterOptions {
metricsAtAllLevels: boolean;
partialRows: boolean;
- timeRange?: { [key: string]: RangeFilterParams };
+ timeRange?: TimeRangeInformation;
}
/** @internal */
diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts
index 7a16386ea484c8..c64f7361a8cf4f 100644
--- a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts
+++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts
@@ -20,7 +20,7 @@
import { flow, omit } from 'lodash';
import { SavedObjectMigrationFn } from 'kibana/server';
-const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({
+const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({
...doc,
attributes: {
...doc.attributes,
@@ -29,7 +29,7 @@ const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc =>
},
});
-const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => {
+const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => {
if (!doc.attributes.fields) return doc;
const fieldsString = doc.attributes.fields;
diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts
index 45fa5e11e2a3d7..c8ded51193c92b 100644
--- a/src/plugins/data/server/saved_objects/search_migrations.ts
+++ b/src/plugins/data/server/saved_objects/search_migrations.ts
@@ -21,7 +21,7 @@ import { flow, get } from 'lodash';
import { SavedObjectMigrationFn } from 'kibana/server';
import { DEFAULT_QUERY_LANGUAGE } from '../../common';
-const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
+const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
if (searchSourceJSON) {
@@ -55,7 +55,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
return doc;
};
-const migrateIndexPattern: SavedObjectMigrationFn = doc => {
+const migrateIndexPattern: SavedObjectMigrationFn = doc => {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
if (typeof searchSourceJSON !== 'string') {
return doc;
@@ -97,13 +97,13 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => {
return doc;
};
-const setNewReferences: SavedObjectMigrationFn = (doc, context) => {
+const setNewReferences: SavedObjectMigrationFn = (doc, context) => {
doc.references = doc.references || [];
// Migrate index pattern
return migrateIndexPattern(doc, context);
};
-const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => {
+const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => {
const sort = get(doc, 'attributes.sort');
if (!sort) return doc;
@@ -122,7 +122,7 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => {
};
export const searchSavedObjectTypeMigrations = {
- '6.7.2': flow(migrateMatchAllQuery),
- '7.0.0': flow(setNewReferences),
- '7.4.0': flow(migrateSearchSortToNestedArray),
+ '6.7.2': flow>(migrateMatchAllQuery),
+ '7.0.0': flow>(setNewReferences),
+ '7.4.0': flow>(migrateSearchSortToNestedArray),
};
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 5d94b6516c2bab..df4ba23244b4dc 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -408,6 +408,8 @@ export interface IIndexPattern {
// (undocumented)
fields: IFieldType[];
// (undocumented)
+ getTimeField?(): IFieldType | undefined;
+ // (undocumented)
id?: string;
// (undocumented)
timeFieldName?: string;
diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts
index 34922976f22ff5..1e5508b44ee0ec 100644
--- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts
+++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts
@@ -20,7 +20,7 @@
import { flow } from 'lodash';
import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server';
-const resetCount: SavedObjectMigrationFn = doc => ({
+const resetCount: SavedObjectMigrationFn = doc => ({
...doc,
attributes: {
...doc.attributes,
diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts
index 94473e35a942d5..f6455d0c1e43f4 100644
--- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts
+++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts
@@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server';
import { cloneDeep, get, omit, has, flow } from 'lodash';
import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common';
-const migrateIndexPattern: SavedObjectMigrationFn = doc => {
+const migrateIndexPattern: SavedObjectMigrationFn = doc => {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
if (typeof searchSourceJSON !== 'string') {
return doc;
@@ -64,7 +64,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => {
};
// [TSVB] Migrate percentile-rank aggregation (value -> values)
-const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => {
+const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
@@ -100,7 +100,7 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => {
};
// [TSVB] Remove stale opperator key
-const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => {
+const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
@@ -132,7 +132,7 @@ const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => {
};
// Migrate date histogram aggregation (remove customInterval)
-const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => {
+const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
@@ -174,7 +174,7 @@ const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => {
return doc;
};
-const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => {
+const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
let visState;
@@ -206,7 +206,7 @@ const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => {
// migrate gauge verticalSplit to alignment
// https://github.com/elastic/kibana/issues/34636
-const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => {
+const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
@@ -241,7 +241,7 @@ const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logge
Path to the series array is thus:
attributes.visState.
*/
-const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => {
+const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => {
// Migrate filters
// If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly
const newDoc = cloneDeep(doc);
@@ -325,7 +325,7 @@ const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger)
return newDoc;
};
-const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => {
+const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => {
// Migrate split_filters in TSVB objects that weren't migrated in 7.3
// If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly
const newDoc = cloneDeep(doc);
@@ -370,7 +370,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc =>
return newDoc;
};
-const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => {
+const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
@@ -402,7 +402,7 @@ const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => {
return doc;
};
-const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => {
+const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
@@ -450,7 +450,7 @@ const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => {
return doc;
};
-const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => {
+const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
@@ -483,12 +483,12 @@ const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger
return doc;
};
-const addDocReferences: SavedObjectMigrationFn = doc => ({
+const addDocReferences: SavedObjectMigrationFn = doc => ({
...doc,
references: doc.references || [],
});
-const migrateSavedSearch: SavedObjectMigrationFn = doc => {
+const migrateSavedSearch: SavedObjectMigrationFn = doc => {
const savedSearchId = get(doc, 'attributes.savedSearchId');
if (savedSearchId && doc.references) {
@@ -505,7 +505,7 @@ const migrateSavedSearch: SavedObjectMigrationFn = doc => {
return doc;
};
-const migrateControls: SavedObjectMigrationFn = doc => {
+const migrateControls: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
@@ -536,7 +536,7 @@ const migrateControls: SavedObjectMigrationFn = doc => {
return doc;
};
-const migrateTableSplits: SavedObjectMigrationFn = doc => {
+const migrateTableSplits: SavedObjectMigrationFn = doc => {
try {
const visState = JSON.parse(doc.attributes.visState);
if (get(visState, 'type') !== 'table') {
@@ -572,7 +572,7 @@ const migrateTableSplits: SavedObjectMigrationFn = doc => {
}
};
-const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
+const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
if (searchSourceJSON) {
@@ -606,7 +606,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
};
// [TSVB] Default color palette is changing, keep the default for older viz
-const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => {
+const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
@@ -649,27 +649,30 @@ export const visualizationSavedObjectTypeMigrations = {
* in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7
* only contained the 6.7.2 migration and not the 7.0.1 migration.
*/
- '6.7.2': flow(migrateMatchAllQuery, removeDateHistogramTimeZones),
- '7.0.0': flow(
+ '6.7.2': flow>(
+ migrateMatchAllQuery,
+ removeDateHistogramTimeZones
+ ),
+ '7.0.0': flow>(
addDocReferences,
migrateIndexPattern,
migrateSavedSearch,
migrateControls,
migrateTableSplits
),
- '7.0.1': flow(removeDateHistogramTimeZones),
- '7.2.0': flow(
+ '7.0.1': flow>(removeDateHistogramTimeZones),
+ '7.2.0': flow>(
migratePercentileRankAggregation,
migrateDateHistogramAggregation
),
- '7.3.0': flow(
+ '7.3.0': flow>(
migrateGaugeVerticalSplitToAlignment,
transformFilterStringToQueryObject,
migrateFiltersAggQuery,
replaceMovAvgToMovFn
),
- '7.3.1': flow(migrateFiltersAggQueryStringQueries),
- '7.4.2': flow(transformSplitFiltersStringToQueryObject),
- '7.7.0': flow(migrateOperatorKeyTypo),
- '7.8.0': flow(migrateTsvbDefaultColorPalettes),
+ '7.3.1': flow>(migrateFiltersAggQueryStringQueries),
+ '7.4.2': flow>(transformSplitFiltersStringToQueryObject),
+ '7.7.0': flow>(migrateOperatorKeyTypo),
+ '7.8.0': flow>(migrateTsvbDefaultColorPalettes),
};
diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js
index c5bfc847d0041f..0c4028905657d5 100644
--- a/test/api_integration/apis/index.js
+++ b/test/api_integration/apis/index.js
@@ -33,5 +33,6 @@ export default function({ loadTestFile }) {
loadTestFile(require.resolve('./status'));
loadTestFile(require.resolve('./stats'));
loadTestFile(require.resolve('./ui_metric'));
+ loadTestFile(require.resolve('./telemetry'));
});
}
diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js
new file mode 100644
index 00000000000000..c79f5cb4708903
--- /dev/null
+++ b/test/api_integration/apis/telemetry/index.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export default function({ loadTestFile }) {
+ describe('Telemetry', () => {
+ loadTestFile(require.resolve('./telemetry_local'));
+ loadTestFile(require.resolve('./opt_in'));
+ loadTestFile(require.resolve('./telemetry_optin_notice_seen'));
+ });
+}
diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts
new file mode 100644
index 00000000000000..e4654ee3985f32
--- /dev/null
+++ b/test/api_integration/apis/telemetry/opt_in.ts
@@ -0,0 +1,123 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+
+import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function optInTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const kibanaServer = getService('kibanaServer');
+ describe('/api/telemetry/v2/optIn API', () => {
+ let defaultAttributes: TelemetrySavedObjectAttributes;
+ let kibanaVersion: any;
+ before(async () => {
+ const kibanaVersionAccessor = kibanaServer.version;
+ kibanaVersion = await kibanaVersionAccessor.get();
+ defaultAttributes =
+ (await getSavedObjectAttributes(supertest).catch(err => {
+ if (err.message === 'expected 200 "OK", got 404 "Not Found"') {
+ return null;
+ }
+ throw err;
+ })) || {};
+
+ expect(typeof kibanaVersion).to.eql('string');
+ expect(kibanaVersion.length).to.be.greaterThan(0);
+ });
+
+ afterEach(async () => {
+ await updateSavedObjectAttributes(supertest, defaultAttributes);
+ });
+
+ it('should support sending false with allowChangingOptInStatus true', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: true,
+ });
+ await postTelemetryV2Optin(supertest, false, 200);
+ const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
+ expect(enabled).to.be(false);
+ expect(lastVersionChecked).to.be(kibanaVersion);
+ });
+
+ it('should support sending true with allowChangingOptInStatus true', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: true,
+ });
+ await postTelemetryV2Optin(supertest, true, 200);
+ const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest);
+ expect(enabled).to.be(true);
+ expect(lastVersionChecked).to.be(kibanaVersion);
+ });
+
+ it('should not support sending false with allowChangingOptInStatus false', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: false,
+ });
+ await postTelemetryV2Optin(supertest, false, 400);
+ });
+
+ it('should not support sending true with allowChangingOptInStatus false', async () => {
+ await updateSavedObjectAttributes(supertest, {
+ ...defaultAttributes,
+ allowChangingOptInStatus: false,
+ });
+ await postTelemetryV2Optin(supertest, true, 400);
+ });
+
+ it('should not support sending null', async () => {
+ await postTelemetryV2Optin(supertest, null, 400);
+ });
+
+ it('should not support sending junk', async () => {
+ await postTelemetryV2Optin(supertest, 42, 400);
+ });
+ });
+}
+
+async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise {
+ const { body } = await supertest
+ .post('/api/telemetry/v2/optIn')
+ .set('kbn-xsrf', 'xxx')
+ .send({ enabled: value })
+ .expect(statusCode);
+
+ return body;
+}
+
+async function updateSavedObjectAttributes(
+ supertest: any,
+ attributes: TelemetrySavedObjectAttributes
+): Promise {
+ return await supertest
+ .post('/api/saved_objects/telemetry/telemetry')
+ .query({ overwrite: true })
+ .set('kbn-xsrf', 'xxx')
+ .send({ attributes })
+ .expect(200);
+}
+
+async function getSavedObjectAttributes(supertest: any): Promise {
+ const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200);
+ return body.attributes;
+}
diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js
new file mode 100644
index 00000000000000..84bfd8a755c116
--- /dev/null
+++ b/test/api_integration/apis/telemetry/telemetry_local.js
@@ -0,0 +1,133 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+import _ from 'lodash';
+
+/*
+ * Create a single-level array with strings for all the paths to values in the
+ * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn
+ * in the tests.
+ */
+function flatKeys(source) {
+ const recursivelyFlatKeys = (obj, path = [], depth = 0) => {
+ return depth < 3 && _.isObject(obj)
+ ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1))
+ : path.join('.');
+ };
+
+ return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b));
+}
+
+export default function({ getService }) {
+ const supertest = getService('supertest');
+
+ describe('/api/telemetry/v2/clusters/_stats', () => {
+ it('should pull local stats and validate data types', async () => {
+ const timeRange = {
+ min: '2018-07-23T22:07:00Z',
+ max: '2018-07-23T22:13:00Z',
+ };
+
+ const { body } = await supertest
+ .post('/api/telemetry/v2/clusters/_stats')
+ .set('kbn-xsrf', 'xxx')
+ .send({ timeRange, unencrypted: true })
+ .expect(200);
+
+ expect(body.length).to.be(1);
+ const stats = body[0];
+ expect(stats.collection).to.be('local');
+ expect(stats.stack_stats.kibana.count).to.be.a('number');
+ expect(stats.stack_stats.kibana.indices).to.be.a('number');
+ expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string');
+ expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1);
+ expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string');
+ expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1);
+ expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false);
+ expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string');
+ expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string');
+ expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object');
+ expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true);
+ expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true);
+ expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false);
+ });
+
+ it('should pull local stats and validate fields', async () => {
+ const timeRange = {
+ min: '2018-07-23T22:07:00Z',
+ max: '2018-07-23T22:13:00Z',
+ };
+
+ const { body } = await supertest
+ .post('/api/telemetry/v2/clusters/_stats')
+ .set('kbn-xsrf', 'xxx')
+ .send({ timeRange, unencrypted: true })
+ .expect(200);
+
+ const stats = body[0];
+
+ const actual = flatKeys(stats);
+ expect(actual).to.be.an('array');
+ const expected = [
+ 'cluster_name',
+ 'cluster_stats.cluster_uuid',
+ 'cluster_stats.indices.analysis',
+ 'cluster_stats.indices.completion',
+ 'cluster_stats.indices.count',
+ 'cluster_stats.indices.docs',
+ 'cluster_stats.indices.fielddata',
+ 'cluster_stats.indices.mappings',
+ 'cluster_stats.indices.query_cache',
+ 'cluster_stats.indices.segments',
+ 'cluster_stats.indices.shards',
+ 'cluster_stats.indices.store',
+ 'cluster_stats.nodes.count',
+ 'cluster_stats.nodes.discovery_types',
+ 'cluster_stats.nodes.fs',
+ 'cluster_stats.nodes.ingest',
+ 'cluster_stats.nodes.jvm',
+ 'cluster_stats.nodes.network_types',
+ 'cluster_stats.nodes.os',
+ 'cluster_stats.nodes.packaging_types',
+ 'cluster_stats.nodes.plugins',
+ 'cluster_stats.nodes.process',
+ 'cluster_stats.nodes.versions',
+ 'cluster_stats.status',
+ 'cluster_stats.timestamp',
+ 'cluster_uuid',
+ 'collection',
+ 'collectionSource',
+ 'stack_stats.kibana.count',
+ 'stack_stats.kibana.indices',
+ 'stack_stats.kibana.os',
+ 'stack_stats.kibana.plugins',
+ 'stack_stats.kibana.versions',
+ 'timestamp',
+ 'version',
+ ];
+
+ expect(expected.every(m => actual.includes(m))).to.be.ok();
+ });
+ });
+}
diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
new file mode 100644
index 00000000000000..4413c672fb46c3
--- /dev/null
+++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts
@@ -0,0 +1,59 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch';
+import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function optInTest({ getService }: FtrProviderContext) {
+ const client: Client = getService('legacyEs');
+ const supertest = getService('supertest');
+
+ describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => {
+ it('should update telemetry setting field via PUT', async () => {
+ try {
+ await client.delete({
+ index: '.kibana',
+ id: 'telemetry:telemetry',
+ } as DeleteDocumentParams);
+ } catch (err) {
+ if (err.statusCode !== 404) {
+ throw err;
+ }
+ }
+
+ await supertest
+ .put('/api/telemetry/v2/userHasSeenNotice')
+ .set('kbn-xsrf', 'xxx')
+ .expect(200);
+
+ const {
+ _source: { telemetry },
+ }: GetResponse<{
+ telemetry: TelemetrySavedObjectAttributes;
+ }> = await client.get({
+ index: '.kibana',
+ id: 'telemetry:telemetry',
+ } as GetParams);
+
+ expect(telemetry.userHasSeenNotice).to.be(true);
+ });
+ });
+}
diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts
index 862e5127bb6704..93debdcc37f0ab 100644
--- a/test/functional/page_objects/common_page.ts
+++ b/test/functional/page_objects/common_page.ts
@@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl: boolean;
shouldLoginIfPrompted: boolean;
useActualUrl: boolean;
+ insertTimestamp: boolean;
}
class CommonPage {
@@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
* Logins to Kibana as default user and navigates to provided app
* @param appUrl Kibana URL
*/
- private async loginIfPrompted(appUrl: string) {
+ private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) {
let currentUrl = await browser.getCurrentUrl();
log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`);
await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting
@@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
'[data-test-subj="kibanaChrome"] nav:not(.ng-hide)',
6 * defaultFindTimeout
);
- await browser.get(appUrl);
+ await browser.get(appUrl, insertTimestamp);
currentUrl = await browser.getCurrentUrl();
log.debug(`Finished login process currentUrl = ${currentUrl}`);
}
@@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
private async navigate(navigateProps: NavigateProps) {
- const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps;
+ const {
+ appConfig,
+ ensureCurrentUrl,
+ shouldLoginIfPrompted,
+ useActualUrl,
+ insertTimestamp,
+ } = navigateProps;
const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig);
await retry.try(async () => {
@@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
const currentUrl = shouldLoginIfPrompted
- ? await this.loginIfPrompted(appUrl)
+ ? await this.loginIfPrompted(appUrl, insertTimestamp)
: await browser.getCurrentUrl();
if (ensureCurrentUrl && !currentUrl.includes(appUrl)) {
@@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl = true,
shouldLoginIfPrompted = true,
useActualUrl = false,
+ insertTimestamp = true,
} = {}
) {
const appConfig = {
@@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl,
shouldLoginIfPrompted,
useActualUrl,
+ insertTimestamp,
});
}
@@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl = true,
shouldLoginIfPrompted = true,
useActualUrl = true,
+ insertTimestamp = true,
} = {}
) {
const appConfig = {
@@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
ensureCurrentUrl,
shouldLoginIfPrompted,
useActualUrl,
+ insertTimestamp,
});
}
@@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
async navigateToApp(
appName: string,
- { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {}
+ { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {}
) {
let appUrl: string;
if (config.has(['apps', appName])) {
@@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
log.debug('returned from get, calling refresh');
await browser.refresh();
let currentUrl = shouldLoginIfPrompted
- ? await this.loginIfPrompted(appUrl)
+ ? await this.loginIfPrompted(appUrl, insertTimestamp)
: await browser.getCurrentUrl();
if (currentUrl.includes('app/kibana')) {
diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts
new file mode 100644
index 00000000000000..5ea151dffdc8ef
--- /dev/null
+++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts
@@ -0,0 +1,93 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+import { ExpectExpression, expectExpressionProvider } from './helpers';
+import { FtrProviderContext } from '../../../functional/ftr_provider_context';
+
+function getCell(esaggsResult: any, column: number, row: number): unknown | undefined {
+ const columnId = esaggsResult?.columns[column]?.id;
+ if (!columnId) {
+ return;
+ }
+ return esaggsResult?.rows[row]?.[columnId];
+}
+
+export default function({
+ getService,
+ updateBaselines,
+}: FtrProviderContext & { updateBaselines: boolean }) {
+ let expectExpression: ExpectExpression;
+ describe('esaggs pipeline expression tests', () => {
+ before(() => {
+ expectExpression = expectExpressionProvider({ getService, updateBaselines });
+ });
+
+ describe('correctly renders tagcloud', () => {
+ it('filters on index pattern primary date field by default', async () => {
+ const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }];
+ const timeRange = {
+ from: '2006-09-21T00:00:00Z',
+ to: '2015-09-22T00:00:00Z',
+ };
+ const expression = `
+ kibana_context timeRange='${JSON.stringify(timeRange)}'
+ | esaggs index='logstash-*' aggConfigs='${JSON.stringify(aggConfigs)}'
+ `;
+ const result = await expectExpression('esaggs_primary_timefield', expression).getResponse();
+ expect(getCell(result, 0, 0)).to.be(9375);
+ });
+
+ it('filters on the specified date field', async () => {
+ const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }];
+ const timeRange = {
+ from: '2006-09-21T00:00:00Z',
+ to: '2015-09-22T00:00:00Z',
+ };
+ const expression = `
+ kibana_context timeRange='${JSON.stringify(timeRange)}'
+ | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' aggConfigs='${JSON.stringify(
+ aggConfigs
+ )}'
+ `;
+ const result = await expectExpression('esaggs_other_timefield', expression).getResponse();
+ expect(getCell(result, 0, 0)).to.be(11134);
+ });
+
+ it('filters on multiple specified date field', async () => {
+ const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }];
+ const timeRange = {
+ from: '2006-09-21T00:00:00Z',
+ to: '2015-09-22T00:00:00Z',
+ };
+ const expression = `
+ kibana_context timeRange='${JSON.stringify(timeRange)}'
+ | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' timeFields='@timestamp' aggConfigs='${JSON.stringify(
+ aggConfigs
+ )}'
+ `;
+ const result = await expectExpression(
+ 'esaggs_multiple_timefields',
+ expression
+ ).getResponse();
+ expect(getCell(result, 0, 0)).to.be(7452);
+ });
+ });
+ });
+}
diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts
index 031a0e3576ccc0..9590f9f8c17940 100644
--- a/test/interpreter_functional/test_suites/run_pipeline/index.ts
+++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts
@@ -46,5 +46,6 @@ export default function({ getService, getPageObjects, loadTestFile }: FtrProvide
loadTestFile(require.resolve('./basic'));
loadTestFile(require.resolve('./tag_cloud'));
loadTestFile(require.resolve('./metric'));
+ loadTestFile(require.resolve('./esaggs'));
});
}
diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts
index b6d13a5604011f..c384e41851e15f 100644
--- a/test/plugin_functional/test_suites/core_plugins/application_status.ts
+++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import url from 'url';
import expect from '@kbn/expect';
import {
AppNavLinkStatus,
@@ -26,6 +27,15 @@ import {
import { PluginFunctionalProviderContext } from '../../services';
import '../../plugins/core_app_status/public/types';
+const getKibanaUrl = (pathname?: string, search?: string) =>
+ url.format({
+ protocol: 'http:',
+ hostname: process.env.TEST_KIBANA_HOST || 'localhost',
+ port: process.env.TEST_KIBANA_PORT || '5620',
+ pathname,
+ search,
+ });
+
// eslint-disable-next-line import/no-default-export
export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) {
const PageObjects = getPageObjects(['common']);
@@ -97,6 +107,22 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
expect(await testSubjects.exists('appStatusApp')).to.eql(true);
});
+ it('allows to change the defaultPath of an application', async () => {
+ let link = await appsMenu.getLink('App Status');
+ expect(link!.href).to.eql(getKibanaUrl('/app/app_status'));
+
+ await setAppStatus({
+ defaultPath: '/arbitrary/path',
+ });
+
+ link = await appsMenu.getLink('App Status');
+ expect(link!.href).to.eql(getKibanaUrl('/app/app_status/arbitrary/path'));
+
+ await navigateToApp('app_status');
+ expect(await testSubjects.exists('appStatusApp')).to.eql(true);
+ expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/app_status/arbitrary/path'));
+ });
+
it('can change the state of the currently mounted app', async () => {
await setAppStatus({
status: AppStatus.accessible,
diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh
index 67d88b308ed91c..951ba8e22d8858 100755
--- a/test/scripts/jenkins_xpack.sh
+++ b/test/scripts/jenkins_xpack.sh
@@ -39,7 +39,7 @@ else
# build runtime for canvas
echo "NODE_ENV=$NODE_ENV"
node ./legacy/plugins/canvas/scripts/shareable_runtime
- node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage
+ node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles --coverage
# rename file in order to be unique one
test -f ../target/kibana-coverage/jest/coverage-final.json \
&& mv ../target/kibana-coverage/jest/coverage-final.json \
diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js
index af5ace8e3cd3b7..4f1251321b0054 100644
--- a/x-pack/dev-tools/jest/create_jest_config.js
+++ b/x-pack/dev-tools/jest/create_jest_config.js
@@ -34,6 +34,20 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) {
'^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`,
'^(!!)?file-loader!': fileMockPath,
},
+ collectCoverageFrom: [
+ 'legacy/plugins/**/*.{js,jsx,ts,tsx}',
+ 'legacy/server/**/*.{js,jsx,ts,tsx}',
+ 'plugins/**/*.{js,jsx,ts,tsx}',
+ '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**',
+ '!**/*.test.{js,ts,tsx}',
+ '!**/flot-charts/**',
+ '!**/test/**',
+ '!**/build/**',
+ '!**/scripts/**',
+ '!**/mocks/**',
+ '!**/plugins/apm/e2e/**',
+ ],
+ coveragePathIgnorePatterns: ['.*\\.d\\.ts'],
coverageDirectory: '/../target/kibana-coverage/jest',
coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'],
setupFiles: [
diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js
index d1e8892fa2c984..a1186e04ee27a6 100644
--- a/x-pack/legacy/plugins/maps/index.js
+++ b/x-pack/legacy/plugins/maps/index.js
@@ -54,7 +54,7 @@ export function maps(kibana) {
emsLandingPageUrl: mapConfig.emsLandingPageUrl,
kbnPkgVersion: serverConfig.get('pkg.version'),
regionmapLayers: _.get(mapConfig, 'regionmap.layers', []),
- tilemap: _.get(mapConfig, 'tilemap', []),
+ tilemap: _.get(mapConfig, 'tilemap', {}),
};
},
styleSheetPaths: `${__dirname}/public/index.scss`,
diff --git a/x-pack/package.json b/x-pack/package.json
index 604889c6094b9c..dcc9b8c61cb960 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -105,6 +105,7 @@
"@types/recompose": "^0.30.6",
"@types/reduce-reducers": "^1.0.0",
"@types/redux-actions": "^2.6.1",
+ "@types/set-value": "^2.0.0",
"@types/sinon": "^7.0.13",
"@types/styled-components": "^4.4.2",
"@types/supertest": "^2.0.5",
@@ -344,6 +345,7 @@
"rison-node": "0.3.1",
"rxjs": "^6.5.3",
"semver": "5.7.0",
+ "set-value": "^3.0.2",
"squel": "^5.13.0",
"stats-lite": "^2.2.0",
"style-it": "^2.1.3",
diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 101e18f2583e39..3e9262c05efac4 100644
--- a/x-pack/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/plugins/actions/server/lib/action_executor.ts
@@ -17,7 +17,7 @@ import {
import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server';
import { SpacesServiceSetup } from '../../../spaces/server';
import { EVENT_LOG_ACTIONS } from '../plugin';
-import { IEvent, IEventLogger } from '../../../event_log/server';
+import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
export interface ActionExecutorContext {
logger: Logger;
@@ -110,7 +110,16 @@ export class ActionExecutor {
const actionLabel = `${actionTypeId}:${actionId}: ${name}`;
const event: IEvent = {
event: { action: EVENT_LOG_ACTIONS.execute },
- kibana: { saved_objects: [{ type: 'action', id: actionId, ...namespace }] },
+ kibana: {
+ saved_objects: [
+ {
+ rel: SAVED_OBJECT_REL_PRIMARY,
+ type: 'action',
+ id: actionId,
+ ...namespace,
+ },
+ ],
+ },
};
eventLogger.startTiming(event);
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
index 0e46ef4919626a..a564b87f2ca50c 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts
@@ -95,6 +95,7 @@ test('calls actionsPlugin.execute per selected action', async () => {
"saved_objects": Array [
Object {
"id": "1",
+ "rel": "primary",
"type": "alert",
},
Object {
diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
index 5c3e36b88879dd..16fadc8b06cd59 100644
--- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
+++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts
@@ -9,7 +9,7 @@ import { AlertAction, State, Context, AlertType } from '../types';
import { Logger } from '../../../../../src/core/server';
import { transformActionParams } from './transform_action_params';
import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server';
-import { IEventLogger, IEvent } from '../../../event_log/server';
+import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { EVENT_LOG_ACTIONS } from '../plugin';
interface CreateExecutionHandlerOptions {
@@ -96,7 +96,7 @@ export function createExecutionHandler({
instance_id: alertInstanceId,
},
saved_objects: [
- { type: 'alert', id: alertId, ...namespace },
+ { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace },
{ type: 'action', id: action.id, ...namespace },
],
},
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
index 26d8a1d1777c04..35a0018049c335 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts
@@ -172,6 +172,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
],
@@ -234,6 +235,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
],
@@ -254,6 +256,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
],
@@ -274,6 +277,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
Object {
@@ -351,6 +355,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
],
@@ -371,6 +376,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
],
@@ -568,6 +574,7 @@ describe('Task Runner', () => {
Object {
"id": "1",
"namespace": undefined,
+ "rel": "primary",
"type": "alert",
},
],
diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts
index 26970dc6b2b0d9..bf005301adc07c 100644
--- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts
@@ -25,7 +25,7 @@ import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
import { AlertInstances } from '../alert_instance/alert_instance';
import { EVENT_LOG_ACTIONS } from '../plugin';
-import { IEvent, IEventLogger } from '../../../event_log/server';
+import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server';
import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error';
const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' };
@@ -174,7 +174,16 @@ export class TaskRunner {
const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`;
const event: IEvent = {
event: { action: EVENT_LOG_ACTIONS.execute },
- kibana: { saved_objects: [{ type: 'alert', id: alertId, namespace }] },
+ kibana: {
+ saved_objects: [
+ {
+ rel: SAVED_OBJECT_REL_PRIMARY,
+ type: 'alert',
+ id: alertId,
+ namespace,
+ },
+ ],
+ },
};
eventLogger.startTiming(event);
@@ -393,7 +402,14 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst
alerting: {
instance_id: id,
},
- saved_objects: [{ type: 'alert', id: params.alertId, namespace: params.namespace }],
+ saved_objects: [
+ {
+ rel: SAVED_OBJECT_REL_PRIMARY,
+ type: 'alert',
+ id: params.alertId,
+ namespace: params.namespace,
+ },
+ ],
},
message,
};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts
index d1b9a2cde4b319..bcfd6b96c9eb88 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts
@@ -70,7 +70,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = core
type: 'serverReturnedHostDetails',
payload: response,
});
- // FIXME: once we have the API implementation in place, we should call it parallel with the above api call and then dispatch this with the results of the second call
dispatch({
type: 'serverReturnedHostPolicyResponse',
payload: {
diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts
index 92bd07a813d264..114251820ce4b4 100644
--- a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts
+++ b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts
@@ -60,8 +60,6 @@ export const getRequestData = async (
reqData.fromIndex = reqData.pageIndex * reqData.pageSize;
}
- // See: https://github.com/elastic/elasticsearch-js/issues/662
- // and https://github.com/elastic/endpoint-app-team/issues/221
if (
reqData.searchBefore !== undefined &&
reqData.searchBefore[0] === '' &&
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
index 99dc4ac9f9e333..08950930441df0 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
@@ -171,7 +171,6 @@ async function enrichHostMetadata(
try {
/**
* Get agent status by elastic agent id if available or use the host id.
- * https://github.com/elastic/endpoint-app-team/issues/354
*/
if (!elasticAgentId) {
diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json
index f487e9262e50e7..0a858969c4f6af 100644
--- a/x-pack/plugins/event_log/generated/mappings.json
+++ b/x-pack/plugins/event_log/generated/mappings.json
@@ -86,6 +86,10 @@
},
"saved_objects": {
"properties": {
+ "rel": {
+ "type": "keyword",
+ "ignore_above": 1024
+ },
"namespace": {
"type": "keyword",
"ignore_above": 1024
diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts
index 9c923fe77d0358..57fe90a8e876ed 100644
--- a/x-pack/plugins/event_log/generated/schemas.ts
+++ b/x-pack/plugins/event_log/generated/schemas.ts
@@ -65,6 +65,7 @@ export const EventSchema = schema.maybe(
saved_objects: schema.maybe(
schema.arrayOf(
schema.object({
+ rel: ecsString(),
namespace: ecsString(),
id: ecsString(),
type: ecsString(),
diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js
index 8cc2c74b60e57b..fd149d132031e1 100644
--- a/x-pack/plugins/event_log/scripts/mappings.js
+++ b/x-pack/plugins/event_log/scripts/mappings.js
@@ -24,6 +24,11 @@ exports.EcsKibanaExtensionsMappings = {
saved_objects: {
type: 'nested',
properties: {
+ // relation; currently only supports "primary" or not set
+ rel: {
+ type: 'keyword',
+ ignore_above: 1024,
+ },
// relevant kibana space
namespace: {
type: 'keyword',
@@ -58,6 +63,7 @@ exports.EcsEventLogProperties = [
'user.name',
'kibana.server_uuid',
'kibana.alerting.instance_id',
+ 'kibana.saved_objects.rel',
'kibana.saved_objects.namespace',
'kibana.saved_objects.id',
'kibana.saved_objects.name',
diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
index f79962a3241317..66c16d0ddf3838 100644
--- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
+++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts
@@ -236,6 +236,13 @@ describe('queryEventsBySavedObject', () => {
query: {
bool: {
must: [
+ {
+ term: {
+ 'kibana.saved_objects.rel': {
+ value: 'primary',
+ },
+ },
+ },
{
term: {
'kibana.saved_objects.type': {
@@ -319,6 +326,13 @@ describe('queryEventsBySavedObject', () => {
query: {
bool: {
must: [
+ {
+ term: {
+ 'kibana.saved_objects.rel': {
+ value: 'primary',
+ },
+ },
+ },
{
term: {
'kibana.saved_objects.type': {
@@ -388,6 +402,13 @@ describe('queryEventsBySavedObject', () => {
query: {
bool: {
must: [
+ {
+ term: {
+ 'kibana.saved_objects.rel': {
+ value: 'primary',
+ },
+ },
+ },
{
term: {
'kibana.saved_objects.type': {
diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
index 47d273b9981e32..c0ff87234c09db 100644
--- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
+++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts
@@ -7,7 +7,7 @@
import { reject, isUndefined } from 'lodash';
import { SearchResponse, Client } from 'elasticsearch';
import { Logger, ClusterClient } from '../../../../../src/core/server';
-import { IEvent } from '../types';
+import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../types';
import { FindOptionsType } from '../event_log_client';
export type EsClusterClient = Pick;
@@ -155,6 +155,13 @@ export class ClusterClientAdapter {
query: {
bool: {
must: [
+ {
+ term: {
+ 'kibana.saved_objects.rel': {
+ value: SAVED_OBJECT_REL_PRIMARY,
+ },
+ },
+ },
{
term: {
'kibana.saved_objects.type': {
diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts
index 6a745931420c0c..2bda194a65d133 100644
--- a/x-pack/plugins/event_log/server/event_logger.test.ts
+++ b/x-pack/plugins/event_log/server/event_logger.test.ts
@@ -150,6 +150,35 @@ describe('EventLogger', () => {
message = await waitForLogMessage(systemLogger);
expect(message).toMatch(/invalid event logged.*action.*undefined.*/);
});
+
+ test('logs warnings when writing invalid events', async () => {
+ service.registerProviderActions('provider', ['action-a']);
+ eventLogger = service.getLogger({});
+
+ eventLogger.logEvent(({ event: { PROVIDER: 'provider' } } as unknown) as IEvent);
+ let message = await waitForLogMessage(systemLogger);
+ expect(message).toMatch(/invalid event logged.*provider.*undefined.*/);
+
+ const event: IEvent = {
+ event: {
+ provider: 'provider',
+ action: 'action-a',
+ },
+ kibana: {
+ saved_objects: [
+ {
+ rel: 'ZZZ-primary',
+ namespace: 'default',
+ type: 'event_log_test',
+ id: '123',
+ },
+ ],
+ },
+ };
+ eventLogger.logEvent(event);
+ message = await waitForLogMessage(systemLogger);
+ expect(message).toMatch(/invalid rel property.*ZZZ-primary.*/);
+ });
});
// return the next logged event; throw if not an event
diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts
index bcfd7bd45a6f5c..1a710a6fa48653 100644
--- a/x-pack/plugins/event_log/server/event_logger.ts
+++ b/x-pack/plugins/event_log/server/event_logger.ts
@@ -19,6 +19,7 @@ import {
ECS_VERSION,
EventSchema,
} from './types';
+import { SAVED_OBJECT_REL_PRIMARY } from './types';
type SystemLogger = Plugin['systemLogger'];
@@ -118,6 +119,8 @@ const RequiredEventSchema = schema.object({
action: schema.string({ minLength: 1 }),
});
+const ValidSavedObjectRels = new Set([undefined, SAVED_OBJECT_REL_PRIMARY]);
+
function validateEvent(eventLogService: IEventLogService, event: IEvent): IValidatedEvent {
if (event?.event == null) {
throw new Error(`no "event" property`);
@@ -137,7 +140,17 @@ function validateEvent(eventLogService: IEventLogService, event: IEvent): IValid
}
// could throw an error
- return EventSchema.validate(event);
+ const result = EventSchema.validate(event);
+
+ if (result?.kibana?.saved_objects?.length) {
+ for (const so of result?.kibana?.saved_objects) {
+ if (!ValidSavedObjectRels.has(so.rel)) {
+ throw new Error(`invalid rel property in saved_objects: "${so.rel}"`);
+ }
+ }
+ }
+
+ return result;
}
export const EVENT_LOGGED_PREFIX = `event logged: `;
diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts
index b7fa25cb6eb9cc..0612b5319c15b2 100644
--- a/x-pack/plugins/event_log/server/index.ts
+++ b/x-pack/plugins/event_log/server/index.ts
@@ -8,6 +8,12 @@ import { PluginInitializerContext } from 'src/core/server';
import { ConfigSchema } from './types';
import { Plugin } from './plugin';
-export { IEventLogService, IEventLogger, IEventLogClientService, IEvent } from './types';
+export {
+ IEventLogService,
+ IEventLogger,
+ IEventLogClientService,
+ IEvent,
+ SAVED_OBJECT_REL_PRIMARY,
+} from './types';
export const config = { schema: ConfigSchema };
export const plugin = (context: PluginInitializerContext) => new Plugin(context);
diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts
index baf53ef4479141..58be6707b03730 100644
--- a/x-pack/plugins/event_log/server/types.ts
+++ b/x-pack/plugins/event_log/server/types.ts
@@ -13,6 +13,8 @@ import { IEvent } from '../generated/schemas';
import { FindOptionsType } from './event_log_client';
import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter';
+export const SAVED_OBJECT_REL_PRIMARY = 'primary';
+
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
logEntries: schema.boolean({ defaultValue: false }),
diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts
index e77d2ea0fb7c97..beb31d548c6702 100644
--- a/x-pack/plugins/graph/server/saved_objects/migrations.ts
+++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts
@@ -8,7 +8,7 @@ import { get } from 'lodash';
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
export const graphMigrations = {
- '7.0.0': (doc: SavedObjectUnsanitizedDoc) => {
+ '7.0.0': (doc: SavedObjectUnsanitizedDoc) => {
// Set new "references" attribute
doc.references = doc.references || [];
// Migrate index pattern
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 946f1c14bf593a..cf691f73bdc2cc 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -51,17 +51,19 @@ const getCurrentValueFromAggregations = (
const getParsedFilterQuery: (
filterQuery: string | undefined
-) => Record = filterQuery => {
+) => Record | Array> = filterQuery => {
if (!filterQuery) return {};
try {
return JSON.parse(filterQuery).bool;
} catch (e) {
- return {
- query_string: {
- query: filterQuery,
- analyze_wildcard: true,
+ return [
+ {
+ query_string: {
+ query: filterQuery,
+ analyze_wildcard: true,
+ },
},
- };
+ ];
}
};
@@ -159,8 +161,12 @@ export const getElasticsearchMetricQuery = (
return {
query: {
bool: {
- filter: [...rangeFilters, ...metricFieldFilters],
- ...parsedFilterQuery,
+ filter: [
+ ...rangeFilters,
+ ...metricFieldFilters,
+ ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []),
+ ],
+ ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}),
},
},
size: 0,
@@ -233,6 +239,7 @@ const getMetric: (
body: searchBody,
index,
});
+
return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) };
} catch (e) {
return { '*': undefined }; // Trigger an Error state
diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts
index c5067480fb9538..9bc1293799d3c5 100644
--- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts
@@ -14,6 +14,7 @@ export const DEFAULT_AGENT_CONFIG = {
status: AgentConfigStatus.Active,
datasources: [],
is_default: true,
+ monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
};
export const DEFAULT_AGENT_CONFIGS_PACKAGES = [DefaultPackages.system];
diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
index a7d4e36d16f2ad..bff799798ff6e8 100644
--- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts
@@ -3,11 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { Datasource, NewDatasource, DatasourceInput } from '../types';
+import { Datasource, DatasourceInput } from '../types';
import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource';
describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
- const mockNewDatasource: NewDatasource = {
+ const mockDatasource: Datasource = {
+ id: 'some-uuid',
name: 'mock-datasource',
description: '',
config_id: '',
@@ -15,11 +16,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
output_id: '',
namespace: 'default',
inputs: [],
- };
-
- const mockDatasource: Datasource = {
- ...mockNewDatasource,
- id: 'some-uuid',
revision: 1,
};
@@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => {
});
});
- it('uses name for id when id is not provided in case of new datasource', () => {
- expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({
- id: 'mock-datasource',
- name: 'mock-datasource',
- namespace: 'default',
- enabled: true,
- use_output: 'default',
- inputs: [],
- });
- });
-
it('returns agent datasource config with flattened input and package stream', () => {
expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({
id: 'some-uuid',
diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
index 5deb33ccf10f1b..620b663451ea39 100644
--- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
+++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts
@@ -3,16 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types';
+import { Datasource, FullAgentConfigDatasource } from '../types';
import { DEFAULT_OUTPUT } from '../constants';
export const storedDatasourceToAgentDatasource = (
- datasource: Datasource | NewDatasource
+ datasource: Datasource
): FullAgentConfigDatasource => {
- const { name, namespace, enabled, package: pkg, inputs } = datasource;
+ const { id, name, namespace, enabled, package: pkg, inputs } = datasource;
const fullDatasource: FullAgentConfigDatasource = {
- id: 'id' in datasource ? datasource.id : name,
+ id: id || name,
name,
namespace,
enabled,
diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts
index cb290e61b17e51..a977a1a66e059a 100644
--- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts
+++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts
@@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => {
name: 'mock-package',
title: 'Mock package',
version: '0.0.0',
+ latestVersion: '0.0.0',
description: 'description',
type: 'mock',
categories: [],
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
index fcd3955f3a32fc..e3ca7635fdb407 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
@@ -60,6 +60,11 @@ export interface AgentEvent {
export interface AgentEventSOAttributes extends AgentEvent, SavedObjectAttributes {}
+type MetadataValue = string | AgentMetadata;
+
+export interface AgentMetadata {
+ [x: string]: MetadataValue;
+}
interface AgentBase {
type: AgentType;
active: boolean;
@@ -72,19 +77,17 @@ interface AgentBase {
config_revision?: number | null;
config_newest_revision?: number;
last_checkin?: string;
+ user_provided_metadata: AgentMetadata;
+ local_metadata: AgentMetadata;
}
export interface Agent extends AgentBase {
id: string;
current_error_events: AgentEvent[];
- user_provided_metadata: Record;
- local_metadata: Record;
access_api_key?: string;
status?: string;
}
export interface AgentSOAttributes extends AgentBase, SavedObjectAttributes {
- user_provided_metadata: string;
- local_metadata: string;
current_error_events?: string;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
index 2372caee512af6..96121251b133eb 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
@@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-import { SavedObjectAttributes } from 'src/core/public';
import {
Datasource,
DatasourcePackage,
@@ -23,9 +21,10 @@ export interface NewAgentConfig {
namespace?: string;
description?: string;
is_default?: boolean;
+ monitoring_enabled?: Array<'logs' | 'metrics'>;
}
-export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes {
+export interface AgentConfig extends NewAgentConfig {
id: string;
status: AgentConfigStatus;
datasources: string[] | Datasource[];
@@ -60,4 +59,12 @@ export interface FullAgentConfig {
};
datasources: FullAgentConfigDatasource[];
revision?: number;
+ settings?: {
+ monitoring: {
+ use_output?: string;
+ enabled: boolean;
+ metrics: boolean;
+ logs: boolean;
+ };
+ };
}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts
index 885e0a9316d793..ca61a93d9be93e 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts
@@ -17,22 +17,29 @@ export interface DatasourceConfigRecordEntry {
export type DatasourceConfigRecord = Record;
-export interface DatasourceInputStream {
+export interface NewDatasourceInputStream {
id: string;
enabled: boolean;
dataset: string;
processors?: string[];
config?: DatasourceConfigRecord;
vars?: DatasourceConfigRecord;
+}
+
+export interface DatasourceInputStream extends NewDatasourceInputStream {
agent_stream?: any;
}
-export interface DatasourceInput {
+export interface NewDatasourceInput {
type: string;
enabled: boolean;
processors?: string[];
config?: DatasourceConfigRecord;
vars?: DatasourceConfigRecord;
+ streams: NewDatasourceInputStream[];
+}
+
+export interface DatasourceInput extends Omit {
streams: DatasourceInputStream[];
}
@@ -44,10 +51,11 @@ export interface NewDatasource {
enabled: boolean;
package?: DatasourcePackage;
output_id: string;
- inputs: DatasourceInput[];
+ inputs: NewDatasourceInput[];
}
-export type Datasource = NewDatasource & {
+export interface Datasource extends Omit {
id: string;
+ inputs: DatasourceInput[];
revision: number;
-};
+}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 05e160cdfb81ac..f8779a879a049f 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -204,6 +204,8 @@ export interface RegistryVarsEntry {
// internal until we need them
interface PackageAdditions {
title: string;
+ latestVersion: string;
+ installedVersion?: string;
assets: AssetsGroupedByServiceByType;
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx
new file mode 100644
index 00000000000000..1e7a14e3502296
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx
@@ -0,0 +1,101 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+import {
+ EuiButtonEmpty,
+ EuiFlyout,
+ EuiFlyoutBody,
+ EuiFlyoutHeader,
+ EuiFlyoutFooter,
+ EuiLink,
+ EuiText,
+ EuiTitle,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+interface Props {
+ onClose: () => void;
+}
+
+export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ forumLink: (
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx
index 0f3ddee29fa443..5a06a9a8794412 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx
@@ -3,35 +3,45 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiText } from '@elastic/eui';
+import { EuiText, EuiLink } from '@elastic/eui';
+import { AlphaFlyout } from './alpha_flyout';
const Message = styled(EuiText).attrs(props => ({
color: 'subdued',
textAlign: 'center',
+ size: 's',
}))`
padding: ${props => props.theme.eui.paddingSizes.m};
`;
-export const AlphaMessaging: React.FC<{}> = () => (
-
-
-
-
+export const AlphaMessaging: React.FC<{}> = () => {
+ const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ {' – '}
-
- {' – '}
-
-
-
-
-);
+ />{' '}
+ setIsAlphaFlyoutOpen(true)}>
+ View more details.
+
+
+
+ {isAlphaFlyoutOpen && setIsAlphaFlyoutOpen(false)} />}
+ >
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx
index 4429b9d8e6b829..1c9bd9107515d3 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx
@@ -92,7 +92,6 @@ function useSuggestions(fieldPrefix: string, search: string) {
const res = (await data.indexPatterns.getFieldsForWildcard({
pattern: INDEX_NAME,
})) as IFieldType[];
-
if (!data || !data.autocomplete) {
throw new Error('Missing data plugin');
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx
index 92146e9ee56793..9863463e68a016 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx
@@ -209,7 +209,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
index 0d19ecd0cb7351..e2fc190e158f97 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
@@ -5,12 +5,18 @@
*/
import { sendRequest, useRequest } from './use_request';
import { datasourceRouteService } from '../../services';
-import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types';
+import {
+ CreateDatasourceRequest,
+ CreateDatasourceResponse,
+ UpdateDatasourceRequest,
+ UpdateDatasourceResponse,
+} from '../../types';
import {
DeleteDatasourcesRequest,
DeleteDatasourcesResponse,
GetDatasourcesRequest,
GetDatasourcesResponse,
+ GetOneDatasourceResponse,
} from '../../../../../common/types/rest_spec';
export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
@@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => {
});
};
+export const sendUpdateDatasource = (
+ datasourceId: string,
+ body: UpdateDatasourceRequest['body']
+) => {
+ return sendRequest({
+ path: datasourceRouteService.getUpdatePath(datasourceId),
+ method: 'put',
+ body: JSON.stringify(body),
+ });
+};
+
export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => {
return sendRequest({
path: datasourceRouteService.getDeletePath(),
@@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) {
query,
});
}
+
+export const sendGetOneDatasource = (datasourceId: string) => {
+ return sendRequest({
+ path: datasourceRouteService.getInfoPath(datasourceId),
+ method: 'get',
+ });
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
index 10245e73520f75..4a9cfe02b74ace 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx
@@ -96,12 +96,28 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre
- setIsSettingsFlyoutOpen(true)}>
-
-
+
+
+
+
+
+
+
+ setIsSettingsFlyoutOpen(true)}>
+
+
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx
index 0d53ca34a1fefd..92c44d86e47c65 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx
@@ -18,6 +18,7 @@ import {
EuiText,
EuiComboBox,
EuiIconTip,
+ EuiCheckboxGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -30,7 +31,7 @@ interface ValidationResults {
const StyledEuiAccordion = styled(EuiAccordion)`
.ingest-active-button {
- color: ${props => props.theme.eui.euiColorPrimary}};
+ color: ${props => props.theme.eui.euiColorPrimary};
}
`;
@@ -244,6 +245,68 @@ export const AgentConfigForm: React.FunctionComponent = ({
)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ acc[key] = true;
+ return acc;
+ },
+ { logs: false, metrics: false }
+ )}
+ onChange={id => {
+ if (id !== 'logs' && id !== 'metrics') {
+ return;
+ }
+
+ const hasLogs =
+ agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0;
+
+ const previousValues = agentConfig.monitoring_enabled || [];
+ updateAgentConfig({
+ monitoring_enabled: hasLogs
+ ? previousValues.filter(type => type !== id)
+ : [...previousValues, id],
+ });
+ }}
+ />
+
+
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx
index 39d882f7fdf65e..f1e3fea6a07423 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx
@@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{
-
+ {from === 'edit' ? (
+
+ ) : (
+
+ )}
- {from === 'config' ? (
+ {from === 'edit' ? (
+
+ ) : from === 'config' ? (
- {agentConfig && from === 'config' ? (
+ {agentConfig && (from === 'config' || from === 'edit') ? (
{
const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => {
if (updatedPackageInfo) {
setPackageInfo(updatedPackageInfo);
+ setFormState('VALID');
} else {
setFormState('INVALID');
setPackageInfo(undefined);
@@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL;
// Save datasource
- const [formState, setFormState] = useState<
- 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'
- >('INVALID');
+ const [formState, setFormState] = useState('INVALID');
const saveDatasource = async () => {
setFormState('LOADING');
const result = await sendCreateDatasource(datasource);
@@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
const { error } = await saveDatasource();
if (!error) {
history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`);
+ notifications.toasts.addSuccess({
+ title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', {
+ defaultMessage: `Successfully added '{datasourceName}'`,
+ values: {
+ datasourceName: datasource.name,
+ },
+ }),
+ text:
+ agentCount && agentConfig
+ ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`,
+ values: {
+ agentConfigName: agentConfig.name,
+ },
+ })
+ : undefined,
+ });
} else {
notifications.toasts.addError(error, {
title: 'Error',
@@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
packageInfo={packageInfo}
datasource={datasource}
updateDatasource={updateDatasource}
+ validationResults={validationResults!}
/>
) : null,
},
@@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => {
children:
agentConfig && packageInfo ? (
) => void;
validationResults: DatasourceValidationResults;
submitAttempted: boolean;
-}> = ({
- agentConfig,
- packageInfo,
- datasource,
- updateDatasource,
- validationResults,
- submitAttempted,
-}) => {
- // Form show/hide states
-
+}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => {
const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
- // Update datasource's package and config info
- useEffect(() => {
- const dsPackage = datasource.package;
- const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : '';
- const pkgKey = `${packageInfo.name}-${packageInfo.version}`;
-
- // If package has changed, create shell datasource with input&stream values based on package info
- if (currentPkgKey !== pkgKey) {
- // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name
- const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`);
- const dsWithMatchingNames = (agentConfig.datasources as Datasource[])
- .filter(ds => Boolean(ds.name.match(dsPackageNamePattern)))
- .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10))
- .sort();
-
- updateDatasource({
- name: `${packageInfo.name}-${
- dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1
- }`,
- package: {
- name: packageInfo.name,
- title: packageInfo.title,
- version: packageInfo.version,
- },
- inputs: packageToConfigDatasourceInputs(packageInfo),
- });
- }
-
- // If agent config has changed, update datasource's config ID and namespace
- if (datasource.config_id !== agentConfig.id) {
- updateDatasource({
- config_id: agentConfig.id,
- namespace: agentConfig.namespace,
- });
- }
- }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
-
- // Step B, configure inputs (and their streams)
+ // Configure inputs (and their streams)
// Assume packages only export one datasource for now
const renderConfigureInputs = () =>
packageInfo.datasources &&
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx
index 792389381eaf04..c4d602c2c20819 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx
@@ -17,13 +17,16 @@ import {
} from '@elastic/eui';
import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types';
import { packageToConfigDatasourceInputs } from '../../../services';
+import { Loading } from '../../../components';
+import { DatasourceValidationResults } from './services';
export const StepDefineDatasource: React.FunctionComponent<{
agentConfig: AgentConfig;
packageInfo: PackageInfo;
datasource: NewDatasource;
updateDatasource: (fields: Partial) => void;
-}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => {
+ validationResults: DatasourceValidationResults;
+}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => {
// Form show/hide states
const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false);
@@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{
}
}, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]);
- return (
+ return validationResults ? (
<>
}
+ isInvalid={!!validationResults.description}
+ error={validationResults.description}
>
) : null}
>
+ ) : (
+
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts
index 85cc758fc4c464..10b30a5696d834 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export type CreateDatasourceFrom = 'package' | 'config';
+export type CreateDatasourceFrom = 'package' | 'config' | 'edit';
+export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx
index 1eee9f6b0c3462..a0418c5f256c48 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx
@@ -19,7 +19,7 @@ import {
import { AgentConfig, Datasource } from '../../../../../types';
import { TableRowActions } from '../../../components/table_row_actions';
import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item';
-import { useCapabilities } from '../../../../../hooks';
+import { useCapabilities, useLink } from '../../../../../hooks';
import { useAgentConfigLink } from '../../hooks/use_details_uri';
import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider';
import { useConfigRefresh } from '../../hooks/use_config';
@@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent = ({
}) => {
const hasWriteCapabilities = useCapabilities().write;
const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id });
+ const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`);
const refreshConfig = useConfigRefresh();
// With the datasources provided on input, generate the list of datasources
@@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent = ({
{}}
+ // key="datasourceView"
+ // >
+ //
+ // ,
{}}
- key="datasourceView"
- >
-
- ,
- // FIXME: implement Edit datasource action
- {}}
+ href={`${editDatasourceLink}/${datasource.id}`}
key="datasourceEdit"
>
= ({
/>
,
// FIXME: implement Copy datasource action
- {}} key="datasourceCopy">
-
- ,
+ // {}} key="datasourceCopy">
+ //
+ // ,
{deleteDatasourcePrompt => {
return (
@@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent = ({
],
},
],
- [config, hasWriteCapabilities, refreshConfig]
+ [config, editDatasourceLink, hasWriteCapabilities, refreshConfig]
);
return (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx
new file mode 100644
index 00000000000000..d4c39f21a1ea6c
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx
@@ -0,0 +1,323 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { useState, useEffect } from 'react';
+import { useRouteMatch, useHistory } from 'react-router-dom';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiButtonEmpty,
+ EuiButton,
+ EuiSteps,
+ EuiBottomBar,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+} from '@elastic/eui';
+import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants';
+import { AgentConfig, PackageInfo, NewDatasource } from '../../../types';
+import {
+ useLink,
+ useCore,
+ useConfig,
+ sendUpdateDatasource,
+ sendGetAgentStatus,
+ sendGetOneAgentConfig,
+ sendGetOneDatasource,
+ sendGetPackageInfoByKey,
+} from '../../../hooks';
+import { Loading, Error } from '../../../components';
+import {
+ CreateDatasourcePageLayout,
+ ConfirmCreateDatasourceModal,
+} from '../create_datasource_page/components';
+import {
+ DatasourceValidationResults,
+ validateDatasource,
+ validationHasErrors,
+} from '../create_datasource_page/services';
+import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types';
+import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource';
+import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource';
+
+export const EditDatasourcePage: React.FunctionComponent = () => {
+ const { notifications } = useCore();
+ const {
+ fleet: { enabled: isFleetEnabled },
+ } = useConfig();
+ const {
+ params: { configId, datasourceId },
+ } = useRouteMatch();
+ const history = useHistory();
+
+ // Agent config, package info, and datasource states
+ const [isLoadingData, setIsLoadingData] = useState(true);
+ const [loadingError, setLoadingError] = useState();
+ const [agentConfig, setAgentConfig] = useState();
+ const [packageInfo, setPackageInfo] = useState();
+ const [datasource, setDatasource] = useState({
+ name: '',
+ description: '',
+ config_id: '',
+ enabled: true,
+ output_id: '',
+ inputs: [],
+ });
+
+ // Retrieve agent config, package, and datasource info
+ useEffect(() => {
+ const getData = async () => {
+ setIsLoadingData(true);
+ setLoadingError(undefined);
+ try {
+ const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([
+ sendGetOneAgentConfig(configId),
+ sendGetOneDatasource(datasourceId),
+ ]);
+ if (agentConfigData?.item) {
+ setAgentConfig(agentConfigData.item);
+ }
+ if (datasourceData?.item) {
+ const { id, revision, inputs, ...restOfDatasource } = datasourceData.item;
+ // Remove `agent_stream` from all stream info, we assign this after saving
+ const newDatasource = {
+ ...restOfDatasource,
+ inputs: inputs.map(input => {
+ const { streams, ...restOfInput } = input;
+ return {
+ ...restOfInput,
+ streams: streams.map(stream => {
+ const { agent_stream, ...restOfStream } = stream;
+ return restOfStream;
+ }),
+ };
+ }),
+ };
+ setDatasource(newDatasource);
+ if (datasourceData.item.package) {
+ const { data: packageData } = await sendGetPackageInfoByKey(
+ `${datasourceData.item.package.name}-${datasourceData.item.package.version}`
+ );
+ if (packageData?.response) {
+ setPackageInfo(packageData.response);
+ setValidationResults(validateDatasource(newDatasource, packageData.response));
+ setFormState('VALID');
+ }
+ }
+ }
+ } catch (e) {
+ setLoadingError(e);
+ }
+ setIsLoadingData(false);
+ };
+ getData();
+ }, [configId, datasourceId]);
+
+ // Retrieve agent count
+ const [agentCount, setAgentCount] = useState(0);
+ useEffect(() => {
+ const getAgentCount = async () => {
+ const { data } = await sendGetAgentStatus({ configId });
+ if (data?.results.total) {
+ setAgentCount(data.results.total);
+ }
+ };
+
+ if (isFleetEnabled) {
+ getAgentCount();
+ }
+ }, [configId, isFleetEnabled]);
+
+ // Datasource validation state
+ const [validationResults, setValidationResults] = useState();
+ const hasErrors = validationResults ? validationHasErrors(validationResults) : false;
+
+ // Update datasource method
+ const updateDatasource = (updatedFields: Partial) => {
+ const newDatasource = {
+ ...datasource,
+ ...updatedFields,
+ };
+ setDatasource(newDatasource);
+
+ // eslint-disable-next-line no-console
+ console.debug('Datasource updated', newDatasource);
+ const newValidationResults = updateDatasourceValidation(newDatasource);
+ const hasValidationErrors = newValidationResults
+ ? validationHasErrors(newValidationResults)
+ : false;
+ if (!hasValidationErrors) {
+ setFormState('VALID');
+ }
+ };
+
+ const updateDatasourceValidation = (newDatasource?: NewDatasource) => {
+ if (packageInfo) {
+ const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo);
+ setValidationResults(newValidationResult);
+ // eslint-disable-next-line no-console
+ console.debug('Datasource validation results', newValidationResult);
+
+ return newValidationResult;
+ }
+ };
+
+ // Cancel url
+ const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`);
+ const cancelUrl = CONFIG_URL;
+
+ // Save datasource
+ const [formState, setFormState] = useState('INVALID');
+ const saveDatasource = async () => {
+ setFormState('LOADING');
+ const result = await sendUpdateDatasource(datasourceId, datasource);
+ setFormState('SUBMITTED');
+ return result;
+ };
+
+ const onSubmit = async () => {
+ if (formState === 'VALID' && hasErrors) {
+ setFormState('INVALID');
+ return;
+ }
+ if (agentCount !== 0 && formState !== 'CONFIRM') {
+ setFormState('CONFIRM');
+ return;
+ }
+ const { error } = await saveDatasource();
+ if (!error) {
+ history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`);
+ notifications.toasts.addSuccess({
+ title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', {
+ defaultMessage: `Successfully updated '{datasourceName}'`,
+ values: {
+ datasourceName: datasource.name,
+ },
+ }),
+ text:
+ agentCount && agentConfig
+ ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', {
+ defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`,
+ values: {
+ agentConfigName: agentConfig.name,
+ },
+ })
+ : undefined,
+ });
+ } else {
+ notifications.toasts.addError(error, {
+ title: 'Error',
+ });
+ setFormState('VALID');
+ }
+ };
+
+ const layoutProps = {
+ from: 'edit' as CreateDatasourceFrom,
+ cancelUrl,
+ agentConfig,
+ packageInfo,
+ };
+
+ return (
+
+ {isLoadingData ? (
+
+ ) : loadingError || !agentConfig || !packageInfo ? (
+
+ }
+ error={
+ loadingError ||
+ i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', {
+ defaultMessage: 'There was an error loading this data source information',
+ })
+ }
+ />
+ ) : (
+ <>
+ {formState === 'CONFIRM' && (
+ setFormState('VALID')}
+ />
+ )}
+
+ ),
+ },
+ {
+ title: i18n.translate(
+ 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle',
+ {
+ defaultMessage: 'Select the data you want to collect',
+ }
+ ),
+ children: (
+
+ ),
+ },
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx
index 71ada155373bfb..ef88aa5d17f1e4 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx
@@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom';
import { AgentConfigListPage } from './list_page';
import { AgentConfigDetailsPage } from './details_page';
import { CreateDatasourcePage } from './create_datasource_page';
+import { EditDatasourcePage } from './edit_datasource_page';
export const AgentConfigApp: React.FunctionComponent = () => (
+
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx
index 1fe116ef360901..9f582e7e2fbe69 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx
@@ -34,6 +34,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos
description: '',
namespace: '',
is_default: undefined,
+ monitoring_enabled: ['logs', 'metrics'],
});
const [isLoading, setIsLoading] = useState(false);
const [withSysMonitoring, setWithSysMonitoring] = useState(true);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
index 1ea162252c741e..3dcc19bc4a5aee 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
@@ -36,13 +36,11 @@ import {
useConfig,
useUrlParams,
} from '../../../hooks';
-import { AgentConfigDeleteProvider } from '../components';
import { CreateAgentConfigFlyout } from './components';
import { SearchBar } from '../../../components/search_bar';
import { LinkedAgentCount } from '../components';
import { useAgentConfigLink } from '../details_page/hooks/use_details_uri';
import { TableRowActions } from '../components/table_row_actions';
-import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item';
const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({
overflow: 'hidden',
@@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>(
defaultMessage="Create data source"
/>
,
-
-
-
- ,
-
-
- {deleteAgentConfigsPrompt => {
- return (
- deleteAgentConfigsPrompt([config.id], onDelete)}
- >
-
-
- );
- }}
- ,
+ //
+ //
+ // ,
]}
/>
);
@@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
: urlParams.kuery ?? ''
);
const { pagination, pageSizeOptions, setPagination } = usePagination();
- const [selectedAgentConfigs, setSelectedAgentConfigs] = useState([]);
const history = useHistory();
const isCreateAgentConfigFlyoutOpen = 'create' in urlParams;
const setIsCreateAgentConfigFlyoutOpen = useCallback(
@@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
/>
) : null}
- {selectedAgentConfigs.length ? (
-
-
- {deleteAgentConfigsPrompt => (
- {
- deleteAgentConfigsPrompt(
- selectedAgentConfigs.map(agentConfig => agentConfig.id),
- () => {
- sendRequest();
- setSelectedAgentConfigs([]);
- }
- );
- }}
- >
-
-
- )}
-
-
- ) : null}
= () => {
items={agentConfigData ? agentConfigData.items : []}
itemId="id"
columns={columns}
- isSelectable={true}
- selection={{
- selectable: (agentConfig: AgentConfig) => !agentConfig.is_default,
- onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => {
- setSelectedAgentConfigs(newSelectedAgentConfigs);
- },
- }}
+ isSelectable={false}
pagination={{
pageIndex: pagination.currentPage - 1,
pageSize: pagination.pageSize,
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
new file mode 100644
index 00000000000000..64223efefaab8c
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiIcon } from '@elastic/eui';
+import React from 'react';
+import styled from 'styled-components';
+
+export const StyledAlert = styled(EuiIcon)`
+ color: ${props => props.theme.eui.euiColorWarning};
+ padding: 0 5px;
+`;
+
+export const UpdateIcon = () => ;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
index 8ad081cbbabe40..ab7e87b3ad06cb 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
@@ -30,9 +30,15 @@ export function PackageCard({
showInstalledBadge,
status,
icons,
+ ...restProps
}: PackageCardProps) {
const { toDetailView } = useLinks();
- const url = toDetailView({ name, version });
+ let urlVersion = version;
+ // if this is an installed package, link to the version installed
+ if ('savedObject' in restProps) {
+ urlVersion = restProps.savedObject.attributes.version || version;
+ }
+ const url = toDetailView({ name, version: urlVersion });
return (
;
+type InstallPackageProps = Pick & {
+ fromUpdate?: boolean;
+};
+type SetPackageInstallStatusProps = Pick & PackageInstallItem;
function usePackageInstall({ notifications }: { notifications: NotificationsStart }) {
+ const { toDetailView } = useLinks();
const [packages, setPackage] = useState({});
const setPackageInstallStatus = useCallback(
- ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => {
+ ({ name, status, version }: SetPackageInstallStatusProps) => {
+ const packageProps: PackageInstallItem = {
+ status,
+ version,
+ };
setPackage((prev: PackagesInstall) => ({
...prev,
- [name]: { status },
+ [name]: packageProps,
}));
},
[]
);
+ const getPackageInstallStatus = useCallback(
+ (pkg: string): PackageInstallItem => {
+ return packages[pkg];
+ },
+ [packages]
+ );
+
const installPackage = useCallback(
- async ({ name, version, title }: InstallPackageProps) => {
- setPackageInstallStatus({ name, status: InstallStatus.installing });
+ async ({ name, version, title, fromUpdate = false }: InstallPackageProps) => {
+ const currStatus = getPackageInstallStatus(name);
+ const newStatus = { ...currStatus, name, status: InstallStatus.installing };
+ setPackageInstallStatus(newStatus);
const pkgkey = `${name}-${version}`;
const res = await sendInstallPackage(pkgkey);
if (res.error) {
- setPackageInstallStatus({ name, status: InstallStatus.notInstalled });
+ if (fromUpdate) {
+ // if there is an error during update, set it back to the previous version
+ // as handling of bad update is not implemented yet
+ setPackageInstallStatus({ ...currStatus, name });
+ } else {
+ setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version });
+ }
notifications.toasts.addWarning({
title: toMountPoint(
{
- return packages[pkg].status;
- },
- [packages]
+ [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView]
);
const uninstallPackage = useCallback(
async ({ name, version, title }: Pick) => {
- setPackageInstallStatus({ name, status: InstallStatus.uninstalling });
+ setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version });
const pkgkey = `${name}-${version}`;
const res = await sendRemovePackage(pkgkey);
if (res.error) {
- setPackageInstallStatus({ name, status: InstallStatus.installed });
+ setPackageInstallStatus({ name, status: InstallStatus.installed, version });
notifications.toasts.addWarning({
title: toMountPoint(
;
export function ContentPanel(props: ContentPanelProps) {
- const { panel, name, version, assets, title, removable } = props;
+ const { panel, name, version, assets, title, removable, latestVersion } = props;
switch (panel) {
case 'settings':
return (
@@ -60,6 +60,7 @@ export function ContentPanel(props: ContentPanelProps) {
assets={assets}
title={title}
removable={removable}
+ latestVersion={latestVersion}
/>
);
case 'data-sources':
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx
index fa3245aec02c5b..c82b7ed2297a7c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx
@@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => {
const packageInstallStatus = getPackageInstallStatus(name);
// if they arrive at this page and the package is not installed, send them to overview
// this happens if they arrive with a direct url or they uninstall while on this tab
- if (packageInstallStatus !== InstallStatus.installed)
+ if (packageInstallStatus.status !== InstallStatus.installed)
return (
props.theme.eui.euiSizeM};
`;
-const StyledVersion = styled(Version)`
- font-size: ${props => props.theme.eui.euiFontSizeS};
- color: ${props => props.theme.eui.euiColorDarkShade};
-`;
-
type HeaderProps = PackageInfo & { iconType?: IconType };
export function Header(props: HeaderProps) {
- const { iconType, name, title, version } = props;
+ const { iconType, name, title, version, installedVersion, latestVersion } = props;
const hasWriteCapabilites = useCapabilities().write;
const { toListView } = useLinks();
const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`);
-
+ const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
return (
@@ -59,7 +54,11 @@ export function Header(props: HeaderProps) {
{title}
-
+
+
+ {version} {updateAvailable && }
+
+
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
index 3239d7b90e3c3c..1f3eb2cc9362e5 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
@@ -32,11 +32,12 @@ export function Detail() {
const packageInfo = response.data?.response;
const title = packageInfo?.title;
const name = packageInfo?.name;
+ const installedVersion = packageInfo?.installedVersion;
const status: InstallStatus = packageInfo?.status as any;
// track install status state
if (name) {
- setPackageInstallStatus({ name, status });
+ setPackageInstallStatus({ name, status, version: installedVersion || null });
}
if (packageInfo) {
setInfo({ ...packageInfo, title: title || '' });
@@ -64,7 +65,6 @@ type LayoutProps = PackageInfo & Pick & Pick
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx
index cbbf1ce53c4ea3..cdad67fd875483 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx
@@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall';
import { ConfirmPackageInstall } from './confirm_package_install';
type InstallationButtonProps = Pick & {
- disabled: boolean;
+ disabled?: boolean;
+ isUpdate?: boolean;
};
export function InstallationButton(props: InstallationButtonProps) {
- const { assets, name, title, version, disabled = true } = props;
+ const { assets, name, title, version, disabled = true, isUpdate = false } = props;
const hasWriteCapabilites = useCapabilities().write;
const installPackage = useInstallPackage();
const uninstallPackage = useUninstallPackage();
const getPackageInstallStatus = useGetPackageInstallStatus();
- const installationStatus = getPackageInstallStatus(name);
+ const { status: installationStatus } = getPackageInstallStatus(name);
const isInstalling = installationStatus === InstallStatus.installing;
const isRemoving = installationStatus === InstallStatus.uninstalling;
const isInstalled = installationStatus === InstallStatus.installed;
+ const showUninstallButton = isInstalled || isRemoving;
const [isModalVisible, setModalVisible] = useState(false);
const toggleModal = useCallback(() => {
setModalVisible(!isModalVisible);
@@ -36,6 +38,10 @@ export function InstallationButton(props: InstallationButtonProps) {
toggleModal();
}, [installPackage, name, title, toggleModal, version]);
+ const handleClickUpdate = useCallback(() => {
+ installPackage({ name, version, title, fromUpdate: true });
+ }, [installPackage, name, title, version]);
+
const handleClickUninstall = useCallback(() => {
uninstallPackage({ name, version, title });
toggleModal();
@@ -78,6 +84,15 @@ export function InstallationButton(props: InstallationButtonProps) {
);
+ const updateButton = (
+
+
+
+ );
+
const uninstallButton = (
- {isInstalled || isRemoving ? uninstallButton : installButton}
+ {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton}
{isModalVisible && (isInstalled ? uninstallModal : installModal)}
) : null;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
index 3589a1a9444e1b..4d4dba2a64e5a6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
@@ -8,11 +8,22 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
+import styled from 'styled-components';
import { InstallStatus, PackageInfo } from '../../../../types';
import { useGetDatasources } from '../../../../hooks';
import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants';
import { useGetPackageInstallStatus } from '../../hooks';
import { InstallationButton } from './installation_button';
+import { UpdateIcon } from '../../components/icons';
+
+const SettingsTitleCell = styled.td`
+ padding-right: ${props => props.theme.eui.spacerSizes.xl};
+ padding-bottom: ${props => props.theme.eui.spacerSizes.m};
+`;
+
+const UpdatesAvailableMsgContainer = styled.span`
+ padding-left: ${props => props.theme.eui.spacerSizes.s};
+`;
const NoteLabel = () => (
(
defaultMessage="Note:"
/>
);
+const UpdatesAvailableMsg = () => (
+
+
+
+
+);
+
export const SettingsPanel = (
- props: Pick
+ props: Pick
) => {
+ const { name, title, removable, latestVersion, version } = props;
const getPackageInstallStatus = useGetPackageInstallStatus();
const { data: datasourcesData } = useGetDatasources({
perPage: 0,
page: 1,
kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`,
});
- const { name, title, removable } = props;
- const packageInstallStatus = getPackageInstallStatus(name);
+ const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name);
const packageHasDatasources = !!datasourcesData?.total;
+ const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
+ const isViewingOldPackage = version < latestVersion;
+ // hide install/remove options if the user has version of the package is installed
+ // and this package is out of date or if they do have a version installed but it's not this one
+ const hideInstallOptions =
+ (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) ||
+ (installationStatus === InstallStatus.installed && installedVersion !== version);
+
+ const isUpdating = installationStatus === InstallStatus.installing && installedVersion;
return (
@@ -43,14 +73,13 @@ export const SettingsPanel = (
- {packageInstallStatus === InstallStatus.notInstalled ||
- packageInstallStatus === InstallStatus.installing ? (
+ {installedVersion !== null && (
-
-
-
+
+
+
+
+
+
+
+
+ {installedVersion}
+
+ {updateAvailable && }
+ |
+
+
+
+
+
+
+
+ {latestVersion}
+
+ |
+
+
+
+ {updateAvailable && (
+
+
+
+ )}
- ) : (
+ )}
+ {!hideInstallOptions && !isUpdating && (
-
-
+
+ {installationStatus === InstallStatus.notInstalled ||
+ installationStatus === InstallStatus.installing ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {packageHasDatasources && removable === true && (
+
+
+
+ ),
}}
/>
-
-
-
-
-
-
+
+ )}
+ {removable === false && (
+
+
+
+
+ ),
+ }}
+ />
+
+ )}
)}
-
-
-
-
-
-
-
- {packageHasDatasources && removable === true && (
-
-
-
-
- ),
- }}
- />
-
- )}
- {removable === false && (
-
-
-
-
- ),
- }}
- />
-
- )}
);
};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx
index 05729ccfc1af42..ab168ef1530bd6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx
@@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) {
: p.theme.eui.euiFontWeightRegular};
`;
// don't display Data Sources tab if the package is not installed
- if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources')
+ if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources')
return null;
return (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts
new file mode 100644
index 00000000000000..508190fef0fc2b
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { AgentMetadata } from '../../../../types';
+
+export function flattenMetadata(metadata: AgentMetadata) {
+ return Object.entries(metadata).reduce((acc, [key, value]) => {
+ if (typeof value === 'string') {
+ acc[key] = value;
+
+ return acc;
+ }
+
+ Object.entries(flattenMetadata(value)).forEach(([flattenedKey, flattenedValue]) => {
+ acc[`${key}.${flattenedKey}`] = flattenedValue;
+ });
+
+ return acc;
+ }, {} as { [k: string]: string });
+}
+export function unflattenMetadata(flattened: { [k: string]: string }) {
+ const metadata: AgentMetadata = {};
+
+ Object.entries(flattened).forEach(([flattenedKey, flattenedValue]) => {
+ const keyParts = flattenedKey.split('.');
+ const lastKey = keyParts.pop();
+
+ if (!lastKey) {
+ throw new Error('Invalid metadata');
+ }
+
+ let metadataPart = metadata;
+ keyParts.forEach(keyPart => {
+ if (!metadataPart[keyPart]) {
+ metadataPart[keyPart] = {};
+ }
+
+ metadataPart = metadataPart[keyPart] as AgentMetadata;
+ });
+ metadataPart[lastKey] = flattenedValue;
+ });
+
+ return metadata;
+}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx
index ee43385e601c28..aa6da36f8fb6c7 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx
@@ -17,11 +17,13 @@ import {
} from '@elastic/eui';
import { MetadataForm } from './metadata_form';
import { Agent } from '../../../../types';
+import { flattenMetadata } from './helper';
interface Props {
agent: Agent;
flyout: { hide: () => void };
}
+
export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, flyout }) => {
const mapMetadata = (obj: { [key: string]: string } | undefined) => {
return Object.keys(obj || {}).map(key => ({
@@ -30,8 +32,8 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly
}));
};
- const localItems = mapMetadata(agent.local_metadata);
- const userProvidedItems = mapMetadata(agent.user_provided_metadata);
+ const localItems = mapMetadata(flattenMetadata(agent.local_metadata));
+ const userProvidedItems = mapMetadata(flattenMetadata(agent.user_provided_metadata));
return (
flyout.hide()} size="s" aria-labelledby="flyoutTitle">
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx
index ce28bbdc590b06..af7e8c674db4c0 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx
@@ -22,6 +22,7 @@ import { useAgentRefresh } from '../hooks';
import { useInput, sendRequest } from '../../../../hooks';
import { Agent } from '../../../../types';
import { agentRouteService } from '../../../../services';
+import { flattenMetadata, unflattenMetadata } from './helper';
function useAddMetadataForm(agent: Agent, done: () => void) {
const refreshAgent = useAgentRefresh();
@@ -66,15 +67,17 @@ function useAddMetadataForm(agent: Agent, done: () => void) {
isLoading: true,
});
+ const metadata = unflattenMetadata({
+ ...flattenMetadata(agent.user_provided_metadata),
+ [keyInput.value]: valueInput.value,
+ });
+
try {
const { error } = await sendRequest({
path: agentRouteService.getUpdatePath(agent.id),
method: 'put',
body: JSON.stringify({
- user_provided_metadata: {
- ...agent.user_provided_metadata,
- [keyInput.value]: valueInput.value,
- },
+ user_provided_metadata: metadata,
}),
});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
index 23fe18b82468cb..05264c157434e0 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
@@ -238,7 +238,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
const columns = [
{
- field: 'local_metadata.host',
+ field: 'local_metadata.host.hostname',
name: i18n.translate('xpack.ingestManager.agentList.hostColumnTitle', {
defaultMessage: 'Host',
}),
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
index 05d150fd9ae231..70d8e7d6882f88 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
@@ -8,6 +8,7 @@ import styled from 'styled-components';
import {
EuiButton,
EuiButtonEmpty,
+ EuiBetaBadge,
EuiPanel,
EuiText,
EuiTitle,
@@ -19,10 +20,11 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
import { WithHeaderLayout } from '../../layouts';
import { useLink, useGetAgentConfigs } from '../../hooks';
import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components';
-import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants';
+import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants';
const OverviewPanel = styled(EuiPanel).attrs(props => ({
paddingSize: 'm',
@@ -57,6 +59,11 @@ const OverviewStats = styled(EuiDescriptionList).attrs(props => ({
}
`;
+const AlphaBadge = styled(EuiBetaBadge)`
+ vertical-align: top;
+ margin-left: ${props => props.theme.eui.paddingSizes.s};
+`;
+
export const IngestManagerOverview: React.FunctionComponent = () => {
// Agent enrollment flyout state
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
@@ -79,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
id="xpack.ingestManager.overviewPageTitle"
defaultMessage="Ingest Manager"
/>
+
@@ -213,7 +233,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => {
/>
-
+
+ (agentConfig: GetAgentConfigsResponseItem) =>
listAgents(soClient, {
showInactive: true,
perPage: 0,
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts
index 882258e859555b..89d8b9e173ffe0 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts
@@ -56,8 +56,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
enrolled_at: { type: 'date' },
access_api_key_id: { type: 'keyword' },
version: { type: 'keyword' },
- user_provided_metadata: { type: 'text' },
- local_metadata: { type: 'text' },
+ user_provided_metadata: { type: 'flattened' },
+ local_metadata: { type: 'flattened' },
config_id: { type: 'keyword' },
last_updated: { type: 'date' },
last_checkin: { type: 'date' },
@@ -128,6 +128,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
updated_on: { type: 'keyword' },
updated_by: { type: 'keyword' },
revision: { type: 'integer' },
+ monitoring_enabled: { type: 'keyword' },
},
},
},
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts
new file mode 100644
index 00000000000000..17758f6e3d7f12
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { savedObjectsClientMock } from 'src/core/server/mocks';
+import { agentConfigService } from './agent_config';
+import { Output } from '../types';
+
+function getSavedObjectMock(configAttributes: any) {
+ const mock = savedObjectsClientMock.create();
+
+ mock.get.mockImplementation(async (type: string, id: string) => {
+ return {
+ type,
+ id,
+ references: [],
+ attributes: configAttributes,
+ };
+ });
+
+ return mock;
+}
+
+jest.mock('./output', () => {
+ return {
+ outputService: {
+ getDefaultOutputId: () => 'test-id',
+ get: (): Output => {
+ return {
+ id: 'test-id',
+ is_default: true,
+ name: 'default',
+ // @ts-ignore
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ };
+ },
+ },
+ };
+});
+
+describe('agent config', () => {
+ describe('getFullConfig', () => {
+ it('should return a config without monitoring if not monitoring is not enabled', async () => {
+ const soClient = getSavedObjectMock({
+ revision: 1,
+ });
+ const config = await agentConfigService.getFullConfig(soClient, 'config');
+
+ expect(config).toMatchObject({
+ id: 'config',
+ outputs: {
+ default: {
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ ca_sha256: undefined,
+ api_key: undefined,
+ },
+ },
+ datasources: [],
+ revision: 1,
+ settings: {
+ monitoring: {
+ enabled: false,
+ logs: false,
+ metrics: false,
+ },
+ },
+ });
+ });
+
+ it('should return a config with monitoring if monitoring is enabled for logs', async () => {
+ const soClient = getSavedObjectMock({
+ revision: 1,
+ monitoring_enabled: ['logs'],
+ });
+ const config = await agentConfigService.getFullConfig(soClient, 'config');
+
+ expect(config).toMatchObject({
+ id: 'config',
+ outputs: {
+ default: {
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ ca_sha256: undefined,
+ api_key: undefined,
+ },
+ },
+ datasources: [],
+ revision: 1,
+ settings: {
+ monitoring: {
+ use_output: 'default',
+ enabled: true,
+ logs: true,
+ metrics: false,
+ },
+ },
+ });
+ });
+
+ it('should return a config with monitoring if monitoring is enabled for metrics', async () => {
+ const soClient = getSavedObjectMock({
+ revision: 1,
+ monitoring_enabled: ['metrics'],
+ });
+ const config = await agentConfigService.getFullConfig(soClient, 'config');
+
+ expect(config).toMatchObject({
+ id: 'config',
+ outputs: {
+ default: {
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+ ca_sha256: undefined,
+ api_key: undefined,
+ },
+ },
+ datasources: [],
+ revision: 1,
+ settings: {
+ monitoring: {
+ use_output: 'default',
+ enabled: true,
+ logs: false,
+ metrics: true,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts
index 75bbfc21293c2b..7ab6ef1920c18a 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts
@@ -301,28 +301,49 @@ class AgentConfigService {
if (!config) {
return null;
}
+ const defaultOutput = await outputService.get(
+ soClient,
+ await outputService.getDefaultOutputId(soClient)
+ );
const agentConfig: FullAgentConfig = {
id: config.id,
outputs: {
// TEMPORARY as we only support a default output
- ...[
- await outputService.get(soClient, await outputService.getDefaultOutputId(soClient)),
- ].reduce((outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => {
- outputs[name] = {
- type,
- hosts,
- ca_sha256,
- api_key,
- ...outputConfig,
- };
- return outputs;
- }, {} as FullAgentConfig['outputs']),
+ ...[defaultOutput].reduce(
+ (outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => {
+ outputs[name] = {
+ type,
+ hosts,
+ ca_sha256,
+ api_key,
+ ...outputConfig,
+ };
+ return outputs;
+ },
+ {} as FullAgentConfig['outputs']
+ ),
},
datasources: (config.datasources as Datasource[])
.filter(datasource => datasource.enabled)
.map(ds => storedDatasourceToAgentDatasource(ds)),
revision: config.revision,
+ ...(config.monitoring_enabled && config.monitoring_enabled.length > 0
+ ? {
+ settings: {
+ monitoring: {
+ use_output: defaultOutput.name,
+ enabled: true,
+ logs: config.monitoring_enabled.indexOf('logs') >= 0,
+ metrics: config.monitoring_enabled.indexOf('metrics') >= 0,
+ },
+ },
+ }
+ : {
+ settings: {
+ monitoring: { enabled: false, logs: false, metrics: false },
+ },
+ }),
};
return agentConfig;
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
index c96a81ed9b7587..9b1565e7d74aab 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
@@ -11,6 +11,7 @@ import {
AgentAction,
AgentSOAttributes,
AgentEventSOAttributes,
+ AgentMetadata,
} from '../../types';
import { agentConfigService } from '../agent_config';
@@ -28,7 +29,7 @@ export async function agentCheckin(
const updateData: {
last_checkin: string;
default_api_key?: string;
- local_metadata?: string;
+ local_metadata?: AgentMetadata;
current_error_events?: string;
} = {
last_checkin: new Date().toISOString(),
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts
index 175b92b75aca05..43fd5a3ce0ac94 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts
@@ -103,7 +103,7 @@ export async function updateAgent(
}
) {
await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, {
- user_provided_metadata: JSON.stringify(data.userProvidedMetatada),
+ user_provided_metadata: data.userProvidedMetatada,
});
}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts
index a34d2e03e9b3de..81afa70ecb818f 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts
@@ -32,8 +32,8 @@ export async function enroll(
config_id: configId,
type,
enrolled_at: enrolledAt,
- user_provided_metadata: JSON.stringify(metadata?.userProvided ?? {}),
- local_metadata: JSON.stringify(metadata?.local ?? {}),
+ user_provided_metadata: metadata?.userProvided ?? {},
+ local_metadata: metadata?.local ?? {},
current_error_events: undefined,
access_api_key_id: undefined,
last_checkin: undefined,
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts
index b182662e0fb4e2..11beba1cd7e43e 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts
@@ -19,8 +19,8 @@ export function savedObjectToAgent(so: SavedObject): Agent {
current_error_events: so.attributes.current_error_events
? JSON.parse(so.attributes.current_error_events)
: [],
- local_metadata: JSON.parse(so.attributes.local_metadata),
- user_provided_metadata: JSON.parse(so.attributes.user_provided_metadata),
+ local_metadata: so.attributes.local_metadata,
+ user_provided_metadata: so.attributes.user_provided_metadata,
access_api_key: undefined,
status: undefined,
};
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
index b6de083cbe0cb2..8140b1e6de470f 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
@@ -18,8 +18,8 @@ describe('Agent status service', () => {
type: AGENT_TYPE_PERMANENT,
attributes: {
active: false,
- local_metadata: '{}',
- user_provided_metadata: '{}',
+ local_metadata: {},
+ user_provided_metadata: {},
},
} as SavedObject);
const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
@@ -33,8 +33,8 @@ describe('Agent status service', () => {
type: AGENT_TYPE_PERMANENT,
attributes: {
active: true,
- local_metadata: '{}',
- user_provided_metadata: '{}',
+ local_metadata: {},
+ user_provided_metadata: {},
},
} as SavedObject);
const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts
index 3679c577ee5713..25180244b02147 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts
@@ -309,3 +309,21 @@ test('tests processing object field with property, reverse order', () => {
const mappings = generateMappings(processedFields);
expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyReversedMapping));
});
+
+test('tests constant_keyword field type handling', () => {
+ const constantKeywordLiteralYaml = `
+- name: constantKeyword
+ type: constant_keyword
+ `;
+ const constantKeywordMapping = {
+ properties: {
+ constantKeyword: {
+ type: 'constant_keyword',
+ },
+ },
+ };
+ const fields: Field[] = safeLoad(constantKeywordLiteralYaml);
+ const processedFields = processFields(fields);
+ const mappings = generateMappings(processedFields);
+ expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping));
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts
index bc1694348b4c2f..f1660fbc015913 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts
@@ -150,6 +150,7 @@ describe('creating index patterns from yaml fields', () => {
{ fields: [{ name: 'testField', type: 'text' }], expect: 'string' },
{ fields: [{ name: 'testField', type: 'date' }], expect: 'date' },
{ fields: [{ name: 'testField', type: 'geo_point' }], expect: 'geo_point' },
+ { fields: [{ name: 'testField', type: 'constant_keyword' }], expect: 'string' },
];
tests.forEach(test => {
@@ -191,6 +192,7 @@ describe('creating index patterns from yaml fields', () => {
attr: 'aggregatable',
},
{ fields: [{ name, type: 'keyword' }], expect: true, attr: 'aggregatable' },
+ { fields: [{ name, type: 'constant_keyword' }], expect: true, attr: 'aggregatable' },
{ fields: [{ name, type: 'text', aggregatable: true }], expect: false, attr: 'aggregatable' },
{ fields: [{ name, type: 'text' }], expect: false, attr: 'aggregatable' },
{
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
index 05e64c6565dc67..ec657820a22251 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts
@@ -47,6 +47,7 @@ const typeMap: TypeMap = {
date: 'date',
ip: 'ip',
boolean: 'boolean',
+ constant_keyword: 'string',
};
export interface IndexPatternField {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
index d76584225877c2..da8d79a04b97cd 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
@@ -67,9 +67,10 @@ export async function getPackageInfo(options: {
pkgVersion: string;
}): Promise {
const { savedObjectsClient, pkgName, pkgVersion } = options;
- const [item, savedObject, assets] = await Promise.all([
+ const [item, savedObject, latestPackage, assets] = await Promise.all([
Registry.fetchInfo(pkgName, pkgVersion),
getInstallationObject({ savedObjectsClient, pkgName }),
+ Registry.fetchFindLatestPackage(pkgName),
Registry.getArchiveInfo(pkgName, pkgVersion),
] as const);
// adding `as const` due to regression in TS 3.7.2
@@ -79,6 +80,7 @@ export async function getPackageInfo(options: {
// add properties that aren't (or aren't yet) on Registry response
const updated = {
...item,
+ latestVersion: latestPackage.version,
title: item.title || nameAsTitle(item.name),
assets: Registry.groupPathsByService(assets || []),
};
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
index d49e0e661440f3..c67cccd044bf5e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
@@ -43,6 +43,7 @@ export function createInstallableFrom(
? {
...from,
status: InstallationStatus.installed,
+ installedVersion: savedObject.attributes.version,
savedObject,
}
: {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index 06f3decdbbe6f2..8f51c4d78305c0 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -106,7 +106,7 @@ export async function installPackage(options: {
try {
await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed);
} catch (err) {
- // some assets may not exist if deleting during a failed update
+ // log these errors, some assets may not exist if deleted during a failed update
}
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index ac2c869f3b9e9f..befb4722b6504f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -121,8 +121,12 @@ export async function deleteKibanaSavedObjectsAssets(
const deletePromises = installedObjects.map(({ id, type }) => {
const assetType = type as AssetType;
if (savedObjectTypes.includes(assetType)) {
- savedObjectsClient.delete(assetType, id);
+ return savedObjectsClient.delete(assetType, id);
}
});
- await Promise.all(deletePromises);
+ try {
+ await Promise.all(deletePromises);
+ } catch (err) {
+ throw new Error('error deleting saved object asset');
+ }
}
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index a7019ebc0a271f..27ed1de8499871 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -8,6 +8,7 @@ import { ScopedClusterClient } from 'src/core/server';
export {
// Object types
Agent,
+ AgentMetadata,
AgentSOAttributes,
AgentStatus,
AgentType,
diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts
index 040b2eb16289ab..59cadf3bd7f74c 100644
--- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts
+++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts
@@ -11,6 +11,9 @@ const AgentConfigBaseSchema = {
name: schema.string(),
namespace: schema.maybe(schema.string()),
description: schema.maybe(schema.string()),
+ monitoring_enabled: schema.maybe(
+ schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')]))
+ ),
};
export const NewAgentConfigSchema = schema.object({
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 359c06a6a9ebce..48729448b2ea57 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -41,6 +41,10 @@ export const datatableVisualization: Visualization<
},
],
+ getVisualizationTypeId() {
+ return 'lnsDatatable';
+ },
+
getLayerIds(state) {
return state.layers.map(l => l.layerId);
},
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx
index 3c61d270b1bcf9..c8d8064e60e38a 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx
@@ -62,7 +62,25 @@ describe('chart_switch', () => {
id: 'subvisC2',
label: 'C2',
},
+ {
+ icon: 'empty',
+ id: 'subvisC3',
+ label: 'C3',
+ },
],
+ getSuggestions: jest.fn(options => {
+ if (options.subVisualizationId === 'subvisC2') {
+ return [];
+ }
+ return [
+ {
+ score: 1,
+ title: '',
+ state: `suggestion`,
+ previewIcon: 'empty',
+ },
+ ];
+ }),
},
};
}
@@ -313,10 +331,11 @@ describe('chart_switch', () => {
expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined();
});
- it('should not indicate data loss if visualization is not changed', () => {
+ it('should not show a warning when the subvisualization is the same', () => {
const dispatch = jest.fn();
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
+ visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2');
const switchVisualizationType = jest.fn(() => 'therebedragons');
visualizations.visC.switchVisualizationType = switchVisualizationType;
@@ -333,10 +352,10 @@ describe('chart_switch', () => {
/>
);
- expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).toBeUndefined();
+ expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined();
});
- it('should remove all layers if there is no suggestion', () => {
+ it('should get suggestions when switching subvisualization', () => {
const dispatch = jest.fn();
const visualizations = mockVisualizations();
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
@@ -377,7 +396,7 @@ describe('chart_switch', () => {
const dispatch = jest.fn();
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
- const switchVisualizationType = jest.fn(() => 'therebedragons');
+ const switchVisualizationType = jest.fn(() => 'switched');
visualizations.visC.switchVisualizationType = switchVisualizationType;
@@ -393,12 +412,12 @@ describe('chart_switch', () => {
/>
);
- switchTo('subvisC2', component);
- expect(switchVisualizationType).toHaveBeenCalledWith('subvisC2', 'therebegriffins');
+ switchTo('subvisC3', component);
+ expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion');
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: 'SWITCH_VISUALIZATION',
- initialState: 'therebedragons',
+ initialState: 'switched',
})
);
expect(frame.removeLayers).not.toHaveBeenCalled();
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx
index 1461449f3c1c8a..d73f83e75c0e4e 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx
@@ -105,7 +105,16 @@ export function ChartSwitch(props: Props) {
const switchVisType =
props.visualizationMap[visualizationId].switchVisualizationType ||
((_type: string, initialState: unknown) => initialState);
- if (props.visualizationId === visualizationId) {
+ const layers = Object.entries(props.framePublicAPI.datasourceLayers);
+ const containsData = layers.some(
+ ([_layerId, datasource]) => datasource.getTableSpec().length > 0
+ );
+ // Always show the active visualization as a valid selection
+ if (
+ props.visualizationId === visualizationId &&
+ props.visualizationState &&
+ newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId
+ ) {
return {
visualizationId,
subVisualizationId,
@@ -116,13 +125,13 @@ export function ChartSwitch(props: Props) {
};
}
- const layers = Object.entries(props.framePublicAPI.datasourceLayers);
- const containsData = layers.some(
- ([_layerId, datasource]) => datasource.getTableSpec().length > 0
+ const topSuggestion = getTopSuggestion(
+ props,
+ visualizationId,
+ newVisualization,
+ subVisualizationId
);
- const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization);
-
let dataLoss: VisualizationSelection['dataLoss'];
if (!containsData) {
@@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) {
function getTopSuggestion(
props: Props,
visualizationId: string,
- newVisualization: Visualization
+ newVisualization: Visualization,
+ subVisualizationId?: string
): Suggestion | undefined {
const suggestions = getSuggestions({
datasourceMap: props.datasourceMap,
@@ -258,6 +268,7 @@ function getTopSuggestion(
visualizationMap: { [visualizationId]: newVisualization },
activeVisualizationId: props.visualizationId,
visualizationState: props.visualizationState,
+ subVisualizationId,
}).filter(suggestion => {
// don't use extended versions of current data table on switching between visualizations
// to avoid confusing the user.
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
index eabcdfa7a24ab4..949ae1f43448ed 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts
@@ -44,6 +44,7 @@ export function getSuggestions({
datasourceStates,
visualizationMap,
activeVisualizationId,
+ subVisualizationId,
visualizationState,
field,
}: {
@@ -57,6 +58,7 @@ export function getSuggestions({
>;
visualizationMap: Record;
activeVisualizationId: string | null;
+ subVisualizationId?: string;
visualizationState: unknown;
field?: unknown;
}): Suggestion[] {
@@ -89,7 +91,8 @@ export function getSuggestions({
table,
visualizationId,
datasourceSuggestion,
- currentVisualizationState
+ currentVisualizationState,
+ subVisualizationId
);
})
)
@@ -108,13 +111,15 @@ function getVisualizationSuggestions(
table: TableSuggestion,
visualizationId: string,
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
- currentVisualizationState: unknown
+ currentVisualizationState: unknown,
+ subVisualizationId?: string
) {
return visualization
.getSuggestions({
table,
state: currentVisualizationState,
keptLayerIds: datasourceSuggestion.keptLayerIds,
+ subVisualizationId,
})
.map(({ state, ...visualizationSuggestion }) => ({
...visualizationSuggestion,
diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
index 50cd1ad8bd53a4..e684fe8b3b5d62 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx
@@ -28,6 +28,7 @@ export function createMockVisualization(): jest.Mocked {
label: 'TEST',
},
],
+ getVisualizationTypeId: jest.fn(_state => 'empty'),
getDescription: jest.fn(_state => ({ label: '' })),
switchVisualizationType: jest.fn((_, x) => x),
getPersistableState: jest.fn(_state => _state),
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts
deleted file mode 100644
index 5f35ef650a08c2..00000000000000
--- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
-import { getAutoDate } from './auto_date';
-
-describe('auto_date', () => {
- let autoDate: ReturnType;
-
- beforeEach(() => {
- autoDate = getAutoDate({ data: dataPluginMock.createSetupContract() });
- });
-
- it('should do nothing if no time range is provided', () => {
- const result = autoDate.fn(
- {
- type: 'kibana_context',
- },
- {
- aggConfigs: 'canttouchthis',
- },
- // eslint-disable-next-line
- {} as any
- );
-
- expect(result).toEqual('canttouchthis');
- });
-
- it('should not change anything if there are no auto date histograms', () => {
- const aggConfigs = JSON.stringify([
- { type: 'date_histogram', params: { interval: '35h' } },
- { type: 'count' },
- ]);
- const result = autoDate.fn(
- {
- timeRange: {
- from: 'now-10d',
- to: 'now',
- },
- type: 'kibana_context',
- },
- {
- aggConfigs,
- },
- // eslint-disable-next-line
- {} as any
- );
-
- expect(result).toEqual(aggConfigs);
- });
-
- it('should change auto date histograms', () => {
- const aggConfigs = JSON.stringify([
- { type: 'date_histogram', params: { interval: 'auto' } },
- { type: 'count' },
- ]);
- const result = autoDate.fn(
- {
- timeRange: {
- from: 'now-10d',
- to: 'now',
- },
- type: 'kibana_context',
- },
- {
- aggConfigs,
- },
- // eslint-disable-next-line
- {} as any
- );
-
- const interval = JSON.parse(result).find(
- (agg: { type: string }) => agg.type === 'date_histogram'
- ).params.interval;
-
- expect(interval).toBeTruthy();
- expect(typeof interval).toEqual('string');
- expect(interval).not.toEqual('auto');
- });
-});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts
deleted file mode 100644
index 97a46f4a3e1765..00000000000000
--- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public';
-import {
- ExpressionFunctionDefinition,
- KibanaContext,
-} from '../../../../../src/plugins/expressions/public';
-
-interface LensAutoDateProps {
- aggConfigs: string;
-}
-
-export function getAutoDate(deps: {
- data: DataPublicPluginSetup;
-}): ExpressionFunctionDefinition<
- 'lens_auto_date',
- KibanaContext | null,
- LensAutoDateProps,
- string
-> {
- function autoIntervalFromContext(ctx?: KibanaContext | null) {
- if (!ctx || !ctx.timeRange) {
- return;
- }
-
- return deps.data.search.aggs.calculateAutoTimeExpression(ctx.timeRange);
- }
-
- /**
- * Convert all 'auto' date histograms into a concrete value (e.g. 2h).
- * This allows us to support 'auto' on all date fields, and opens the
- * door to future customizations (e.g. adjusting the level of detail, etc).
- */
- return {
- name: 'lens_auto_date',
- aliases: [],
- help: '',
- inputTypes: ['kibana_context', 'null'],
- args: {
- aggConfigs: {
- types: ['string'],
- default: '""',
- help: '',
- },
- },
- fn(input, args) {
- const interval = autoIntervalFromContext(input);
-
- if (!interval) {
- return args.aggConfigs;
- }
-
- const configs = JSON.parse(args.aggConfigs) as Array<{
- type: string;
- params: { interval: string };
- }>;
-
- const updatedConfigs = configs.map(c => {
- if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') {
- return c;
- }
-
- return {
- ...c,
- params: {
- ...c.params,
- interval,
- },
- };
- });
-
- return JSON.stringify(updatedConfigs);
- },
- };
-}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index 074c40759f8d82..9df79aa9e09085 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -1380,5 +1380,43 @@ describe('IndexPatternDimensionEditorPanel', () => {
},
});
});
+
+ it('does not set the size of the terms aggregation', () => {
+ const dragging = {
+ field: { type: 'string', name: 'mystring', aggregatable: true },
+ indexPatternId: 'foo',
+ };
+ const testState = dragDropState();
+ onDrop({
+ ...defaultProps,
+ dragDropContext: {
+ ...dragDropContext,
+ dragging,
+ },
+ droppedItem: dragging,
+ state: testState,
+ columnId: 'col2',
+ filterOperations: (op: OperationMetadata) => op.isBucketed,
+ layerId: 'myLayer',
+ });
+
+ expect(setState).toBeCalledTimes(1);
+ expect(setState).toHaveBeenCalledWith({
+ ...testState,
+ layers: {
+ myLayer: {
+ ...testState.layers.myLayer,
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ ...testState.layers.myLayer.columns,
+ col2: expect.objectContaining({
+ operationType: 'terms',
+ params: expect.objectContaining({ size: 3 }),
+ }),
+ },
+ },
+ },
+ });
+ });
});
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
index fe14f472341afd..73fd144b9c7f87 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
@@ -8,7 +8,6 @@ import { CoreSetup } from 'kibana/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { getIndexPatternDatasource } from './indexpattern';
import { renameColumns } from './rename_columns';
-import { getAutoDate } from './auto_date';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
import {
DataPublicPluginSetup,
@@ -31,10 +30,9 @@ export class IndexPatternDatasource {
setup(
core: CoreSetup,
- { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins
+ { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins
) {
expressions.registerFunction(renameColumns);
- expressions.registerFunction(getAutoDate({ data: dataSetup }));
editorFrame.registerDatasource(
core.getStartServices().then(([coreStart, { data }]) =>
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index e4f3677d0fe88e..06635e663361db 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -10,6 +10,7 @@ import { DatasourcePublicAPI, Operation, Datasource } from '../types';
import { coreMock } from 'src/core/public/mocks';
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
+import { Ast } from '@kbn/interpreter/common';
jest.mock('./loader');
jest.mock('../id_generator');
@@ -262,20 +263,7 @@ describe('IndexPattern Data Source', () => {
Object {
"arguments": Object {
"aggConfigs": Array [
- Object {
- "chain": Array [
- Object {
- "arguments": Object {
- "aggConfigs": Array [
- "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]",
- ],
- },
- "function": "lens_auto_date",
- "type": "function",
- },
- ],
- "type": "expression",
- },
+ "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]",
],
"includeFormatHints": Array [
true,
@@ -289,6 +277,9 @@ describe('IndexPattern Data Source', () => {
"partialRows": Array [
false,
],
+ "timeFields": Array [
+ "timestamp",
+ ],
},
"function": "esaggs",
"type": "function",
@@ -307,6 +298,89 @@ describe('IndexPattern Data Source', () => {
}
`);
});
+
+ it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => {
+ const queryPersistedState: IndexPatternPersistedState = {
+ currentIndexPatternId: '1',
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2', 'col3'],
+ columns: {
+ col1: {
+ label: 'Count of records',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
+ col2: {
+ label: 'Date',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'auto',
+ },
+ },
+ col3: {
+ label: 'Date 2',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'another_datefield',
+ params: {
+ interval: 'auto',
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const state = stateFromPersistedState(queryPersistedState);
+
+ const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
+ expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
+ });
+
+ it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => {
+ const queryPersistedState: IndexPatternPersistedState = {
+ currentIndexPatternId: '1',
+ layers: {
+ first: {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col1: {
+ label: 'Count of records',
+ dataType: 'date',
+ isBucketed: false,
+ sourceField: 'timefield',
+ operationType: 'cardinality',
+ },
+ col2: {
+ label: 'Date',
+ dataType: 'date',
+ isBucketed: true,
+ operationType: 'date_histogram',
+ sourceField: 'timestamp',
+ params: {
+ interval: 'auto',
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const state = stateFromPersistedState(queryPersistedState);
+
+ const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
+ expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']);
+ expect(ast.chain[0].arguments.timeFields).not.toContain('timefield');
+ });
});
describe('#insertLayer', () => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
index 2008b326a539ce..02471b935c97c0 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
@@ -184,6 +184,7 @@ describe('IndexPattern Data Source suggestions', () => {
id2: expect.objectContaining({
operationType: 'terms',
sourceField: 'source',
+ params: expect.objectContaining({ size: 5 }),
}),
id3: expect.objectContaining({
operationType: 'count',
@@ -388,6 +389,7 @@ describe('IndexPattern Data Source suggestions', () => {
id1: expect.objectContaining({
operationType: 'terms',
sourceField: 'source',
+ params: expect.objectContaining({ size: 5 }),
}),
id2: expect.objectContaining({
operationType: 'count',
@@ -779,7 +781,7 @@ describe('IndexPattern Data Source suggestions', () => {
expect(suggestions[0].table.columns[0].operation.isBucketed).toBeFalsy();
});
- it('appends a terms column on string field', () => {
+ it('appends a terms column with default size on string field', () => {
const initialState = stateWithNonEmptyTables();
const suggestions = getDatasourceSuggestionsForField(initialState, '1', {
name: 'dest',
@@ -800,6 +802,7 @@ describe('IndexPattern Data Source suggestions', () => {
id1: expect.objectContaining({
operationType: 'terms',
sourceField: 'dest',
+ params: expect.objectContaining({ size: 3 }),
}),
},
}),
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
index 2b3e976a77ea75..44963722f8afcb 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts
@@ -17,6 +17,7 @@ import {
OperationType,
} from './operations';
import { operationDefinitions } from './operations/definitions';
+import { TermsIndexPatternColumn } from './operations/definitions/terms';
import { hasField } from './utils';
import {
IndexPattern,
@@ -232,6 +233,10 @@ function addFieldAsBucketOperation(
[newColumnId]: newColumn,
};
+ if (buckets.length === 0 && operation === 'terms') {
+ (newColumn as TermsIndexPatternColumn).params.size = 5;
+ }
+
const oldDateHistogramIndex = layer.columnOrder.findIndex(
columnId => layer.columns[columnId].operationType === 'date_histogram'
);
@@ -327,6 +332,9 @@ function createNewLayerWithBucketAggregation(
field,
suggestedPriority: undefined,
});
+ if (operation === 'terms') {
+ (column as TermsIndexPatternColumn).params.size = 5;
+ }
return {
indexPatternId: indexPattern.id,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
index 3ab51b5fa3f2b4..1308fa3b7ca603 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
@@ -10,6 +10,7 @@ import { IndexPatternColumn } from './indexpattern';
import { operationDefinitionMap } from './operations';
import { IndexPattern, IndexPatternPrivateState } from './types';
import { OriginalColumn } from './rename_columns';
+import { dateHistogramOperation } from './operations/definitions';
function getExpressionForLayer(
indexPattern: IndexPattern,
@@ -68,6 +69,12 @@ function getExpressionForLayer(
return base;
});
+ const allDateHistogramFields = Object.values(columns)
+ .map(column =>
+ column.operationType === dateHistogramOperation.type ? column.sourceField : null
+ )
+ .filter((field): field is string => Boolean(field));
+
return {
type: 'expression',
chain: [
@@ -79,20 +86,8 @@ function getExpressionForLayer(
metricsAtAllLevels: [false],
partialRows: [false],
includeFormatHints: [true],
- aggConfigs: [
- {
- type: 'expression',
- chain: [
- {
- type: 'function',
- function: 'lens_auto_date',
- arguments: {
- aggConfigs: [JSON.stringify(aggs)],
- },
- },
- ],
- },
- ],
+ timeFields: allDateHistogramFields,
+ aggConfigs: [JSON.stringify(aggs)],
},
},
{
diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
index 73b8019a31eaa6..04a1c3865f22d5 100644
--- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx
@@ -53,6 +53,10 @@ export const metricVisualization: Visualization = {
},
],
+ getVisualizationTypeId() {
+ return 'lnsMetric';
+ },
+
clearLayer(state) {
return {
...state,
diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts
index 181f192520d0dd..ed0af8545f0122 100644
--- a/x-pack/plugins/lens/public/types.ts
+++ b/x-pack/plugins/lens/public/types.ts
@@ -312,6 +312,10 @@ export interface SuggestionRequest {
* The visualization needs to know which table is being suggested
*/
keptLayerIds: string[];
+ /**
+ * Different suggestions can be generated for each subtype of the visualization
+ */
+ subVisualizationId?: string;
}
/**
@@ -388,6 +392,11 @@ export interface Visualization {
* but can register multiple subtypes
*/
visualizationTypes: VisualizationType[];
+ /**
+ * Return the ID of the current visualization. Used to highlight
+ * the active subtype of the visualization.
+ */
+ getVisualizationTypeId: (state: T) => string;
/**
* If the visualization has subtypes, update the subtype in state.
*/
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
index beccf0dc46eb45..d176905c65120b 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts
@@ -27,7 +27,7 @@ function exampleState(): State {
}
describe('xy_visualization', () => {
- describe('getDescription', () => {
+ describe('#getDescription', () => {
function mixedState(...types: SeriesType[]) {
const state = exampleState();
return {
@@ -81,6 +81,45 @@ describe('xy_visualization', () => {
});
});
+ describe('#getVisualizationTypeId', () => {
+ function mixedState(...types: SeriesType[]) {
+ const state = exampleState();
+ return {
+ ...state,
+ layers: types.map((t, i) => ({
+ ...state.layers[0],
+ layerId: `layer_${i}`,
+ seriesType: t,
+ })),
+ };
+ }
+
+ it('should show mixed when each layer is different', () => {
+ expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed');
+ });
+
+ it('should show the preferredSeriesType if there are no layers', () => {
+ expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar');
+ });
+
+ it('should combine multiple layers into one type', () => {
+ expect(
+ xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal'))
+ ).toEqual('bar_horizontal');
+ });
+
+ it('should return the subtype for single layers', () => {
+ expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area');
+ expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line');
+ expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual(
+ 'area_stacked'
+ );
+ expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual(
+ 'bar_horizontal_stacked'
+ );
+ });
+ });
+
describe('#initialize', () => {
it('loads default state', () => {
const mockFrame = createMockFramePublicAPI();
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
index c72fa0fec24d77..e91edf9cc01833 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx
@@ -12,7 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { getSuggestions } from './xy_suggestions';
import { LayerContextMenu } from './xy_config_panel';
-import { Visualization, OperationMetadata } from '../types';
+import { Visualization, OperationMetadata, VisualizationType } from '../types';
import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types';
import { toExpression, toPreviewExpression } from './to_expression';
import chartBarStackedSVG from '../assets/chart_bar_stacked.svg';
@@ -24,6 +24,18 @@ const defaultSeriesType = 'bar_stacked';
const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number';
const isBucketed = (op: OperationMetadata) => op.isBucketed;
+function getVisualizationType(state: State): VisualizationType | 'mixed' {
+ if (!state.layers.length) {
+ return (
+ visualizationTypes.find(t => t.id === state.preferredSeriesType) ?? visualizationTypes[0]
+ );
+ }
+ const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType);
+ const seriesTypes = _.unique(state.layers.map(l => l.seriesType));
+
+ return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed';
+}
+
function getDescription(state?: State) {
if (!state) {
return {
@@ -34,32 +46,31 @@ function getDescription(state?: State) {
};
}
+ const visualizationType = getVisualizationType(state);
+
if (!state.layers.length) {
- const visualizationType = visualizationTypes.find(v => v.id === state.preferredSeriesType)!;
+ const preferredType = visualizationType as VisualizationType;
return {
- icon: visualizationType.largeIcon || visualizationType.icon,
- label: visualizationType.label,
+ icon: preferredType.largeIcon || preferredType.icon,
+ label: preferredType.label,
};
}
- const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!;
- const seriesTypes = _.unique(state.layers.map(l => l.seriesType));
-
return {
icon:
- seriesTypes.length === 1
- ? visualizationType.largeIcon || visualizationType.icon
- : chartMixedSVG,
+ visualizationType === 'mixed'
+ ? chartMixedSVG
+ : visualizationType.largeIcon || visualizationType.icon,
label:
- seriesTypes.length === 1
- ? visualizationType.label
- : isHorizontalChart(state.layers)
- ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', {
- defaultMessage: 'Mixed horizontal bar',
- })
- : i18n.translate('xpack.lens.xyVisualization.mixedLabel', {
- defaultMessage: 'Mixed XY',
- }),
+ visualizationType === 'mixed'
+ ? isHorizontalChart(state.layers)
+ ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', {
+ defaultMessage: 'Mixed horizontal bar',
+ })
+ : i18n.translate('xpack.lens.xyVisualization.mixedLabel', {
+ defaultMessage: 'Mixed XY',
+ })
+ : visualizationType.label,
};
}
@@ -67,6 +78,10 @@ export const xyVisualization: Visualization = {
id: 'lnsXY',
visualizationTypes,
+ getVisualizationTypeId(state) {
+ const type = getVisualizationType(state);
+ return type === 'mixed' ? type : type.id;
+ },
getLayerIds(state) {
return state.layers.map(l => l.layerId);
diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts
index e80308cc9acdb0..4cc330d40efd7f 100644
--- a/x-pack/plugins/lens/server/migrations.test.ts
+++ b/x-pack/plugins/lens/server/migrations.test.ts
@@ -158,4 +158,124 @@ describe('Lens migrations', () => {
]);
});
});
+
+ describe('7.8.0 auto timestamp', () => {
+ const context = {} as SavedObjectMigrationContext;
+
+ const example = {
+ type: 'lens',
+ attributes: {
+ expression: `kibana
+ | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]"
+ | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c"
+ tables={esaggs
+ index="ff959d40-b880-11e8-a6d9-e546fe2bba5f"
+ metricsAtAllLevels=false
+ partialRows=false
+ includeFormatHints=true
+ aggConfigs={
+ lens_auto_date
+ aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]"
+ }
+ | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}"
+ }
+ | lens_xy_chart
+ xTitle="products.created_on"
+ yTitle="Count of records"
+ legend={lens_xy_legendConfig isVisible=true position="right"}
+ layers={lens_xy_layer
+ layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c"
+ hide=false
+ xAccessor="1d9cc16c-1460-41de-88f8-471932ecbc97"
+ yScaleType="linear"
+ xScaleType="time"
+ isHistogram=true
+ seriesType="bar_stacked"
+ accessors="66115819-8481-4917-a6dc-8ffb10dd02df"
+ columnToLabel="{\\"66115819-8481-4917-a6dc-8ffb10dd02df\\":\\"Count of records\\"}"
+ }
+ `,
+ state: {
+ datasourceStates: {
+ indexpattern: {
+ currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ layers: {
+ 'bd09dc71-a7e2-42d0-83bd-85df8291f03c': {
+ indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ columns: {
+ '1d9cc16c-1460-41de-88f8-471932ecbc97': {
+ label: 'products.created_on',
+ dataType: 'date',
+ operationType: 'date_histogram',
+ sourceField: 'products.created_on',
+ isBucketed: true,
+ scale: 'interval',
+ params: { interval: 'auto' },
+ },
+ '66115819-8481-4917-a6dc-8ffb10dd02df': {
+ label: 'Count of records',
+ dataType: 'number',
+ operationType: 'count',
+ suggestedPriority: 0,
+ isBucketed: false,
+ scale: 'ratio',
+ sourceField: 'Records',
+ },
+ },
+ columnOrder: [
+ '1d9cc16c-1460-41de-88f8-471932ecbc97',
+ '66115819-8481-4917-a6dc-8ffb10dd02df',
+ ],
+ },
+ },
+ },
+ },
+ datasourceMetaData: {
+ filterableIndexPatterns: [
+ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' },
+ ],
+ },
+ visualization: {
+ legend: { isVisible: true, position: 'right' },
+ preferredSeriesType: 'bar_stacked',
+ layers: [
+ {
+ layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c',
+ accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'],
+ position: 'top',
+ seriesType: 'bar_stacked',
+ showGridlines: false,
+ xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97',
+ },
+ ],
+ },
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ },
+ title: 'Bar chart',
+ visualizationType: 'lnsXY',
+ },
+ };
+
+ it('should remove the lens_auto_date expression', () => {
+ const result = migrations['7.8.0'](example, context);
+ expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`);
+ });
+
+ it('should handle pre-migrated expression', () => {
+ const input = {
+ type: 'lens',
+ attributes: {
+ ...example.attributes,
+ expression: `kibana
+| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]"
+| lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c"
+ tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"}
+| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`,
+ },
+ };
+ const result = migrations['7.8.0'](input, context);
+ expect(result).toEqual(input);
+ });
+ });
});
diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts
index 3d238723b7438c..583fba1a4a9992 100644
--- a/x-pack/plugins/lens/server/migrations.ts
+++ b/x-pack/plugins/lens/server/migrations.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { cloneDeep } from 'lodash';
+import { cloneDeep, flow } from 'lodash';
+import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common';
import { SavedObjectMigrationFn } from 'src/core/server';
interface XYLayerPre77 {
@@ -14,7 +15,126 @@ interface XYLayerPre77 {
accessors: string[];
}
-export const migrations: Record = {
+/**
+ * Removes the `lens_auto_date` subexpression from a stored expression
+ * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"}
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => {
+ const expression: string = doc.attributes?.expression;
+ try {
+ const ast = fromExpression(expression);
+ const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => {
+ if (topNode.function !== 'lens_merge_tables') {
+ return topNode;
+ }
+ return {
+ ...topNode,
+ arguments: {
+ ...topNode.arguments,
+ tables: (topNode.arguments.tables as Ast[]).map(middleNode => {
+ return {
+ type: 'expression',
+ chain: middleNode.chain.map(node => {
+ // Check for sub-expression in aggConfigs
+ if (
+ node.function === 'esaggs' &&
+ typeof node.arguments.aggConfigs[0] !== 'string'
+ ) {
+ return {
+ ...node,
+ arguments: {
+ ...node.arguments,
+ aggConfigs: (node.arguments.aggConfigs[0] as Ast).chain[0].arguments
+ .aggConfigs,
+ },
+ };
+ }
+ return node;
+ }),
+ };
+ }),
+ },
+ };
+ });
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ expression: toExpression({ ...ast, chain: newChain }),
+ },
+ };
+ } catch (e) {
+ context.log.warning(e.message);
+ return { ...doc };
+ }
+};
+
+/**
+ * Adds missing timeField arguments to esaggs in the Lens expression
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => {
+ const expression: string = doc.attributes?.expression;
+
+ try {
+ const ast = fromExpression(expression);
+ const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => {
+ if (topNode.function !== 'lens_merge_tables') {
+ return topNode;
+ }
+ return {
+ ...topNode,
+ arguments: {
+ ...topNode.arguments,
+ tables: (topNode.arguments.tables as Ast[]).map(middleNode => {
+ return {
+ type: 'expression',
+ chain: middleNode.chain.map(node => {
+ // Skip if there are any timeField arguments already, because that indicates
+ // the fix is already applied
+ if (node.function !== 'esaggs' || node.arguments.timeFields) {
+ return node;
+ }
+ const timeFields: string[] = [];
+ JSON.parse(node.arguments.aggConfigs[0] as string).forEach(
+ (agg: { type: string; params: { field: string } }) => {
+ if (agg.type !== 'date_histogram') {
+ return;
+ }
+ timeFields.push(agg.params.field);
+ }
+ );
+ return {
+ ...node,
+ arguments: {
+ ...node.arguments,
+ timeFields,
+ },
+ };
+ }),
+ };
+ }),
+ },
+ };
+ });
+
+ return {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ expression: toExpression({ ...ast, chain: newChain }),
+ },
+ };
+ } catch (e) {
+ context.log.warning(e.message);
+ return { ...doc };
+ }
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const migrations: Record> = {
'7.7.0': doc => {
const newDoc = cloneDeep(doc);
if (newDoc.attributes?.visualizationType === 'lnsXY') {
@@ -34,4 +154,7 @@ export const migrations: Record = {
}
return newDoc;
},
+ // The order of these migrations matter, since the timefield migration relies on the aggConfigs
+ // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was).
+ '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs),
};
diff --git a/x-pack/plugins/lists/server/get_space_id.test.ts b/x-pack/plugins/lists/server/get_space_id.test.ts
new file mode 100644
index 00000000000000..9c1d11b71984d1
--- /dev/null
+++ b/x-pack/plugins/lists/server/get_space_id.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { httpServerMock } from 'src/core/server/mocks';
+import { KibanaRequest } from 'src/core/server';
+
+import { spacesServiceMock } from '../../spaces/server/spaces_service/spaces_service.mock';
+
+import { getSpaceId } from './get_space_id';
+
+describe('get_space_id', () => {
+ let request = KibanaRequest.from(httpServerMock.createRawRequest({}));
+ beforeEach(() => {
+ request = KibanaRequest.from(httpServerMock.createRawRequest({}));
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it returns "default" as the space id given a space id of "default"', () => {
+ const spaces = spacesServiceMock.createSetupContract();
+ const space = getSpaceId({ request, spaces });
+ expect(space).toEqual('default');
+ });
+
+ test('it returns "another-space" as the space id given a space id of "another-space"', () => {
+ const spaces = spacesServiceMock.createSetupContract('another-space');
+ const space = getSpaceId({ request, spaces });
+ expect(space).toEqual('another-space');
+ });
+
+ test('it returns "default" as the space id given a space id of undefined', () => {
+ const space = getSpaceId({ request, spaces: undefined });
+ expect(space).toEqual('default');
+ });
+
+ test('it returns "default" as the space id given a space id of null', () => {
+ const space = getSpaceId({ request, spaces: null });
+ expect(space).toEqual('default');
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/utils/get_space.ts b/x-pack/plugins/lists/server/get_space_id.ts
similarity index 83%
rename from x-pack/plugins/lists/server/services/utils/get_space.ts
rename to x-pack/plugins/lists/server/get_space_id.ts
index e23f963b2c40d2..f224e37e044672 100644
--- a/x-pack/plugins/lists/server/services/utils/get_space.ts
+++ b/x-pack/plugins/lists/server/get_space_id.ts
@@ -6,9 +6,9 @@
import { KibanaRequest } from 'kibana/server';
-import { SpacesServiceSetup } from '../../../../spaces/server';
+import { SpacesServiceSetup } from '../../spaces/server';
-export const getSpace = ({
+export const getSpaceId = ({
spaces,
request,
}: {
diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts
new file mode 100644
index 00000000000000..0992e3c361fcfe
--- /dev/null
+++ b/x-pack/plugins/lists/server/get_user.test.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { httpServerMock } from 'src/core/server/mocks';
+import { KibanaRequest } from 'src/core/server';
+
+import { securityMock } from '../../security/server/mocks';
+import { SecurityPluginSetup } from '../../security/server';
+
+import { getUser } from './get_user';
+
+describe('get_user', () => {
+ let request = KibanaRequest.from(httpServerMock.createRawRequest({}));
+ beforeEach(() => {
+ jest.clearAllMocks();
+ request = KibanaRequest.from(httpServerMock.createRawRequest({}));
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it returns "bob" as the user given a security request with "bob"', () => {
+ const security: SecurityPluginSetup = securityMock.createSetup();
+ security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' });
+ const user = getUser({ request, security });
+ expect(user).toEqual('bob');
+ });
+
+ test('it returns "alice" as the user given a security request with "alice"', () => {
+ const security: SecurityPluginSetup = securityMock.createSetup();
+ security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' });
+ const user = getUser({ request, security });
+ expect(user).toEqual('alice');
+ });
+
+ test('it returns "elastic" as the user given null as the current user', () => {
+ const security: SecurityPluginSetup = securityMock.createSetup();
+ security.authc.getCurrentUser = jest.fn().mockReturnValue(null);
+ const user = getUser({ request, security });
+ expect(user).toEqual('elastic');
+ });
+
+ test('it returns "elastic" as the user given undefined as the current user', () => {
+ const security: SecurityPluginSetup = securityMock.createSetup();
+ security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined);
+ const user = getUser({ request, security });
+ expect(user).toEqual('elastic');
+ });
+
+ test('it returns "elastic" as the user given undefined as the plugin', () => {
+ const security: SecurityPluginSetup = securityMock.createSetup();
+ security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined);
+ const user = getUser({ request, security: undefined });
+ expect(user).toEqual('elastic');
+ });
+
+ test('it returns "elastic" as the user given null as the plugin', () => {
+ const security: SecurityPluginSetup = securityMock.createSetup();
+ security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined);
+ const user = getUser({ request, security: null });
+ expect(user).toEqual('elastic');
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/utils/get_user.ts b/x-pack/plugins/lists/server/get_user.ts
similarity index 54%
rename from x-pack/plugins/lists/server/services/utils/get_user.ts
rename to x-pack/plugins/lists/server/get_user.ts
index 1ddad047da7229..3b59853d0ab62a 100644
--- a/x-pack/plugins/lists/server/services/utils/get_user.ts
+++ b/x-pack/plugins/lists/server/get_user.ts
@@ -6,17 +6,21 @@
import { KibanaRequest } from 'kibana/server';
-import { SecurityPluginSetup } from '../../../../security/server';
+import { SecurityPluginSetup } from '../../security/server';
-interface GetUserOptions {
- security: SecurityPluginSetup;
+export interface GetUserOptions {
+ security: SecurityPluginSetup | null | undefined;
request: KibanaRequest;
}
export const getUser = ({ security, request }: GetUserOptions): string => {
- const authenticatedUser = security.authc.getCurrentUser(request);
- if (authenticatedUser != null) {
- return authenticatedUser.username;
+ if (security != null) {
+ const authenticatedUser = security.authc.getCurrentUser(request);
+ if (authenticatedUser != null) {
+ return authenticatedUser.username;
+ } else {
+ return 'elastic';
+ }
} else {
return 'elastic';
}
diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts
index 4473d68d3c6465..2498c36967a536 100644
--- a/x-pack/plugins/lists/server/plugin.ts
+++ b/x-pack/plugins/lists/server/plugin.ts
@@ -5,7 +5,7 @@
*/
import { first } from 'rxjs/operators';
-import { ElasticsearchServiceSetup, Logger, PluginInitializerContext } from 'kibana/server';
+import { Logger, PluginInitializerContext } from 'kibana/server';
import { CoreSetup } from 'src/core/server';
import { SecurityPluginSetup } from '../../security/server';
@@ -16,12 +16,13 @@ import { initRoutes } from './routes/init_routes';
import { ListClient } from './services/lists/client';
import { ContextProvider, ContextProviderReturn, PluginsSetup } from './types';
import { createConfig$ } from './create_config';
+import { getSpaceId } from './get_space_id';
+import { getUser } from './get_user';
export class ListPlugin {
private readonly logger: Logger;
private spaces: SpacesServiceSetup | undefined | null;
private config: ConfigType | undefined | null;
- private elasticsearch: ElasticsearchServiceSetup | undefined | null;
private security: SecurityPluginSetup | undefined | null;
constructor(private readonly initializerContext: PluginInitializerContext) {
@@ -38,7 +39,6 @@ export class ListPlugin {
);
this.spaces = plugins.spaces?.spacesService;
this.config = config;
- this.elasticsearch = core.elasticsearch;
this.security = plugins.security;
core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext());
@@ -56,28 +56,28 @@ export class ListPlugin {
private createRouteHandlerContext = (): ContextProvider => {
return async (context, request): ContextProviderReturn => {
- const { spaces, config, security, elasticsearch } = this;
+ const { spaces, config, security } = this;
const {
core: {
- elasticsearch: { dataClient },
+ elasticsearch: {
+ dataClient: { callAsCurrentUser },
+ },
},
} = context;
if (config == null) {
throw new TypeError('Configuration is required for this plugin to operate');
- } else if (elasticsearch == null) {
- throw new TypeError('Elastic Search is required for this plugin to operate');
- } else if (security == null) {
- // TODO: This might be null, test authentication being turned off.
- throw new TypeError('Security plugin is required for this plugin to operate');
} else {
+ const spaceId = getSpaceId({ request, spaces });
+ const user = getUser({ request, security });
return {
getListClient: (): ListClient =>
new ListClient({
+ callCluster: callAsCurrentUser,
config,
- dataClient,
request,
security,
- spaces,
+ spaceId,
+ user,
}),
};
}
diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
index 946e1c240be31c..48deb3ee86820d 100644
--- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
+++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { TestReadable } from '../mocks/test_readable';
+import { TestReadable } from '../mocks';
import { BufferLines } from './buffer_lines';
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
index b2bca241c468cb..abbb2701499557 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
@@ -31,7 +31,7 @@ describe('crete_list_item', () => {
expect(listItem).toEqual(expected);
});
- test('It calls "callAsCurrentUser" with body, index, and listIndex', async () => {
+ test('It calls "callCluster" with body, index, and listIndex', async () => {
const options = getCreateListItemOptionsMock();
await createListItem(options);
const body = getIndexESListItemMock();
@@ -40,7 +40,7 @@ describe('crete_list_item', () => {
id: LIST_ITEM_ID,
index: LIST_ITEM_INDEX,
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('index', expected);
+ expect(options.callCluster).toBeCalledWith('index', expected);
});
test('It returns an auto-generated id if id is sent in undefined', async () => {
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts
index da1e192bf2412d..83a118b7951923 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts
@@ -6,6 +6,7 @@
import uuid from 'uuid';
import { CreateDocumentResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import {
IdOrUndefined,
@@ -14,7 +15,6 @@ import {
MetaOrUndefined,
Type,
} from '../../../common/schemas';
-import { DataClient } from '../../types';
import { transformListItemToElasticQuery } from '../utils';
export interface CreateListItemOptions {
@@ -22,7 +22,7 @@ export interface CreateListItemOptions {
listId: string;
type: Type;
value: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
user: string;
meta: MetaOrUndefined;
@@ -35,7 +35,7 @@ export const createListItem = async ({
listId,
type,
value,
- dataClient,
+ callCluster,
listItemIndex,
user,
meta,
@@ -58,7 +58,7 @@ export const createListItem = async ({
...transformListItemToElasticQuery({ type, value }),
};
- const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('index', {
+ const response: CreateDocumentResponse = await callCluster('index', {
body,
id,
index: listItemIndex,
diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
index 9263b975b20e74..94cc57b53b4e24 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
@@ -24,13 +24,13 @@ describe('crete_list_item_bulk', () => {
jest.clearAllMocks();
});
- test('It calls "callAsCurrentUser" with body, index, and the bulk items', async () => {
+ test('It calls "callCluster" with body, index, and the bulk items', async () => {
const options = getCreateListItemBulkOptionsMock();
await createListItemsBulk(options);
const firstRecord: IndexEsListItemSchema = getIndexESListItemMock();
const secondRecord: IndexEsListItemSchema = getIndexESListItemMock(VALUE_2);
[firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS;
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('bulk', {
+ expect(options.callCluster).toBeCalledWith('bulk', {
body: [
{ create: { _index: LIST_ITEM_INDEX } },
firstRecord,
@@ -44,6 +44,6 @@ describe('crete_list_item_bulk', () => {
test('It should not call the dataClient when the values are empty', async () => {
const options = getCreateListItemBulkOptionsMock();
options.value = [];
- expect(options.dataClient.callAsCurrentUser).not.toBeCalled();
+ expect(options.callCluster).not.toBeCalled();
});
});
diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
index 7100a5f8eaabca..eac294c5f244a2 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
@@ -5,9 +5,9 @@
*/
import uuid from 'uuid';
+import { APICaller } from 'kibana/server';
import { transformListItemToElasticQuery } from '../utils';
-import { DataClient } from '../../types';
import {
CreateEsBulkTypeSchema,
IndexEsListItemSchema,
@@ -19,7 +19,7 @@ export interface CreateListItemsBulkOptions {
listId: string;
type: Type;
value: string[];
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
user: string;
meta: MetaOrUndefined;
@@ -31,7 +31,7 @@ export const createListItemsBulk = async ({
listId,
type,
value,
- dataClient,
+ callCluster,
listItemIndex,
user,
meta,
@@ -63,7 +63,7 @@ export const createListItemsBulk = async ({
[]
);
- await dataClient.callAsCurrentUser('bulk', {
+ await callCluster('bulk', {
body,
index: listItemIndex,
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
index 795c579462b691..00fcefb2c379fc 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
@@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LIST_ITEM_ID, LIST_ITEM_INDEX, getListItemResponseMock } from '../mocks';
-import { getDeleteListItemOptionsMock } from '../mocks/get_delete_list_item_options_mock';
+import {
+ LIST_ITEM_ID,
+ LIST_ITEM_INDEX,
+ getDeleteListItemOptionsMock,
+ getListItemResponseMock,
+} from '../mocks';
import { getListItem } from './get_list_item';
import { deleteListItem } from './delete_list_item';
@@ -37,6 +41,7 @@ describe('delete_list_item', () => {
const deletedListItem = await deleteListItem(options);
expect(deletedListItem).toEqual(listItem);
});
+
test('Delete calls "delete" if a list item is returned from "getListItem"', async () => {
const listItem = getListItemResponseMock();
((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem);
@@ -46,6 +51,6 @@ describe('delete_list_item', () => {
id: LIST_ITEM_ID,
index: LIST_ITEM_INDEX,
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('delete', deleteQuery);
+ expect(options.callCluster).toBeCalledWith('delete', deleteQuery);
});
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts
index ffce2d3b2af816..9992f43387c89f 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts
@@ -4,27 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { APICaller } from 'kibana/server';
+
import { Id, ListItemSchema } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { getListItem } from '.';
export interface DeleteListItemOptions {
id: Id;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
}
export const deleteListItem = async ({
id,
- dataClient,
+ callCluster,
listItemIndex,
}: DeleteListItemOptions): Promise => {
- const listItem = await getListItem({ dataClient, id, listItemIndex });
+ const listItem = await getListItem({ callCluster, id, listItemIndex });
if (listItem == null) {
return null;
} else {
- await dataClient.callAsCurrentUser('delete', {
+ await callCluster('delete', {
id,
index: listItemIndex,
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
index dee890445f9a3b..c7c80638e4c374 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
@@ -52,6 +52,6 @@ describe('delete_list_item_by_value', () => {
},
index: '.items',
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('deleteByQuery', deleteByQuery);
+ expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery);
});
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
index f2f5ec3078e621..ec29f14a0ff647 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { APICaller } from 'kibana/server';
+
import { ListItemArraySchema, Type } from '../../../common/schemas';
import { getQueryFilterFromTypeValue } from '../utils';
-import { DataClient } from '../../types';
import { getListItemByValues } from './get_list_item_by_values';
@@ -14,7 +15,7 @@ export interface DeleteListItemByValueOptions {
listId: string;
type: Type;
value: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
}
@@ -22,11 +23,11 @@ export const deleteListItemByValue = async ({
listId,
value,
type,
- dataClient,
+ callCluster,
listItemIndex,
}: DeleteListItemByValueOptions): Promise => {
const listItems = await getListItemByValues({
- dataClient,
+ callCluster,
listId,
listItemIndex,
type,
@@ -38,7 +39,7 @@ export const deleteListItemByValue = async ({
type,
value: values,
});
- await dataClient.callAsCurrentUser('deleteByQuery', {
+ await callCluster('deleteByQuery', {
body: {
query: {
bool: {
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts
index 937993f1d8f713..31a421c2e31bfe 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts
@@ -4,8 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LIST_ID, LIST_INDEX, getDataClientMock, getListItemResponseMock } from '../mocks';
-import { getSearchListItemMock } from '../mocks/get_search_list_item_mock';
+import {
+ LIST_ID,
+ LIST_INDEX,
+ getCallClusterMock,
+ getListItemResponseMock,
+ getSearchListItemMock,
+} from '../mocks';
import { getListItem } from './get_list_item';
@@ -20,8 +25,8 @@ describe('get_list_item', () => {
test('it returns a list item as expected if the list item is found', async () => {
const data = getSearchListItemMock();
- const dataClient = getDataClientMock(data);
- const list = await getListItem({ dataClient, id: LIST_ID, listItemIndex: LIST_INDEX });
+ const callCluster = getCallClusterMock(data);
+ const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX });
const expected = getListItemResponseMock();
expect(list).toEqual(expected);
});
@@ -29,8 +34,8 @@ describe('get_list_item', () => {
test('it returns null if the search is empty', async () => {
const data = getSearchListItemMock();
data.hits.hits = [];
- const dataClient = getDataClientMock(data);
- const list = await getListItem({ dataClient, id: LIST_ID, listItemIndex: LIST_INDEX });
+ const callCluster = getCallClusterMock(data);
+ const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX });
expect(list).toEqual(null);
});
});
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts
index 1c91b698016483..83b30d336ccd47 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts
@@ -5,36 +5,33 @@
*/
import { SearchResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { deriveTypeFromItem, transformElasticToListItem } from '../utils';
interface GetListItemOptions {
id: Id;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
}
export const getListItem = async ({
id,
- dataClient,
+ callCluster,
listItemIndex,
}: GetListItemOptions): Promise => {
- const listItemES: SearchResponse = await dataClient.callAsCurrentUser(
- 'search',
- {
- body: {
- query: {
- term: {
- _id: id,
- },
+ const listItemES: SearchResponse = await callCluster('search', {
+ body: {
+ query: {
+ term: {
+ _id: id,
},
},
- ignoreUnavailable: true,
- index: listItemIndex,
- }
- );
+ },
+ ignoreUnavailable: true,
+ index: listItemIndex,
+ });
if (listItemES.hits.hits.length) {
const type = deriveTypeFromItem({ item: listItemES.hits.hits[0]._source });
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts
index a6efcbc0d3ffb9..49bcf12043d7c9 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts
@@ -4,14 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { APICaller } from 'kibana/server';
+
import { ListItemArraySchema, Type } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { getListItemByValues } from '.';
export interface GetListItemByValueOptions {
listId: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
type: Type;
value: string;
@@ -19,13 +20,13 @@ export interface GetListItemByValueOptions {
export const getListItemByValue = async ({
listId,
- dataClient,
+ callCluster,
listItemIndex,
type,
value,
}: GetListItemByValueOptions): Promise =>
getListItemByValues({
- dataClient,
+ callCluster,
listId,
listItemIndex,
type,
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts
index 55b170487d95a4..7f5fff4dc3147a 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts
@@ -4,8 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2, getDataClientMock } from '../mocks';
-import { getSearchListItemMock } from '../mocks/get_search_list_item_mock';
+import {
+ LIST_ID,
+ LIST_ITEM_INDEX,
+ TYPE,
+ VALUE,
+ VALUE_2,
+ getCallClusterMock,
+ getSearchListItemMock,
+} from '../mocks';
import { getListItemByValues } from './get_list_item_by_values';
@@ -21,27 +28,29 @@ describe('get_list_item_by_values', () => {
test('Returns a an empty array if the ES query is also empty', async () => {
const data = getSearchListItemMock();
data.hits.hits = [];
- const dataClient = getDataClientMock(data);
+ const callCluster = getCallClusterMock(data);
const listItem = await getListItemByValues({
- dataClient,
+ callCluster,
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
type: TYPE,
value: [VALUE, VALUE_2],
});
+
expect(listItem).toEqual([]);
});
test('Returns transformed list item if the data exists within ES', async () => {
const data = getSearchListItemMock();
- const dataClient = getDataClientMock(data);
+ const callCluster = getCallClusterMock(data);
const listItem = await getListItemByValues({
- dataClient,
+ callCluster,
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
type: TYPE,
value: [VALUE, VALUE_2],
});
+
expect(listItem).toEqual([
{
created_at: '2020-04-20T15:25:31.830Z',
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts
index 1e5c0b4a6655cc..29b9b017540270 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts
@@ -5,14 +5,14 @@
*/
import { SearchResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils';
export interface GetListItemByValuesOptions {
listId: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
type: Type;
value: string[];
@@ -20,25 +20,22 @@ export interface GetListItemByValuesOptions {
export const getListItemByValues = async ({
listId,
- dataClient,
+ callCluster,
listItemIndex,
type,
value,
}: GetListItemByValuesOptions): Promise => {
- const response: SearchResponse = await dataClient.callAsCurrentUser(
- 'search',
- {
- body: {
- query: {
- bool: {
- filter: getQueryFilterFromTypeValue({ listId, type, value }),
- },
+ const response: SearchResponse = await callCluster('search', {
+ body: {
+ query: {
+ bool: {
+ filter: getQueryFilterFromTypeValue({ listId, type, value }),
},
},
- ignoreUnavailable: true,
- index: listItemIndex,
- size: value.length, // This has a limit on the number which is 10k
- }
- );
+ },
+ ignoreUnavailable: true,
+ index: listItemIndex,
+ size: value.length, // This has a limit on the number which is 10k
+ });
return transformElasticToListItem({ response, type });
};
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts
index 0ea8320e966bdf..ffe2eff9f3ca70 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts
@@ -4,17 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { httpServerMock } from 'src/core/server/mocks';
-import { KibanaRequest } from 'src/core/server';
-
-import { getSpace } from '../utils';
-
import { getListItemIndex } from './get_list_item_index';
-jest.mock('../utils', () => ({
- getSpace: jest.fn(),
-}));
-
describe('get_list_item_index', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -25,13 +16,9 @@ describe('get_list_item_index', () => {
});
test('Returns the list item index when there is a space', async () => {
- ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space');
- const rawRequest = httpServerMock.createRawRequest({});
- const request = KibanaRequest.from(rawRequest);
const listIndex = getListItemIndex({
listsItemsIndexName: 'lists-items-index',
- request,
- spaces: undefined,
+ spaceId: 'test-space',
});
expect(listIndex).toEqual('lists-items-index-test-space');
});
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts
index c9f1bfd4d44e46..4cd93df6d9bf42 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts
@@ -4,19 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { KibanaRequest } from 'kibana/server';
-
-import { SpacesServiceSetup } from '../../../../spaces/server';
-import { getSpace } from '../utils';
-
-interface GetListItemIndexOptions {
- spaces: SpacesServiceSetup | undefined | null;
- request: KibanaRequest;
+export interface GetListItemIndexOptions {
+ spaceId: string;
listsItemsIndexName: string;
}
export const getListItemIndex = ({
- spaces,
- request,
+ spaceId,
listsItemsIndexName,
-}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${getSpace({ request, spaces })}`;
+}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${spaceId}`;
diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts
index ce4f8125d77af3..6a71b2a0caf415 100644
--- a/x-pack/plugins/lists/server/services/items/update_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts
@@ -5,6 +5,7 @@
*/
import { CreateDocumentResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import {
Id,
@@ -13,14 +14,13 @@ import {
UpdateEsListItemSchema,
} from '../../../common/schemas';
import { transformListItemToElasticQuery } from '../utils';
-import { DataClient } from '../../types';
import { getListItem } from './get_list_item';
export interface UpdateListItemOptions {
id: Id;
value: string | null | undefined;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
user: string;
meta: MetaOrUndefined;
@@ -30,14 +30,14 @@ export interface UpdateListItemOptions {
export const updateListItem = async ({
id,
value,
- dataClient,
+ callCluster,
listItemIndex,
user,
meta,
dateNow,
}: UpdateListItemOptions): Promise => {
const updatedAt = dateNow ?? new Date().toISOString();
- const listItem = await getListItem({ dataClient, id, listItemIndex });
+ const listItem = await getListItem({ callCluster, id, listItemIndex });
if (listItem == null) {
return null;
} else {
@@ -48,7 +48,7 @@ export const updateListItem = async ({
...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }),
};
- const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('update', {
+ const response: CreateDocumentResponse = await callCluster('update', {
body: {
doc,
},
diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
index 1fe1023e28ab9f..542c2bb12d8e51 100644
--- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
+++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
@@ -6,8 +6,9 @@
import { Readable } from 'stream';
+import { APICaller } from 'kibana/server';
+
import { MetaOrUndefined, Type } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { BufferLines } from './buffer_lines';
import { getListItemByValues } from './get_list_item_by_values';
@@ -16,7 +17,7 @@ import { createListItemsBulk } from './create_list_items_bulk';
export interface ImportListItemsToStreamOptions {
listId: string;
stream: Readable;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
type: Type;
user: string;
@@ -26,7 +27,7 @@ export interface ImportListItemsToStreamOptions {
export const importListItemsToStream = ({
listId,
stream,
- dataClient,
+ callCluster,
listItemIndex,
type,
user,
@@ -37,7 +38,7 @@ export const importListItemsToStream = ({
readBuffer.on('lines', async (lines: string[]) => {
await writeBufferToItems({
buffer: lines,
- dataClient,
+ callCluster,
listId,
listItemIndex,
meta,
@@ -54,7 +55,7 @@ export const importListItemsToStream = ({
export interface WriteBufferToItemsOptions {
listId: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
buffer: string[];
type: Type;
@@ -69,7 +70,7 @@ export interface LinesResult {
export const writeBufferToItems = async ({
listId,
- dataClient,
+ callCluster,
listItemIndex,
buffer,
type,
@@ -77,7 +78,7 @@ export const writeBufferToItems = async ({
meta,
}: WriteBufferToItemsOptions): Promise => {
const items = await getListItemByValues({
- dataClient,
+ callCluster,
listId,
listItemIndex,
type,
@@ -89,7 +90,7 @@ export const writeBufferToItems = async ({
const linesProcessed = duplicatesRemoved.length;
const duplicatesFound = buffer.length - duplicatesRemoved.length;
await createListItemsBulk({
- dataClient,
+ callCluster,
listId,
listItemIndex,
meta,
diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts
index 63e9aeb61bad06..b08e5fa688b4b0 100644
--- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts
+++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts
@@ -7,13 +7,13 @@
import {
LIST_ID,
LIST_ITEM_INDEX,
- getDataClientMock,
+ getCallClusterMock,
getExportListItemsToStreamOptionsMock,
getResponseOptionsMock,
+ getSearchListItemMock,
getWriteNextResponseOptions,
getWriteResponseHitsToStreamOptionsMock,
} from '../mocks';
-import { getSearchListItemMock } from '../mocks/get_search_list_item_mock';
import {
exportListItemsToStream,
@@ -37,7 +37,7 @@ describe('write_list_items_to_stream', () => {
const options = getExportListItemsToStreamOptionsMock();
const firstResponse = getSearchListItemMock();
firstResponse.hits.hits = [];
- options.dataClient = getDataClientMock(firstResponse);
+ options.callCluster = getCallClusterMock(firstResponse);
exportListItemsToStream(options);
let chunks: string[] = [];
@@ -71,7 +71,7 @@ describe('write_list_items_to_stream', () => {
const firstResponse = getSearchListItemMock();
const secondResponse = getSearchListItemMock();
firstResponse.hits.hits = [...firstResponse.hits.hits, ...secondResponse.hits.hits];
- options.dataClient = getDataClientMock(firstResponse);
+ options.callCluster = getCallClusterMock(firstResponse);
exportListItemsToStream(options);
let chunks: string[] = [];
@@ -90,15 +90,14 @@ describe('write_list_items_to_stream', () => {
const firstResponse = getSearchListItemMock();
firstResponse.hits.hits[0].sort = ['some-sort-value'];
+
const secondResponse = getSearchListItemMock();
secondResponse.hits.hits[0]._source.ip = '255.255.255.255';
- const jestCalls = jest.fn().mockResolvedValueOnce(firstResponse);
- jestCalls.mockResolvedValueOnce(secondResponse);
-
- const dataClient = getDataClientMock(firstResponse);
- dataClient.callAsCurrentUser = jestCalls;
- options.dataClient = dataClient;
+ options.callCluster = jest
+ .fn()
+ .mockResolvedValueOnce(firstResponse)
+ .mockResolvedValueOnce(secondResponse);
exportListItemsToStream(options);
@@ -125,7 +124,7 @@ describe('write_list_items_to_stream', () => {
const listItem = getSearchListItemMock();
listItem.hits.hits[0].sort = ['sort-value-1'];
const options = getWriteNextResponseOptions();
- options.dataClient = getDataClientMock(listItem);
+ options.callCluster = getCallClusterMock(listItem);
const searchAfter = await writeNextResponse(options);
expect(searchAfter).toEqual(['sort-value-1']);
});
@@ -134,7 +133,7 @@ describe('write_list_items_to_stream', () => {
const listItem = getSearchListItemMock();
listItem.hits.hits = [];
const options = getWriteNextResponseOptions();
- options.dataClient = getDataClientMock(listItem);
+ options.callCluster = getCallClusterMock(listItem);
const searchAfter = await writeNextResponse(options);
expect(searchAfter).toEqual(undefined);
});
@@ -187,7 +186,7 @@ describe('write_list_items_to_stream', () => {
index: LIST_ITEM_INDEX,
size: 100,
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('search', expected);
+ expect(options.callCluster).toBeCalledWith('search', expected);
});
test('It returns a simple response with expected values and size changed', async () => {
@@ -205,7 +204,7 @@ describe('write_list_items_to_stream', () => {
index: LIST_ITEM_INDEX,
size: 33,
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('search', expected);
+ expect(options.callCluster).toBeCalledWith('search', expected);
});
});
diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts
index 0e0ae7b924e17a..b81e4a4fc73c20 100644
--- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts
+++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts
@@ -7,9 +7,9 @@
import { PassThrough } from 'stream';
import { SearchResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import { SearchEsListItemSchema } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { ErrorWithStatusCode } from '../../error_with_status_code';
/**
@@ -20,7 +20,7 @@ export const SIZE = 100;
export interface ExportListItemsToStreamOptions {
listId: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
stream: PassThrough;
stringToAppend: string | null | undefined;
@@ -28,7 +28,7 @@ export interface ExportListItemsToStreamOptions {
export const exportListItemsToStream = ({
listId,
- dataClient,
+ callCluster,
stream,
listItemIndex,
stringToAppend,
@@ -37,7 +37,7 @@ export const exportListItemsToStream = ({
// and prevent the async await from bubbling up to the caller
setTimeout(async () => {
let searchAfter = await writeNextResponse({
- dataClient,
+ callCluster,
listId,
listItemIndex,
searchAfter: undefined,
@@ -46,7 +46,7 @@ export const exportListItemsToStream = ({
});
while (searchAfter != null) {
searchAfter = await writeNextResponse({
- dataClient,
+ callCluster,
listId,
listItemIndex,
searchAfter,
@@ -60,7 +60,7 @@ export const exportListItemsToStream = ({
export interface WriteNextResponseOptions {
listId: string;
- dataClient: DataClient;
+ callCluster: APICaller;
listItemIndex: string;
stream: PassThrough;
searchAfter: string[] | undefined;
@@ -69,14 +69,14 @@ export interface WriteNextResponseOptions {
export const writeNextResponse = async ({
listId,
- dataClient,
+ callCluster,
stream,
listItemIndex,
searchAfter,
stringToAppend,
}: WriteNextResponseOptions): Promise => {
const response = await getResponse({
- dataClient,
+ callCluster,
listId,
listItemIndex,
searchAfter,
@@ -100,7 +100,7 @@ export const getSearchAfterFromResponse = ({
: undefined;
export interface GetResponseOptions {
- dataClient: DataClient;
+ callCluster: APICaller;
listId: string;
searchAfter: undefined | string[];
listItemIndex: string;
@@ -108,13 +108,13 @@ export interface GetResponseOptions {
}
export const getResponse = async ({
- dataClient,
+ callCluster,
searchAfter,
listId,
listItemIndex,
size = SIZE,
}: GetResponseOptions): Promise> => {
- return dataClient.callAsCurrentUser('search', {
+ return callCluster('search', {
body: {
query: {
term: {
diff --git a/x-pack/plugins/lists/server/services/lists/client.ts b/x-pack/plugins/lists/server/services/lists/client.ts
index 32578fc739f265..ba22bf72cc18c3 100644
--- a/x-pack/plugins/lists/server/services/lists/client.ts
+++ b/x-pack/plugins/lists/server/services/lists/client.ts
@@ -4,10 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { KibanaRequest, ScopedClusterClient } from 'src/core/server';
+import { APICaller } from 'kibana/server';
-import { SecurityPluginSetup } from '../../../../security/server';
-import { SpacesServiceSetup } from '../../../../spaces/server';
import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas';
import { ConfigType } from '../../config';
import {
@@ -31,7 +29,6 @@ import {
importListItemsToStream,
updateListItem,
} from '../../services/items';
-import { getUser } from '../../services/utils';
import {
createBootstrapIndex,
deleteAllIndex,
@@ -64,47 +61,39 @@ import {
UpdateListOptions,
} from './client_types';
-// TODO: Consider an interface and a factory
export class ListClient {
- private readonly spaces: SpacesServiceSetup | undefined | null;
+ private readonly spaceId: string;
+ private readonly user: string;
private readonly config: ConfigType;
- private readonly dataClient: Pick<
- ScopedClusterClient,
- 'callAsCurrentUser' | 'callAsInternalUser'
- >;
- private readonly request: KibanaRequest;
- private readonly security: SecurityPluginSetup;
-
- constructor({ request, spaces, config, dataClient, security }: ConstructorOptions) {
- this.request = request;
- this.spaces = spaces;
+ private readonly callCluster: APICaller;
+
+ constructor({ spaceId, user, config, callCluster }: ConstructorOptions) {
+ this.spaceId = spaceId;
+ this.user = user;
this.config = config;
- this.dataClient = dataClient;
- this.security = security;
+ this.callCluster = callCluster;
}
public getListIndex = (): string => {
const {
- spaces,
- request,
+ spaceId,
config: { listIndex: listsIndexName },
} = this;
- return getListIndex({ listsIndexName, request, spaces });
+ return getListIndex({ listsIndexName, spaceId });
};
public getListItemIndex = (): string => {
const {
- spaces,
- request,
+ spaceId,
config: { listItemIndex: listsItemsIndexName },
} = this;
- return getListItemIndex({ listsItemsIndexName, request, spaces });
+ return getListItemIndex({ listsItemsIndexName, spaceId });
};
public getList = async ({ id }: GetListOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return getList({ dataClient, id, listIndex });
+ return getList({ callCluster, id, listIndex });
};
public createList = async ({
@@ -114,10 +103,9 @@ export class ListClient {
type,
meta,
}: CreateListOptions): Promise => {
- const { dataClient, security, request } = this;
+ const { callCluster, user } = this;
const listIndex = this.getListIndex();
- const user = getUser({ request, security });
- return createList({ dataClient, description, id, listIndex, meta, name, type, user });
+ return createList({ callCluster, description, id, listIndex, meta, name, type, user });
};
public createListIfItDoesNotExist = async ({
@@ -136,67 +124,51 @@ export class ListClient {
};
public getListIndexExists = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return getIndexExists(callAsCurrentUser, listIndex);
+ return getIndexExists(callCluster, listIndex);
};
public getListItemIndexExists = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return getIndexExists(callAsCurrentUser, listItemIndex);
+ return getIndexExists(callCluster, listItemIndex);
};
public createListBootStrapIndex = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return createBootstrapIndex(callAsCurrentUser, listIndex);
+ return createBootstrapIndex(callCluster, listIndex);
};
public createListItemBootStrapIndex = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return createBootstrapIndex(callAsCurrentUser, listItemIndex);
+ return createBootstrapIndex(callCluster, listItemIndex);
};
public getListPolicyExists = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return getPolicyExists(callAsCurrentUser, listIndex);
+ return getPolicyExists(callCluster, listIndex);
};
public getListItemPolicyExists = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listsItemIndex = this.getListItemIndex();
- return getPolicyExists(callAsCurrentUser, listsItemIndex);
+ return getPolicyExists(callCluster, listsItemIndex);
};
public getListTemplateExists = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return getTemplateExists(callAsCurrentUser, listIndex);
+ return getTemplateExists(callCluster, listIndex);
};
public getListItemTemplateExists = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return getTemplateExists(callAsCurrentUser, listItemIndex);
+ return getTemplateExists(callCluster, listItemIndex);
};
public getListTemplate = (): Record => {
@@ -210,91 +182,71 @@ export class ListClient {
};
public setListTemplate = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const template = this.getListTemplate();
const listIndex = this.getListIndex();
- return setTemplate(callAsCurrentUser, listIndex, template);
+ return setTemplate(callCluster, listIndex, template);
};
public setListItemTemplate = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const template = this.getListItemTemplate();
const listItemIndex = this.getListItemIndex();
- return setTemplate(callAsCurrentUser, listItemIndex, template);
+ return setTemplate(callCluster, listItemIndex, template);
};
public setListPolicy = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return setPolicy(callAsCurrentUser, listIndex, listPolicy);
+ return setPolicy(callCluster, listIndex, listPolicy);
};
public setListItemPolicy = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return setPolicy(callAsCurrentUser, listItemIndex, listsItemsPolicy);
+ return setPolicy(callCluster, listItemIndex, listsItemsPolicy);
};
public deleteListIndex = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return deleteAllIndex(callAsCurrentUser, `${listIndex}-*`);
+ return deleteAllIndex(callCluster, `${listIndex}-*`);
};
public deleteListItemIndex = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return deleteAllIndex(callAsCurrentUser, `${listItemIndex}-*`);
+ return deleteAllIndex(callCluster, `${listItemIndex}-*`);
};
public deleteListPolicy = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return deletePolicy(callAsCurrentUser, listIndex);
+ return deletePolicy(callCluster, listIndex);
};
public deleteListItemPolicy = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return deletePolicy(callAsCurrentUser, listItemIndex);
+ return deletePolicy(callCluster, listItemIndex);
};
public deleteListTemplate = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
- return deleteTemplate(callAsCurrentUser, listIndex);
+ return deleteTemplate(callCluster, listIndex);
};
public deleteListItemTemplate = async (): Promise => {
- const {
- dataClient: { callAsCurrentUser },
- } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return deleteTemplate(callAsCurrentUser, listItemIndex);
+ return deleteTemplate(callCluster, listItemIndex);
};
public deleteListItem = async ({ id }: DeleteListItemOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
- return deleteListItem({ dataClient, id, listItemIndex });
+ return deleteListItem({ callCluster, id, listItemIndex });
};
public deleteListItemByValue = async ({
@@ -302,10 +254,10 @@ export class ListClient {
value,
type,
}: DeleteListItemByValueOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
return deleteListItemByValue({
- dataClient,
+ callCluster,
listId,
listItemIndex,
type,
@@ -314,11 +266,11 @@ export class ListClient {
};
public deleteList = async ({ id }: DeleteListOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listIndex = this.getListIndex();
const listItemIndex = this.getListItemIndex();
return deleteList({
- dataClient,
+ callCluster,
id,
listIndex,
listItemIndex,
@@ -330,10 +282,10 @@ export class ListClient {
listId,
stream,
}: ExportListItemsToStreamOptions): void => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
exportListItemsToStream({
- dataClient,
+ callCluster,
listId,
listItemIndex,
stream,
@@ -347,11 +299,10 @@ export class ListClient {
stream,
meta,
}: ImportListItemsToStreamOptions): Promise => {
- const { dataClient, security, request } = this;
+ const { callCluster, user } = this;
const listItemIndex = this.getListItemIndex();
- const user = getUser({ request, security });
return importListItemsToStream({
- dataClient,
+ callCluster,
listId,
listItemIndex,
meta,
@@ -366,10 +317,10 @@ export class ListClient {
value,
type,
}: GetListItemByValueOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
return getListItemByValue({
- dataClient,
+ callCluster,
listId,
listItemIndex,
type,
@@ -384,11 +335,10 @@ export class ListClient {
type,
meta,
}: CreateListItemOptions): Promise => {
- const { dataClient, security, request } = this;
+ const { callCluster, user } = this;
const listItemIndex = this.getListItemIndex();
- const user = getUser({ request, security });
return createListItem({
- dataClient,
+ callCluster,
id,
listId,
listItemIndex,
@@ -404,11 +354,10 @@ export class ListClient {
value,
meta,
}: UpdateListItemOptions): Promise => {
- const { dataClient, security, request } = this;
- const user = getUser({ request, security });
+ const { callCluster, user } = this;
const listItemIndex = this.getListItemIndex();
return updateListItem({
- dataClient,
+ callCluster,
id,
listItemIndex,
meta,
@@ -423,11 +372,10 @@ export class ListClient {
description,
meta,
}: UpdateListOptions): Promise => {
- const { dataClient, security, request } = this;
- const user = getUser({ request, security });
+ const { callCluster, user } = this;
const listIndex = this.getListIndex();
return updateList({
- dataClient,
+ callCluster,
description,
id,
listIndex,
@@ -438,10 +386,10 @@ export class ListClient {
};
public getListItem = async ({ id }: GetListItemOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
return getListItem({
- dataClient,
+ callCluster,
id,
listItemIndex,
});
@@ -452,10 +400,10 @@ export class ListClient {
listId,
value,
}: GetListItemsByValueOptions): Promise => {
- const { dataClient } = this;
+ const { callCluster } = this;
const listItemIndex = this.getListItemIndex();
return getListItemByValues({
- dataClient,
+ callCluster,
listId,
listItemIndex,
type,
diff --git a/x-pack/plugins/lists/server/services/lists/client_types.ts b/x-pack/plugins/lists/server/services/lists/client_types.ts
index c3b6a484d87876..2cc58c02dbfcff 100644
--- a/x-pack/plugins/lists/server/services/lists/client_types.ts
+++ b/x-pack/plugins/lists/server/services/lists/client_types.ts
@@ -6,10 +6,9 @@
import { PassThrough, Readable } from 'stream';
-import { KibanaRequest } from 'kibana/server';
+import { APICaller, KibanaRequest } from 'kibana/server';
import { SecurityPluginSetup } from '../../../../security/server';
-import { SpacesServiceSetup } from '../../../../spaces/server';
import {
Description,
DescriptionOrUndefined,
@@ -21,14 +20,14 @@ import {
Type,
} from '../../../common/schemas';
import { ConfigType } from '../../config';
-import { DataClient } from '../../types';
export interface ConstructorOptions {
+ callCluster: APICaller;
config: ConfigType;
- dataClient: DataClient;
request: KibanaRequest;
- spaces: SpacesServiceSetup | undefined | null;
- security: SecurityPluginSetup;
+ spaceId: string;
+ user: string;
+ security: SecurityPluginSetup | undefined | null;
}
export interface GetListOptions {
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts
index d6ba435155c60f..36284a70fb97df 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts
@@ -31,7 +31,7 @@ describe('crete_list', () => {
expect(list).toEqual(expected);
});
- test('It calls "callAsCurrentUser" with body, index, and listIndex', async () => {
+ test('It calls "callCluster" with body, index, and listIndex', async () => {
const options = getCreateListOptionsMock();
await createList(options);
const body = getIndexESListMock();
@@ -40,7 +40,7 @@ describe('crete_list', () => {
id: LIST_ID,
index: LIST_INDEX,
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('index', expected);
+ expect(options.callCluster).toBeCalledWith('index', expected);
});
test('It returns an auto-generated id if id is sent in undefined', async () => {
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts
index dcf87b3ad1ef16..ddbc99c88a877a 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.ts
@@ -6,8 +6,8 @@
import uuid from 'uuid';
import { CreateDocumentResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
-import { DataClient } from '../../types';
import {
Description,
IdOrUndefined,
@@ -23,7 +23,7 @@ export interface CreateListOptions {
type: Type;
name: Name;
description: Description;
- dataClient: DataClient;
+ callCluster: APICaller;
listIndex: string;
user: string;
meta: MetaOrUndefined;
@@ -36,7 +36,7 @@ export const createList = async ({
name,
type,
description,
- dataClient,
+ callCluster,
listIndex,
user,
meta,
@@ -55,7 +55,7 @@ export const createList = async ({
updated_at: createdAt,
updated_by: user,
};
- const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('index', {
+ const response: CreateDocumentResponse = await callCluster('index', {
body,
id,
index: listIndex,
diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
index f32273e3e7f768..62b5e7c7aec4a3 100644
--- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
@@ -52,7 +52,7 @@ describe('delete_list', () => {
body: { query: { term: { list_id: LIST_ID } } },
index: LIST_ITEM_INDEX,
};
- expect(options.dataClient.callAsCurrentUser).toBeCalledWith('deleteByQuery', deleteByQuery);
+ expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery);
});
test('Delete calls "delete" second if a list is returned from getList', async () => {
@@ -64,13 +64,13 @@ describe('delete_list', () => {
id: LIST_ID,
index: LIST_INDEX,
};
- expect(options.dataClient.callAsCurrentUser).toHaveBeenNthCalledWith(2, 'delete', deleteQuery);
+ expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery);
});
test('Delete does not call data client if the list returns null', async () => {
((getList as unknown) as jest.Mock).mockResolvedValueOnce(null);
const options = getDeleteListOptionsMock();
await deleteList(options);
- expect(options.dataClient.callAsCurrentUser).not.toHaveBeenCalled();
+ expect(options.callCluster).not.toHaveBeenCalled();
});
});
diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts
index 653a8da74a105d..bc66c88b082a31 100644
--- a/x-pack/plugins/lists/server/services/lists/delete_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts
@@ -4,29 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { APICaller } from 'kibana/server';
+
import { Id, ListSchema } from '../../../common/schemas';
-import { DataClient } from '../../types';
import { getList } from './get_list';
export interface DeleteListOptions {
id: Id;
- dataClient: DataClient;
+ callCluster: APICaller;
listIndex: string;
listItemIndex: string;
}
export const deleteList = async ({
id,
- dataClient,
+ callCluster,
listIndex,
listItemIndex,
}: DeleteListOptions): Promise => {
- const list = await getList({ dataClient, id, listIndex });
+ const list = await getList({ callCluster, id, listIndex });
if (list == null) {
return null;
} else {
- await dataClient.callAsCurrentUser('deleteByQuery', {
+ await callCluster('deleteByQuery', {
body: {
query: {
term: {
@@ -37,7 +38,7 @@ export const deleteList = async ({
index: listItemIndex,
});
- await dataClient.callAsCurrentUser('delete', {
+ await callCluster('delete', {
id,
index: listIndex,
});
diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts
index 1f9a33c191764f..c997d5325296a6 100644
--- a/x-pack/plugins/lists/server/services/lists/get_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts
@@ -7,7 +7,7 @@
import {
LIST_ID,
LIST_INDEX,
- getDataClientMock,
+ getCallClusterMock,
getListResponseMock,
getSearchListMock,
} from '../mocks';
@@ -25,8 +25,8 @@ describe('get_list', () => {
test('it returns a list as expected if the list is found', async () => {
const data = getSearchListMock();
- const dataClient = getDataClientMock(data);
- const list = await getList({ dataClient, id: LIST_ID, listIndex: LIST_INDEX });
+ const callCluster = getCallClusterMock(data);
+ const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX });
const expected = getListResponseMock();
expect(list).toEqual(expected);
});
@@ -34,8 +34,8 @@ describe('get_list', () => {
test('it returns null if the search is empty', async () => {
const data = getSearchListMock();
data.hits.hits = [];
- const dataClient = getDataClientMock(data);
- const list = await getList({ dataClient, id: LIST_ID, listIndex: LIST_INDEX });
+ const callCluster = getCallClusterMock(data);
+ const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX });
expect(list).toEqual(null);
});
});
diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts
index 216703f08f0697..c04bd504ad8c01 100644
--- a/x-pack/plugins/lists/server/services/lists/get_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/get_list.ts
@@ -5,22 +5,22 @@
*/
import { SearchResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas';
-import { DataClient } from '../../types';
interface GetListOptions {
id: Id;
- dataClient: DataClient;
+ callCluster: APICaller;
listIndex: string;
}
export const getList = async ({
id,
- dataClient,
+ callCluster,
listIndex,
}: GetListOptions): Promise => {
- const result: SearchResponse = await dataClient.callAsCurrentUser('search', {
+ const result: SearchResponse = await callCluster('search', {
body: {
query: {
term: {
diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts
index 22a738a340b25d..f82928ffeddd2f 100644
--- a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts
@@ -4,17 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { httpServerMock } from 'src/core/server/mocks';
-import { KibanaRequest } from 'src/core/server';
-
-import { getSpace } from '../utils';
-
import { getListIndex } from './get_list_index';
-jest.mock('../utils', () => ({
- getSpace: jest.fn(),
-}));
-
describe('get_list_index', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -25,13 +16,9 @@ describe('get_list_index', () => {
});
test('Returns the list index when there is a space', async () => {
- ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space');
- const rawRequest = httpServerMock.createRawRequest({});
- const request = KibanaRequest.from(rawRequest);
const listIndex = getListIndex({
listsIndexName: 'lists-index',
- request,
- spaces: undefined,
+ spaceId: 'test-space',
});
expect(listIndex).toEqual('lists-index-test-space');
});
diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.ts
index 70b85fc97ebfa1..5086603fa84035 100644
--- a/x-pack/plugins/lists/server/services/lists/get_list_index.ts
+++ b/x-pack/plugins/lists/server/services/lists/get_list_index.ts
@@ -4,16 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { KibanaRequest } from 'kibana/server';
-
-import { SpacesServiceSetup } from '../../../../spaces/server';
-import { getSpace } from '../utils';
-
interface GetListIndexOptions {
- spaces: SpacesServiceSetup | undefined | null;
- request: KibanaRequest;
+ spaceId: string;
listsIndexName: string;
}
-export const getListIndex = ({ spaces, request, listsIndexName }: GetListIndexOptions): string =>
- `${listsIndexName}-${getSpace({ request, spaces })}`;
+export const getListIndex = ({ spaceId, listsIndexName }: GetListIndexOptions): string =>
+ `${listsIndexName}-${spaceId}`;
diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts
index 55f110e9a8291f..9859adf0624857 100644
--- a/x-pack/plugins/lists/server/services/lists/update_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/update_list.ts
@@ -5,6 +5,7 @@
*/
import { CreateDocumentResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import {
DescriptionOrUndefined,
@@ -14,13 +15,12 @@ import {
NameOrUndefined,
UpdateEsListSchema,
} from '../../../common/schemas';
-import { DataClient } from '../../types';
import { getList } from '.';
export interface UpdateListOptions {
id: Id;
- dataClient: DataClient;
+ callCluster: APICaller;
listIndex: string;
user: string;
name: NameOrUndefined;
@@ -33,14 +33,14 @@ export const updateList = async ({
id,
name,
description,
- dataClient,
+ callCluster,
listIndex,
user,
meta,
dateNow,
}: UpdateListOptions): Promise => {
const updatedAt = dateNow ?? new Date().toISOString();
- const list = await getList({ dataClient, id, listIndex });
+ const list = await getList({ callCluster, id, listIndex });
if (list == null) {
return null;
} else {
@@ -51,7 +51,7 @@ export const updateList = async ({
updated_at: updatedAt,
updated_by: user,
};
- const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('update', {
+ const response: CreateDocumentResponse = await callCluster('update', {
body: { doc },
id,
index: listIndex,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts
similarity index 57%
rename from x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts
rename to x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts
index 6e4cc40efeed73..180ecbb7973392 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts
@@ -5,15 +5,11 @@
*/
import { CreateDocumentResponse } from 'elasticsearch';
+import { APICaller } from 'kibana/server';
import { LIST_INDEX } from './lists_services_mock_constants';
import { getShardMock } from './get_shard_mock';
-interface DataClientReturn {
- callAsCurrentUser: () => Promise;
- callAsInternalUser: () => Promise;
-}
-
export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({
_id: 'elastic-id-123',
_index: LIST_INDEX,
@@ -24,11 +20,6 @@ export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse =>
result: '',
});
-export const getDataClientMock = (
- callAsCurrentUserData: unknown = getEmptyCreateDocumentResponseMock()
-): DataClientReturn => ({
- callAsCurrentUser: jest.fn().mockResolvedValue(callAsCurrentUserData),
- callAsInternalUser: (): Promise => {
- throw new Error('This function should not be calling "callAsInternalUser"');
- },
-});
+export const getCallClusterMock = (
+ callCluster: unknown = getEmptyCreateDocumentResponseMock()
+): APICaller => jest.fn().mockResolvedValue(callCluster);
diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts
index 0f4d92cabaa7a1..fcdad66d652518 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts
@@ -6,7 +6,7 @@
import { CreateListItemsBulkOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import {
DATE_NOW,
LIST_ID,
@@ -20,7 +20,7 @@ import {
} from './lists_services_mock_constants';
export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
dateNow: DATE_NOW,
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts
index 960db293f11245..17e3ad2f8de083 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts
@@ -6,7 +6,7 @@
import { CreateListItemOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import {
DATE_NOW,
LIST_ID,
@@ -19,7 +19,7 @@ import {
} from './lists_services_mock_constants';
export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
dateNow: DATE_NOW,
id: LIST_ITEM_ID,
listId: LIST_ID,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts
index 1a005a76547f5c..0ea6533fc122a8 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts
@@ -6,7 +6,7 @@
import { CreateListOptions } from '../lists';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import {
DATE_NOW,
DESCRIPTION,
@@ -20,7 +20,7 @@ import {
} from './lists_services_mock_constants';
export const getCreateListOptionsMock = (): CreateListOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
dateNow: DATE_NOW,
description: DESCRIPTION,
id: LIST_ID,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts
index 58fd319589ea3d..f6859e72d71b35 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts
@@ -6,11 +6,11 @@
import { DeleteListItemByValueOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants';
export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
type: TYPE,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts
index 1e7167547a6de9..271c185860b074 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts
@@ -6,11 +6,11 @@
import { DeleteListItemOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants';
export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
id: LIST_ITEM_ID,
listItemIndex: LIST_ITEM_INDEX,
});
diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts
index 9d70dae969362d..8ec92dfa4ef775 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts
@@ -6,11 +6,11 @@
import { DeleteListOptions } from '../lists';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants';
export const getDeleteListOptionsMock = (): DeleteListOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
id: LIST_ID,
listIndex: LIST_INDEX,
listItemIndex: LIST_ITEM_INDEX,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts
index 4cc6d85cd947ab..d7541f3e09e6cd 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts
@@ -5,12 +5,12 @@
*/
import { ImportListItemsToStreamOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants';
import { TestReadable } from './test_readable';
export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
meta: META,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts
index ab1bde48e7ebf9..96bc22ca7e6f27 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts
@@ -6,11 +6,11 @@
import { GetListItemByValueOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants';
export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
type: TYPE,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts
index c15d417d10289c..f21f97dc8d15f2 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts
@@ -6,11 +6,11 @@
import { GetListItemByValuesOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from './lists_services_mock_constants';
export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
type: TYPE,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts
index b60d6f5113e06b..0555997941baa0 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts
@@ -5,7 +5,7 @@
*/
import { UpdateListItemOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import {
DATE_NOW,
LIST_ITEM_ID,
@@ -16,7 +16,7 @@ import {
} from './lists_services_mock_constants';
export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
dateNow: DATE_NOW,
id: LIST_ITEM_ID,
listItemIndex: LIST_ITEM_INDEX,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts
index e56ebc24bdae1e..fe6fc37eaf81e6 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts
@@ -5,7 +5,7 @@
*/
import { UpdateListOptions } from '../lists';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import {
DATE_NOW,
DESCRIPTION,
@@ -17,7 +17,7 @@ import {
} from './lists_services_mock_constants';
export const getUpdateListOptionsMock = (): UpdateListOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
dateNow: DATE_NOW,
description: DESCRIPTION,
id: LIST_ID,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts
index 9a77453b65d6aa..d6b7d70c1aa778 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts
@@ -5,12 +5,12 @@
*/
import { WriteBufferToItemsOptions } from '../items';
-import { getDataClientMock } from './get_data_client_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants';
export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({
buffer: [],
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
meta: META,
diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts
index 96724c2a88045c..c945818a83e8a8 100644
--- a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts
+++ b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts
@@ -13,12 +13,12 @@ import {
WriteResponseHitsToStreamOptions,
} from '../items';
-import { getDataClientMock } from './get_data_client_mock';
import { LIST_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants';
import { getSearchListItemMock } from './get_search_list_item_mock';
+import { getCallClusterMock } from './get_call_cluster_mock';
export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({
- dataClient: getDataClientMock(getSearchListItemMock()),
+ callCluster: getCallClusterMock(getSearchListItemMock()),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
stream: new Stream.PassThrough(),
@@ -26,7 +26,7 @@ export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStream
});
export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({
- dataClient: getDataClientMock(getSearchListItemMock()),
+ callCluster: getCallClusterMock(getSearchListItemMock()),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
searchAfter: [],
@@ -35,7 +35,7 @@ export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({
});
export const getResponseOptionsMock = (): GetResponseOptions => ({
- dataClient: getDataClientMock(),
+ callCluster: getCallClusterMock(),
listId: LIST_ID,
listItemIndex: LIST_ITEM_INDEX,
searchAfter: [],
diff --git a/x-pack/plugins/lists/server/services/mocks/index.ts b/x-pack/plugins/lists/server/services/mocks/index.ts
index 516264149fac70..c555ba322fa2bb 100644
--- a/x-pack/plugins/lists/server/services/mocks/index.ts
+++ b/x-pack/plugins/lists/server/services/mocks/index.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './get_data_client_mock';
+export * from './get_call_cluster_mock';
export * from './get_delete_list_options_mock';
export * from './get_create_list_options_mock';
export * from './get_list_response_mock';
@@ -27,3 +27,5 @@ export * from './get_update_list_item_options_mock';
export * from './get_write_buffer_to_items_options_mock';
export * from './get_import_list_items_to_stream_options_mock';
export * from './get_write_list_items_to_stream_options_mock';
+export * from './get_search_list_item_mock';
+export * from './test_readable';
diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts
index 6e5dca7d54e5b8..3b6f58479a2f22 100644
--- a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts
+++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts
@@ -10,6 +10,14 @@ import { Type } from '../../../common/schemas';
import { deriveTypeFromItem } from './derive_type_from_es_type';
describe('derive_type_from_es_type', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
test('it returns the item ip if it exists', () => {
const item = getSearchEsListItemMock();
const derivedType = deriveTypeFromItem({ item });
diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts
new file mode 100644
index 00000000000000..3d48e44e26eaa3
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { QueryFilterType, getQueryFilterFromTypeValue } from './get_query_filter_from_type_value';
+
+describe('get_query_filter_from_type_value', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it returns an ip if given an ip', () => {
+ const queryFilter = getQueryFilterFromTypeValue({
+ listId: 'list-123',
+ type: 'ip',
+ value: ['127.0.0.1'],
+ });
+ const expected: QueryFilterType = [
+ { term: { list_id: 'list-123' } },
+ { terms: { ip: ['127.0.0.1'] } },
+ ];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it returns two ip if given two ip', () => {
+ const queryFilter = getQueryFilterFromTypeValue({
+ listId: 'list-123',
+ type: 'ip',
+ value: ['127.0.0.1', '127.0.0.2'],
+ });
+ const expected: QueryFilterType = [
+ { term: { list_id: 'list-123' } },
+ { terms: { ip: ['127.0.0.1', '127.0.0.2'] } },
+ ];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it returns a keyword if given a keyword', () => {
+ const queryFilter = getQueryFilterFromTypeValue({
+ listId: 'list-123',
+ type: 'keyword',
+ value: ['host-name-1'],
+ });
+ const expected: QueryFilterType = [
+ { term: { list_id: 'list-123' } },
+ { terms: { keyword: ['host-name-1'] } },
+ ];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it returns two keywords if given two values', () => {
+ const queryFilter = getQueryFilterFromTypeValue({
+ listId: 'list-123',
+ type: 'keyword',
+ value: ['host-name-1', 'host-name-2'],
+ });
+ const expected: QueryFilterType = [
+ { term: { list_id: 'list-123' } },
+ { terms: { keyword: ['host-name-1', 'host-name-2'] } },
+ ];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it returns an empty keyword given an empty value', () => {
+ const queryFilter = getQueryFilterFromTypeValue({
+ listId: 'list-123',
+ type: 'keyword',
+ value: [],
+ });
+ const expected: QueryFilterType = [
+ { term: { list_id: 'list-123' } },
+ { terms: { keyword: [] } },
+ ];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it returns an empty ip given an empty value', () => {
+ const queryFilter = getQueryFilterFromTypeValue({
+ listId: 'list-123',
+ type: 'ip',
+ value: [],
+ });
+ const expected: QueryFilterType = [{ term: { list_id: 'list-123' } }, { terms: { ip: [] } }];
+ expect(queryFilter).toEqual(expected);
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts
index f256b6cb8f2d50..8a44b5ab607bf5 100644
--- a/x-pack/plugins/lists/server/services/utils/index.ts
+++ b/x-pack/plugins/lists/server/services/utils/index.ts
@@ -7,6 +7,4 @@
export * from './get_query_filter_from_type_value';
export * from './transform_elastic_to_list_item';
export * from './transform_list_item_to_elastic_query';
-export * from './get_user';
export * from './derive_type_from_es_type';
-export * from './get_space';
diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts
new file mode 100644
index 00000000000000..3b9864be6df538
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ListItemArraySchema } from '../../../common/schemas';
+import { getListItemResponseMock, getSearchListItemMock } from '../mocks';
+
+import { transformElasticToListItem } from './transform_elastic_to_list_item';
+
+describe('transform_elastic_to_list_item', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it transforms an elastic type to a list item type', () => {
+ const response = getSearchListItemMock();
+ const queryFilter = transformElasticToListItem({
+ response,
+ type: 'ip',
+ });
+ const expected: ListItemArraySchema = [getListItemResponseMock()];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it transforms an elastic keyword type to a list item type', () => {
+ const response = getSearchListItemMock();
+ response.hits.hits[0]._source.ip = undefined;
+ response.hits.hits[0]._source.keyword = 'host-name-example';
+ const queryFilter = transformElasticToListItem({
+ response,
+ type: 'keyword',
+ });
+ const listItemResponse = getListItemResponseMock();
+ listItemResponse.type = 'keyword';
+ listItemResponse.value = 'host-name-example';
+ const expected: ListItemArraySchema = [listItemResponse];
+ expect(queryFilter).toEqual(expected);
+ });
+
+ test('it does a throw if it cannot determine the list item type from "ip"', () => {
+ const response = getSearchListItemMock();
+ response.hits.hits[0]._source.ip = undefined;
+ response.hits.hits[0]._source.keyword = 'host-name-example';
+ expect(() =>
+ transformElasticToListItem({
+ response,
+ type: 'ip',
+ })
+ ).toThrow('Was expecting ip to not be null/undefined');
+ });
+
+ test('it does a throw if it cannot determine the list item type from "keyword"', () => {
+ const response = getSearchListItemMock();
+ response.hits.hits[0]._source.ip = '127.0.0.1';
+ response.hits.hits[0]._source.keyword = undefined;
+ expect(() =>
+ transformElasticToListItem({
+ response,
+ type: 'keyword',
+ })
+ ).toThrow('Was expecting keyword to not be null/undefined');
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts
index 9cf673081d3206..2dc0f4fe7a8216 100644
--- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts
+++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts
@@ -9,13 +9,15 @@ import { SearchResponse } from 'elasticsearch';
import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas';
import { ErrorWithStatusCode } from '../../error_with_status_code';
+export interface TransformElasticToListItemOptions {
+ response: SearchResponse;
+ type: Type;
+}
+
export const transformElasticToListItem = ({
response,
type,
-}: {
- response: SearchResponse;
- type: Type;
-}): ListItemArraySchema => {
+}: TransformElasticToListItemOptions): ListItemArraySchema => {
return response.hits.hits.map(hit => {
const {
_id,
@@ -64,11 +66,10 @@ export const transformElasticToListItem = ({
};
}
}
- // TypeScript is not happy unless I have this line here
return assertUnreachable();
});
};
-export const assertUnreachable = (): never => {
+const assertUnreachable = (): never => {
throw new Error('Unknown type in elastic_to_list_items');
};
diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts
new file mode 100644
index 00000000000000..217cad30bfdbbd
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EsDataTypeUnion, Type } from '../../../common/schemas';
+
+import { transformListItemToElasticQuery } from './transform_list_item_to_elastic_query';
+
+describe('transform_elastic_to_elastic_query', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it transforms a ip type and value to a union', () => {
+ const elasticQuery = transformListItemToElasticQuery({
+ type: 'ip',
+ value: '127.0.0.1',
+ });
+ const expected: EsDataTypeUnion = { ip: '127.0.0.1' };
+ expect(elasticQuery).toEqual(expected);
+ });
+
+ test('it transforms a keyword type and value to a union', () => {
+ const elasticQuery = transformListItemToElasticQuery({
+ type: 'keyword',
+ value: 'host-name',
+ });
+ const expected: EsDataTypeUnion = { keyword: 'host-name' };
+ expect(elasticQuery).toEqual(expected);
+ });
+
+ test('it throws if the type is not known', () => {
+ const type: Type = 'made-up' as Type;
+ expect(() =>
+ transformListItemToElasticQuery({
+ type,
+ value: 'some-value',
+ })
+ ).toThrow('Unknown type: "made-up" in transformListItemToElasticQuery');
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts
index e68851dc3582b7..051802cc41b5ba 100644
--- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts
+++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts
@@ -12,8 +12,6 @@ export const transformListItemToElasticQuery = ({
}: {
type: Type;
value: string;
- // We disable the consistent return since we want to use typescript for exhaustive type checks
- // eslint-disable-next-line consistent-return
}): EsDataTypeUnion => {
switch (type) {
case 'ip': {
@@ -27,4 +25,9 @@ export const transformListItemToElasticQuery = ({
};
}
}
+ return assertUnreachable(type);
+};
+
+const assertUnreachable = (type: string): never => {
+ throw new Error(`Unknown type: "${type}" in transformListItemToElasticQuery`);
};
diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts
index 7d509c4e271675..e0e4495d47c341 100644
--- a/x-pack/plugins/lists/server/types.ts
+++ b/x-pack/plugins/lists/server/types.ts
@@ -4,18 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IContextProvider, RequestHandler, ScopedClusterClient } from 'kibana/server';
+import { IContextProvider, RequestHandler } from 'kibana/server';
import { SecurityPluginSetup } from '../../security/server';
import { SpacesPluginSetup } from '../../spaces/server';
import { ListClient } from './services/lists/client';
-export type DataClient = Pick;
export type ContextProvider = IContextProvider, 'lists'>;
export interface PluginsSetup {
- security: SecurityPluginSetup;
+ security: SecurityPluginSetup | undefined | null;
spaces: SpacesPluginSetup | undefined | null;
}
diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts
index 633e8c86d8c94f..7715541b1c52d7 100644
--- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts
+++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts
@@ -20,6 +20,7 @@ export type RenderWizardArguments = {
};
export type LayerWizard = {
+ checkVisibility?: () => boolean;
description: string;
icon: string;
isIndexingSource?: boolean;
@@ -34,5 +35,7 @@ export function registerLayerWizard(layerWizard: LayerWizard) {
}
export function getLayerWizards(): LayerWizard[] {
- return [...registry];
+ return registry.filter(layerWizard => {
+ return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true;
+ });
}
diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
index f31e770df2d958..a6e2e7f42657c3 100644
--- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
@@ -12,8 +12,13 @@ import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'
import { EMSFileCreateSourceEditor } from './create_source_editor';
// @ts-ignore
import { EMSFileSource, sourceTitle } from './ems_file_source';
+// @ts-ignore
+import { isEmsEnabled } from '../../../meta';
export const emsBoundariesLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ return isEmsEnabled();
+ },
description: i18n.translate('xpack.maps.source.emsFileDescription', {
defaultMessage: 'Administrative boundaries from Elastic Maps Service',
}),
diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
index ced33a0bcf84a9..fc745edbabee87 100644
--- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
@@ -12,8 +12,13 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source';
import { VectorTileLayer } from '../../vector_tile_layer';
// @ts-ignore
import { TileServiceSelect } from './tile_service_select';
+// @ts-ignore
+import { isEmsEnabled } from '../../../meta';
export const emsBaseMapLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ return isEmsEnabled();
+ },
description: i18n.translate('xpack.maps.source.emsTileDescription', {
defaultMessage: 'Tile map service from Elastic Maps Service',
}),
diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
index 4321501760fafd..a9adec2bda2c81 100644
--- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
@@ -12,8 +12,14 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source';
import { VectorLayer } from '../../vector_layer';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
+// @ts-ignore
+import { getKibanaRegionList } from '../../../meta';
export const kibanaRegionMapLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ const regions = getKibanaRegionList();
+ return regions.length;
+ },
description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', {
defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml',
}),
diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
index aeea2d6084f847..141fabeedd3e51 100644
--- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
@@ -12,8 +12,14 @@ import { CreateSourceEditor } from './create_source_editor';
// @ts-ignore
import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source';
import { TileLayer } from '../../tile_layer';
+// @ts-ignore
+import { getKibanaTileMap } from '../../../meta';
export const kibanaBasemapLayerWizardConfig: LayerWizard = {
+ checkVisibility: () => {
+ const tilemap = getKibanaTileMap();
+ return !!tilemap.url;
+ },
description: i18n.translate('xpack.maps.source.kbnTMSDescription', {
defaultMessage: 'Tile map service configured in kibana.yml',
}),
diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js
index d4612554cf00b9..c3245e8e98db2e 100644
--- a/x-pack/plugins/maps/public/meta.js
+++ b/x-pack/plugins/maps/public/meta.js
@@ -36,12 +36,15 @@ function fetchFunction(...args) {
return fetch(...args);
}
+export function isEmsEnabled() {
+ return getInjectedVarFunc()('isEmsEnabled', true);
+}
+
let emsClient = null;
let latestLicenseId = null;
export function getEMSClient() {
if (!emsClient) {
- const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true);
- if (isEmsEnabled) {
+ if (isEmsEnabled()) {
const proxyElasticMapsServiceInMaps = getInjectedVarFunc()(
'proxyElasticMapsServiceInMaps',
false
@@ -86,7 +89,7 @@ export function getEMSClient() {
}
export function getGlyphUrl() {
- if (!getInjectedVarFunc()('isEmsEnabled', true)) {
+ if (!isEmsEnabled()) {
return '';
}
return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false)
diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts
index 2a449c95faa5b5..572217ce16eee2 100644
--- a/x-pack/plugins/ml/common/types/capabilities.ts
+++ b/x-pack/plugins/ml/common/types/capabilities.ts
@@ -7,6 +7,7 @@
import { KibanaRequest } from 'kibana/server';
export const userMlCapabilities = {
+ canAccessML: false,
// Anomaly Detection
canGetJobs: false,
canGetDatafeeds: false,
@@ -14,10 +15,12 @@ export const userMlCapabilities = {
canGetCalendars: false,
// File Data Visualizer
canFindFileStructure: false,
- // Filters
- canGetFilters: false,
// Data Frame Analytics
canGetDataFrameAnalytics: false,
+ // Annotations
+ canGetAnnotations: false,
+ canCreateAnnotation: false,
+ canDeleteAnnotation: false,
};
export const adminMlCapabilities = {
@@ -26,11 +29,15 @@ export const adminMlCapabilities = {
canDeleteJob: false,
canOpenJob: false,
canCloseJob: false,
+ canUpdateJob: false,
canForecastJob: false,
+ canCreateDatafeed: false,
+ canDeleteDatafeed: false,
canStartStopDatafeed: false,
- canUpdateJob: false,
canUpdateDatafeed: false,
canPreviewDatafeed: false,
+ // Filters
+ canGetFilters: false,
// Calendars
canCreateCalendar: false,
canDeleteCalendar: false,
@@ -38,8 +45,8 @@ export const adminMlCapabilities = {
canCreateFilter: false,
canDeleteFilter: false,
// Data Frame Analytics
- canDeleteDataFrameAnalytics: false,
canCreateDataFrameAnalytics: false,
+ canDeleteDataFrameAnalytics: false,
canStartStopDataFrameAnalytics: false,
};
@@ -47,7 +54,9 @@ export type UserMlCapabilities = typeof userMlCapabilities;
export type AdminMlCapabilities = typeof adminMlCapabilities;
export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities;
-export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array;
+export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array<
+ keyof MlCapabilities
+>;
export function getDefaultCapabilities(): MlCapabilities {
return {
@@ -56,6 +65,23 @@ export function getDefaultCapabilities(): MlCapabilities {
};
}
+export function getPluginPrivileges() {
+ const userMlCapabilitiesKeys = Object.keys(userMlCapabilities);
+ const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities);
+ const allMlCapabilities = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys];
+
+ return {
+ user: {
+ ui: userMlCapabilitiesKeys,
+ api: userMlCapabilitiesKeys.map(k => `ml:${k}`),
+ },
+ admin: {
+ ui: allMlCapabilities,
+ api: allMlCapabilities.map(k => `ml:${k}`),
+ },
+ };
+}
+
export interface MlCapabilitiesResponse {
capabilities: MlCapabilities;
upgradeInProgress: boolean;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
index 8c65af1d92959f..cc75ddbe08cfb9 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx
@@ -18,6 +18,7 @@ import {
} from '../../hooks/use_create_analytics_form';
import { State } from '../../hooks/use_create_analytics_form/state';
import { DataFrameAnalyticsListRow } from './common';
+import { checkPermission } from '../../../../../capabilities/check_capabilities';
interface PropDefinition {
/**
@@ -322,6 +323,8 @@ interface CloneActionProps {
* to support EuiContext with a valid DOM structure without nested buttons.
*/
export const CloneAction: FC = ({ createAnalyticsForm, item }) => {
+ const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics');
+
const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', {
defaultMessage: 'Clone job',
});
@@ -338,6 +341,7 @@ export const CloneAction: FC = ({ createAnalyticsForm, item })
iconType="copy"
onClick={onClick}
aria-label={buttonText}
+ disabled={canCreateDataFrameAnalytics === false}
>
{buttonText}
diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts
index 5093801d2d1846..746c9da47d0adb 100644
--- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts
+++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts
@@ -36,7 +36,7 @@ describe('check_capabilities', () => {
);
const { capabilities } = await getCapabilities();
const count = Object.keys(capabilities).length;
- expect(count).toBe(22);
+ expect(count).toBe(28);
done();
});
});
@@ -49,28 +49,42 @@ describe('check_capabilities', () => {
mlLicense,
mlIsEnabled
);
- const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities();
+ const {
+ capabilities,
+ upgradeInProgress,
+ mlFeatureEnabledInSpace,
+ isPlatinumOrTrialLicense,
+ } = await getCapabilities();
expect(upgradeInProgress).toBe(false);
expect(mlFeatureEnabledInSpace).toBe(true);
+ expect(isPlatinumOrTrialLicense).toBe(true);
+
+ expect(capabilities.canAccessML).toBe(true);
expect(capabilities.canGetJobs).toBe(true);
+ expect(capabilities.canGetDatafeeds).toBe(true);
+ expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canFindFileStructure).toBe(true);
+ expect(capabilities.canGetDataFrameAnalytics).toBe(true);
+ expect(capabilities.canGetAnnotations).toBe(true);
+ expect(capabilities.canCreateAnnotation).toBe(true);
+ expect(capabilities.canDeleteAnnotation).toBe(true);
+
expect(capabilities.canCreateJob).toBe(false);
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
- expect(capabilities.canGetDatafeeds).toBe(true);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
+ expect(capabilities.canCreateDatafeed).toBe(false);
+ expect(capabilities.canDeleteDatafeed).toBe(false);
expect(capabilities.canUpdateDatafeed).toBe(false);
expect(capabilities.canPreviewDatafeed).toBe(false);
- expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canCreateCalendar).toBe(false);
expect(capabilities.canDeleteCalendar).toBe(false);
- expect(capabilities.canGetFilters).toBe(true);
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
- expect(capabilities.canFindFileStructure).toBe(true);
- expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
@@ -84,28 +98,42 @@ describe('check_capabilities', () => {
mlLicense,
mlIsEnabled
);
- const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities();
+ const {
+ capabilities,
+ upgradeInProgress,
+ mlFeatureEnabledInSpace,
+ isPlatinumOrTrialLicense,
+ } = await getCapabilities();
expect(upgradeInProgress).toBe(false);
expect(mlFeatureEnabledInSpace).toBe(true);
+ expect(isPlatinumOrTrialLicense).toBe(true);
+
+ expect(capabilities.canAccessML).toBe(true);
expect(capabilities.canGetJobs).toBe(true);
+ expect(capabilities.canGetDatafeeds).toBe(true);
+ expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canFindFileStructure).toBe(true);
+ expect(capabilities.canGetDataFrameAnalytics).toBe(true);
+ expect(capabilities.canGetAnnotations).toBe(true);
+ expect(capabilities.canCreateAnnotation).toBe(true);
+ expect(capabilities.canDeleteAnnotation).toBe(true);
+
expect(capabilities.canCreateJob).toBe(true);
expect(capabilities.canDeleteJob).toBe(true);
expect(capabilities.canOpenJob).toBe(true);
expect(capabilities.canCloseJob).toBe(true);
expect(capabilities.canForecastJob).toBe(true);
- expect(capabilities.canGetDatafeeds).toBe(true);
expect(capabilities.canStartStopDatafeed).toBe(true);
expect(capabilities.canUpdateJob).toBe(true);
+ expect(capabilities.canCreateDatafeed).toBe(true);
+ expect(capabilities.canDeleteDatafeed).toBe(true);
expect(capabilities.canUpdateDatafeed).toBe(true);
expect(capabilities.canPreviewDatafeed).toBe(true);
- expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canGetFilters).toBe(true);
expect(capabilities.canCreateCalendar).toBe(true);
expect(capabilities.canDeleteCalendar).toBe(true);
- expect(capabilities.canGetFilters).toBe(true);
expect(capabilities.canCreateFilter).toBe(true);
expect(capabilities.canDeleteFilter).toBe(true);
- expect(capabilities.canFindFileStructure).toBe(true);
- expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(true);
expect(capabilities.canCreateDataFrameAnalytics).toBe(true);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(true);
@@ -119,28 +147,42 @@ describe('check_capabilities', () => {
mlLicense,
mlIsEnabled
);
- const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities();
+ const {
+ capabilities,
+ upgradeInProgress,
+ mlFeatureEnabledInSpace,
+ isPlatinumOrTrialLicense,
+ } = await getCapabilities();
expect(upgradeInProgress).toBe(true);
expect(mlFeatureEnabledInSpace).toBe(true);
+ expect(isPlatinumOrTrialLicense).toBe(true);
+
+ expect(capabilities.canAccessML).toBe(true);
expect(capabilities.canGetJobs).toBe(true);
+ expect(capabilities.canGetDatafeeds).toBe(true);
+ expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canFindFileStructure).toBe(true);
+ expect(capabilities.canGetDataFrameAnalytics).toBe(true);
+ expect(capabilities.canGetAnnotations).toBe(true);
+ expect(capabilities.canCreateAnnotation).toBe(false);
+ expect(capabilities.canDeleteAnnotation).toBe(false);
+
expect(capabilities.canCreateJob).toBe(false);
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
- expect(capabilities.canGetDatafeeds).toBe(true);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
+ expect(capabilities.canCreateDatafeed).toBe(false);
+ expect(capabilities.canDeleteDatafeed).toBe(false);
+ expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canUpdateDatafeed).toBe(false);
expect(capabilities.canPreviewDatafeed).toBe(false);
- expect(capabilities.canGetCalendars).toBe(true);
expect(capabilities.canCreateCalendar).toBe(false);
expect(capabilities.canDeleteCalendar).toBe(false);
- expect(capabilities.canGetFilters).toBe(true);
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
- expect(capabilities.canFindFileStructure).toBe(true);
- expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
@@ -154,28 +196,42 @@ describe('check_capabilities', () => {
mlLicense,
mlIsEnabled
);
- const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities();
+ const {
+ capabilities,
+ upgradeInProgress,
+ mlFeatureEnabledInSpace,
+ isPlatinumOrTrialLicense,
+ } = await getCapabilities();
expect(upgradeInProgress).toBe(true);
expect(mlFeatureEnabledInSpace).toBe(true);
+ expect(isPlatinumOrTrialLicense).toBe(true);
+
+ expect(capabilities.canAccessML).toBe(true);
expect(capabilities.canGetJobs).toBe(true);
+ expect(capabilities.canGetDatafeeds).toBe(true);
+ expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canFindFileStructure).toBe(true);
+ expect(capabilities.canGetDataFrameAnalytics).toBe(true);
+ expect(capabilities.canGetAnnotations).toBe(true);
+ expect(capabilities.canCreateAnnotation).toBe(false);
+ expect(capabilities.canDeleteAnnotation).toBe(false);
+
expect(capabilities.canCreateJob).toBe(false);
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
- expect(capabilities.canGetDatafeeds).toBe(true);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
+ expect(capabilities.canCreateDatafeed).toBe(false);
+ expect(capabilities.canDeleteDatafeed).toBe(false);
expect(capabilities.canUpdateDatafeed).toBe(false);
expect(capabilities.canPreviewDatafeed).toBe(false);
- expect(capabilities.canGetCalendars).toBe(true);
+ expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canCreateCalendar).toBe(false);
expect(capabilities.canDeleteCalendar).toBe(false);
- expect(capabilities.canGetFilters).toBe(true);
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
- expect(capabilities.canFindFileStructure).toBe(true);
- expect(capabilities.canGetDataFrameAnalytics).toBe(true);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
@@ -189,28 +245,42 @@ describe('check_capabilities', () => {
mlLicense,
mlIsNotEnabled
);
- const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities();
+ const {
+ capabilities,
+ upgradeInProgress,
+ mlFeatureEnabledInSpace,
+ isPlatinumOrTrialLicense,
+ } = await getCapabilities();
expect(upgradeInProgress).toBe(false);
expect(mlFeatureEnabledInSpace).toBe(false);
+ expect(isPlatinumOrTrialLicense).toBe(true);
+
+ expect(capabilities.canAccessML).toBe(false);
expect(capabilities.canGetJobs).toBe(false);
+ expect(capabilities.canGetDatafeeds).toBe(false);
+ expect(capabilities.canGetCalendars).toBe(false);
+ expect(capabilities.canFindFileStructure).toBe(false);
+ expect(capabilities.canGetDataFrameAnalytics).toBe(false);
+ expect(capabilities.canGetAnnotations).toBe(false);
+ expect(capabilities.canCreateAnnotation).toBe(false);
+ expect(capabilities.canDeleteAnnotation).toBe(false);
+
expect(capabilities.canCreateJob).toBe(false);
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
- expect(capabilities.canGetDatafeeds).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
+ expect(capabilities.canCreateDatafeed).toBe(false);
+ expect(capabilities.canDeleteDatafeed).toBe(false);
expect(capabilities.canUpdateDatafeed).toBe(false);
expect(capabilities.canPreviewDatafeed).toBe(false);
- expect(capabilities.canGetCalendars).toBe(false);
+ expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canCreateCalendar).toBe(false);
expect(capabilities.canDeleteCalendar).toBe(false);
- expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
- expect(capabilities.canFindFileStructure).toBe(false);
- expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
@@ -225,28 +295,43 @@ describe('check_capabilities', () => {
mlLicenseBasic,
mlIsNotEnabled
);
- const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities();
+ const {
+ capabilities,
+ upgradeInProgress,
+ mlFeatureEnabledInSpace,
+ isPlatinumOrTrialLicense,
+ } = await getCapabilities();
+
expect(upgradeInProgress).toBe(false);
expect(mlFeatureEnabledInSpace).toBe(false);
+ expect(isPlatinumOrTrialLicense).toBe(false);
+
+ expect(capabilities.canAccessML).toBe(false);
expect(capabilities.canGetJobs).toBe(false);
+ expect(capabilities.canGetDatafeeds).toBe(false);
+ expect(capabilities.canGetCalendars).toBe(false);
+ expect(capabilities.canFindFileStructure).toBe(false);
+ expect(capabilities.canGetDataFrameAnalytics).toBe(false);
+ expect(capabilities.canGetAnnotations).toBe(false);
+ expect(capabilities.canCreateAnnotation).toBe(false);
+ expect(capabilities.canDeleteAnnotation).toBe(false);
+
expect(capabilities.canCreateJob).toBe(false);
expect(capabilities.canDeleteJob).toBe(false);
expect(capabilities.canOpenJob).toBe(false);
expect(capabilities.canCloseJob).toBe(false);
expect(capabilities.canForecastJob).toBe(false);
- expect(capabilities.canGetDatafeeds).toBe(false);
expect(capabilities.canStartStopDatafeed).toBe(false);
expect(capabilities.canUpdateJob).toBe(false);
+ expect(capabilities.canCreateDatafeed).toBe(false);
+ expect(capabilities.canDeleteDatafeed).toBe(false);
expect(capabilities.canUpdateDatafeed).toBe(false);
expect(capabilities.canPreviewDatafeed).toBe(false);
- expect(capabilities.canGetCalendars).toBe(false);
+ expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canCreateCalendar).toBe(false);
expect(capabilities.canDeleteCalendar).toBe(false);
- expect(capabilities.canGetFilters).toBe(false);
expect(capabilities.canCreateFilter).toBe(false);
expect(capabilities.canDeleteFilter).toBe(false);
- expect(capabilities.canFindFileStructure).toBe(false);
- expect(capabilities.canGetDataFrameAnalytics).toBe(false);
expect(capabilities.canDeleteDataFrameAnalytics).toBe(false);
expect(capabilities.canCreateDataFrameAnalytics).toBe(false);
expect(capabilities.canStartStopDataFrameAnalytics).toBe(false);
diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts
index a2ad83c5522de3..d955cf981faca6 100644
--- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts
+++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts
@@ -44,4 +44,6 @@ function disableAdminPrivileges(capabilities: MlCapabilities) {
Object.keys(adminMlCapabilities).forEach(k => {
capabilities[k as keyof MlCapabilities] = false;
});
+ capabilities.canCreateAnnotation = false;
+ capabilities.canDeleteAnnotation = false;
}
diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts
index 64f8eb4b0acd3a..969b74194148b3 100644
--- a/x-pack/plugins/ml/server/plugin.ts
+++ b/x-pack/plugins/ml/server/plugin.ts
@@ -45,7 +45,7 @@ import { systemRoutes } from './routes/system';
import { MlLicense } from '../common/license';
import { MlServerLicense } from './lib/license';
import { createSharedServices, SharedServices } from './shared_services';
-import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities';
+import { getPluginPrivileges } from '../common/types/capabilities';
import { setupCapabilitiesSwitcher } from './lib/capabilities';
import { registerKibanaSettings } from './lib/register_settings';
@@ -75,8 +75,7 @@ export class MlServerPlugin implements Plugin {
try {
@@ -86,6 +89,9 @@ export function annotationRoutes(
validate: {
body: indexAnnotationSchema,
},
+ options: {
+ tags: ['access:ml:canCreateAnnotation'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -130,6 +136,9 @@ export function annotationRoutes(
validate: {
params: deleteAnnotationSchema,
},
+ options: {
+ tags: ['access:ml:canDeleteAnnotation'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
index ca63d69f403f6b..63cd5498231af5 100644
--- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
+++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts
@@ -37,6 +37,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/anomaly_detectors',
validate: false,
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -65,6 +68,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: jobIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -93,6 +99,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/anomaly_detectors/_stats',
validate: false,
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -121,6 +130,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: jobIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -154,6 +166,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
params: jobIdSchema,
body: schema.object(anomalyDetectionJobSchema),
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -188,6 +203,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
params: jobIdSchema,
body: anomalyDetectionUpdateJobSchema,
},
+ options: {
+ tags: ['access:ml:canUpdateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -220,6 +238,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: jobIdSchema,
},
+ options: {
+ tags: ['access:ml:canOpenJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -251,6 +272,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: jobIdSchema,
},
+ options: {
+ tags: ['access:ml:canCloseJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -286,6 +310,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: jobIdSchema,
},
+ options: {
+ tags: ['access:ml:canDeleteJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -319,6 +346,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
body: schema.any(),
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -351,6 +381,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
params: jobIdSchema,
body: forecastAnomalyDetector,
},
+ options: {
+ tags: ['access:ml:canForecastJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -389,6 +422,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
params: jobIdSchema,
body: getRecordsSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -425,6 +461,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
params: getBucketParamsSchema,
body: getBucketsSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -462,6 +501,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
params: jobIdSchema,
body: getOverallBucketsSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -496,6 +538,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: getCategoriesSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts
index a17601f74ae93e..9c80651a13999e 100644
--- a/x-pack/plugins/ml/server/routes/calendars.ts
+++ b/x-pack/plugins/ml/server/routes/calendars.ts
@@ -52,6 +52,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/calendars',
validate: false,
+ options: {
+ tags: ['access:ml:canGetCalendars'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -81,6 +84,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) {
validate: {
params: calendarIdsSchema,
},
+ options: {
+ tags: ['access:ml:canGetCalendars'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
let returnValue;
@@ -117,6 +123,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) {
validate: {
body: calendarSchema,
},
+ options: {
+ tags: ['access:ml:canCreateCalendar'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -149,6 +158,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) {
params: calendarIdSchema,
body: calendarSchema,
},
+ options: {
+ tags: ['access:ml:canCreateCalendar'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -180,6 +192,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) {
validate: {
params: calendarIdSchema,
},
+ options: {
+ tags: ['access:ml:canDeleteCalendar'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
index dd9e0ea66aa9dd..32cb2b343f8760 100644
--- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
@@ -33,6 +33,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
{
path: '/api/ml/data_frame/analytics',
validate: false,
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -61,6 +64,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
params: analyticsIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -88,6 +94,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
{
path: '/api/ml/data_frame/analytics/_stats',
validate: false,
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -118,6 +127,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
params: analyticsIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -155,6 +167,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
params: analyticsIdSchema,
body: dataAnalyticsJobConfigSchema,
},
+ options: {
+ tags: ['access:ml:canCreateDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -190,6 +205,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
body: dataAnalyticsEvaluateSchema,
},
+ options: {
+ tags: ['access:ml:canCreateDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -224,6 +242,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
body: dataAnalyticsExplainSchema,
},
+ options: {
+ tags: ['access:ml:canCreateDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -257,6 +278,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
params: analyticsIdSchema,
},
+ options: {
+ tags: ['access:ml:canDeleteDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -291,6 +315,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
params: analyticsIdSchema,
},
+ options: {
+ tags: ['access:ml:canStartStopDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -324,6 +351,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
params: analyticsIdSchema,
query: stopsDataFrameAnalyticsJobQuerySchema,
},
+ options: {
+ tags: ['access:ml:canStartStopDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -364,6 +394,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
validate: {
params: analyticsIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts
index 20029fbd8d1a6b..04008a896a1a22 100644
--- a/x-pack/plugins/ml/server/routes/data_visualizer.ts
+++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts
@@ -88,6 +88,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization)
params: indexPatternTitleSchema,
body: dataVisualizerFieldStatsSchema,
},
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
@@ -150,6 +153,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization)
params: indexPatternTitleSchema,
body: dataVisualizerOverallStatsSchema,
},
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts
index ec667e1d305f52..1fa1d408372da3 100644
--- a/x-pack/plugins/ml/server/routes/datafeeds.ts
+++ b/x-pack/plugins/ml/server/routes/datafeeds.ts
@@ -28,6 +28,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/datafeeds',
validate: false,
+ options: {
+ tags: ['access:ml:canGetDatafeeds'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -57,6 +60,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: datafeedIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetDatafeeds'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -83,6 +89,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/datafeeds/_stats',
validate: false,
+ options: {
+ tags: ['access:ml:canGetDatafeeds'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -112,6 +121,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: datafeedIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetDatafeeds'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -146,6 +158,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
params: datafeedIdSchema,
body: datafeedConfigSchema,
},
+ options: {
+ tags: ['access:ml:canCreateDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -181,6 +196,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
params: datafeedIdSchema,
body: datafeedConfigSchema,
},
+ options: {
+ tags: ['access:ml:canUpdateDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -216,6 +234,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
params: datafeedIdSchema,
query: deleteDatafeedQuerySchema,
},
+ options: {
+ tags: ['access:ml:canDeleteDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -255,6 +276,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
params: datafeedIdSchema,
body: startDatafeedSchema,
},
+ options: {
+ tags: ['access:ml:canStartStopDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -291,6 +315,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: datafeedIdSchema,
},
+ options: {
+ tags: ['access:ml:canStartStopDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -324,6 +351,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: datafeedIdSchema,
},
+ options: {
+ tags: ['access:ml:canPreviewDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts
index 577e8e0161342a..b0f13df294145e 100644
--- a/x-pack/plugins/ml/server/routes/fields_service.ts
+++ b/x-pack/plugins/ml/server/routes/fields_service.ts
@@ -46,8 +46,10 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) {
validate: {
body: getCardinalityOfFieldsSchema,
},
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
-
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
const resp = await getCardinalityOfFields(context, request.body);
@@ -79,6 +81,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) {
validate: {
body: getTimeFieldRangeSchema,
},
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts
index 3f3fc3f547b6a0..0f389f9505943b 100644
--- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts
+++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts
@@ -71,6 +71,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat
accepts: ['text/*', 'application/json'],
maxBytes: MAX_FILE_SIZE_BYTES,
},
+ tags: ['access:ml:canFindFileStructure'],
},
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
@@ -105,6 +106,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat
accepts: ['application/json'],
maxBytes: MAX_FILE_SIZE_BYTES,
},
+ tags: ['access:ml:canFindFileStructure'],
},
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts
index 738c25070358d1..d5287c349a8fca 100644
--- a/x-pack/plugins/ml/server/routes/filters.ts
+++ b/x-pack/plugins/ml/server/routes/filters.ts
@@ -57,6 +57,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/filters',
validate: false,
+ options: {
+ tags: ['access:ml:canGetFilters'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -89,6 +92,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: filterIdSchema,
},
+ options: {
+ tags: ['access:ml:canGetFilters'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -120,6 +126,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
body: createFilterSchema,
},
+ options: {
+ tags: ['access:ml:canCreateFilter'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -155,6 +164,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) {
params: filterIdSchema,
body: updateFilterSchema,
},
+ options: {
+ tags: ['access:ml:canCreateFilter'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -186,6 +198,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
params: filterIdSchema,
},
+ options: {
+ tags: ['access:ml:canDeleteFilter'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -216,6 +231,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/filters/_stats',
validate: false,
+ options: {
+ tags: ['access:ml:canGetFilters'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts
index e434936beba630..fb3ef7fc41c767 100644
--- a/x-pack/plugins/ml/server/routes/indices.ts
+++ b/x-pack/plugins/ml/server/routes/indices.ts
@@ -27,6 +27,9 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) {
validate: {
body: indicesSchema,
},
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts
index 1fe5a7af95d4f0..5acc89e7d13be7 100644
--- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts
+++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts
@@ -33,6 +33,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio
params: jobAuditMessagesJobIdSchema,
query: jobAuditMessagesQuerySchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -67,6 +70,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio
validate: {
query: jobAuditMessagesQuerySchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts
index 149ca2591fd765..05c44e1da97575 100644
--- a/x-pack/plugins/ml/server/routes/job_service.ts
+++ b/x-pack/plugins/ml/server/routes/job_service.ts
@@ -4,11 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import Boom from 'boom';
import { schema } from '@kbn/config-schema';
-import { KibanaRequest } from 'kibana/server';
import { wrapError } from '../client/error_wrapper';
-import { RouteInitialization, JobServiceRouteDeps } from '../types';
+import { RouteInitialization } from '../types';
import {
categorizationFieldExamplesSchema,
chartSchema,
@@ -27,19 +25,7 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job';
/**
* Routes for job service
*/
-export function jobServiceRoutes(
- { router, mlLicense }: RouteInitialization,
- { resolveMlCapabilities }: JobServiceRouteDeps
-) {
- async function hasPermissionToCreateJobs(request: KibanaRequest) {
- const mlCapabilities = await resolveMlCapabilities(request);
- if (mlCapabilities === null) {
- throw new Error('resolveMlCapabilities is not defined');
- }
-
- return mlCapabilities.canCreateJob;
- }
-
+export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) {
/**
* @apiGroup JobService
*
@@ -55,6 +41,9 @@ export function jobServiceRoutes(
validate: {
body: forceStartDatafeedSchema,
},
+ options: {
+ tags: ['access:ml:canStartStopDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -86,6 +75,9 @@ export function jobServiceRoutes(
validate: {
body: datafeedIdsSchema,
},
+ options: {
+ tags: ['access:ml:canStartStopDatafeed'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -117,6 +109,9 @@ export function jobServiceRoutes(
validate: {
body: jobIdsSchema,
},
+ options: {
+ tags: ['access:ml:canDeleteJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -148,6 +143,9 @@ export function jobServiceRoutes(
validate: {
body: jobIdsSchema,
},
+ options: {
+ tags: ['access:ml:canCloseJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -184,6 +182,9 @@ export function jobServiceRoutes(
validate: {
body: jobIdsSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -215,6 +216,9 @@ export function jobServiceRoutes(
validate: {
body: schema.object(jobsWithTimerangeSchema),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -245,6 +249,9 @@ export function jobServiceRoutes(
validate: {
body: jobIdsSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -272,6 +279,9 @@ export function jobServiceRoutes(
{
path: '/api/ml/jobs/groups',
validate: false,
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -302,6 +312,9 @@ export function jobServiceRoutes(
validate: {
body: schema.object(updateGroupsSchema),
},
+ options: {
+ tags: ['access:ml:canUpdateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -329,6 +342,9 @@ export function jobServiceRoutes(
{
path: '/api/ml/jobs/deleting_jobs_tasks',
validate: false,
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -359,6 +375,9 @@ export function jobServiceRoutes(
validate: {
body: jobIdsSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -389,6 +408,9 @@ export function jobServiceRoutes(
params: schema.object({ indexPattern: schema.string() }),
query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -422,6 +444,9 @@ export function jobServiceRoutes(
validate: {
body: schema.object(chartSchema),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -474,6 +499,9 @@ export function jobServiceRoutes(
validate: {
body: schema.object(chartSchema),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -522,6 +550,9 @@ export function jobServiceRoutes(
{
path: '/api/ml/jobs/all_jobs_and_group_ids',
validate: false,
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -552,6 +583,9 @@ export function jobServiceRoutes(
validate: {
body: schema.object(lookBackProgressSchema),
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -583,17 +617,12 @@ export function jobServiceRoutes(
validate: {
body: schema.object(categorizationFieldExamplesSchema),
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
- // due to the use of the _analyze endpoint which is called by the kibana user,
- // basic job creation privileges are required to use this endpoint
- if ((await hasPermissionToCreateJobs(request)) === false) {
- throw Boom.forbidden(
- 'Insufficient privileges, the machine_learning_admin role is required.'
- );
- }
-
const { validateCategoryExamples } = categorizationExamplesProvider(
context.ml!.mlClient.callAsCurrentUser,
context.ml!.mlClient.callAsInternalUser
@@ -644,6 +673,9 @@ export function jobServiceRoutes(
validate: {
body: schema.object(topCategoriesSchema),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts
index dd2bd9deadf43f..632166d6d5fb8f 100644
--- a/x-pack/plugins/ml/server/routes/job_validation.ts
+++ b/x-pack/plugins/ml/server/routes/job_validation.ts
@@ -57,6 +57,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization,
validate: {
body: estimateBucketSpanSchema,
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -106,6 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization,
validate: {
body: modelMemoryLimitSchema,
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -135,6 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization,
validate: {
body: validateCardinalitySchema,
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -167,6 +176,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization,
validate: {
body: validateJobSchema,
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts
index 2891144fc4574a..622ae66ede4264 100644
--- a/x-pack/plugins/ml/server/routes/modules.ts
+++ b/x-pack/plugins/ml/server/routes/modules.ts
@@ -97,6 +97,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
indexPatternTitle: schema.string(),
}),
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -127,6 +130,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
...getModuleIdParamSchema(true),
}),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -161,6 +167,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
params: schema.object(getModuleIdParamSchema()),
body: setupModuleBodySchema,
},
+ options: {
+ tags: ['access:ml:canCreateJob'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -218,6 +227,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) {
validate: {
params: schema.object(getModuleIdParamSchema()),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts
index 59458b1e486db2..e4a9abb0784beb 100644
--- a/x-pack/plugins/ml/server/routes/notification_settings.ts
+++ b/x-pack/plugins/ml/server/routes/notification_settings.ts
@@ -22,6 +22,9 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) {
{
path: '/api/ml/notification_settings',
validate: false,
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts
index 89c267340fe52f..94ca0827ccfa59 100644
--- a/x-pack/plugins/ml/server/routes/results_service.ts
+++ b/x-pack/plugins/ml/server/routes/results_service.ts
@@ -88,6 +88,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization)
validate: {
body: anomaliesTableDataSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -117,6 +120,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization)
validate: {
body: categoryDefinitionSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -146,6 +152,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization)
validate: {
body: maxAnomalyScoreSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -175,6 +184,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization)
validate: {
body: categoryExamplesSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
@@ -204,6 +216,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization)
validate: {
body: partitionFieldValuesSchema,
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts
index d5fe45728c56c7..7ae7dd8eef0653 100644
--- a/x-pack/plugins/ml/server/routes/system.ts
+++ b/x-pack/plugins/ml/server/routes/system.ts
@@ -54,6 +54,9 @@ export function systemRoutes(
validate: {
body: schema.maybe(schema.any()),
},
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
@@ -110,6 +113,9 @@ export function systemRoutes(
{
path: '/api/ml/ml_capabilities',
validate: false,
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
@@ -150,7 +156,11 @@ export function systemRoutes(
{
path: '/api/ml/ml_node_count',
validate: false,
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
+
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
// check for basic license first for consistency with other
@@ -201,6 +211,9 @@ export function systemRoutes(
{
path: '/api/ml/info',
validate: false,
+ options: {
+ tags: ['access:ml:canAccessML'],
+ },
},
mlLicense.basicLicenseAPIGuard(async (context, request, response) => {
try {
@@ -229,6 +242,9 @@ export function systemRoutes(
validate: {
body: schema.maybe(schema.any()),
},
+ options: {
+ tags: ['access:ml:canGetJobs'],
+ },
},
mlLicense.fullLicenseAPIGuard(async (context, request, response) => {
try {
diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts
index d4cd61a7fa4f71..678e81d3526ac8 100644
--- a/x-pack/plugins/ml/server/types.ts
+++ b/x-pack/plugins/ml/server/types.ts
@@ -30,10 +30,6 @@ export interface SystemRouteDeps {
resolveMlCapabilities: ResolveMlCapabilities;
}
-export interface JobServiceRouteDeps {
- resolveMlCapabilities: ResolveMlCapabilities;
-}
-
export interface PluginsSetup {
cloud: CloudSetup;
features: FeaturesPluginSetup;
diff --git a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts
index 59c6cb35669070..ca889e20e695f3 100644
--- a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts
+++ b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts
@@ -7,7 +7,7 @@
import { Action, Dispatch, MiddlewareAPI } from 'redux';
import { track, METRIC_TYPE, TELEMETRY_EVENT } from './';
-import { timelineActions } from '../../store/actions';
+import * as timelineActions from '../../store/timeline/actions';
export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => {
if (timelineActions.endTimelineSaving.match(action)) {
diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts
index 9e9e663a59fe09..686dc096e61b01 100644
--- a/x-pack/plugins/siem/public/store/model.ts
+++ b/x-pack/plugins/siem/public/store/model.ts
@@ -9,15 +9,4 @@ export { dragAndDropModel } from './drag_and_drop';
export { hostsModel } from './hosts';
export { inputsModel } from './inputs';
export { networkModel } from './network';
-
-export type KueryFilterQueryKind = 'kuery' | 'lucene';
-
-export interface KueryFilterQuery {
- kind: KueryFilterQueryKind;
- expression: string;
-}
-
-export interface SerializedFilterQuery {
- kuery: KueryFilterQuery | null;
- serializedQuery: string;
-}
+export * from './types';
diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts
index a03cc2643e014a..12155decf40d44 100644
--- a/x-pack/plugins/siem/public/store/timeline/actions.ts
+++ b/x-pack/plugins/siem/public/store/timeline/actions.ts
@@ -12,7 +12,7 @@ import {
DataProvider,
QueryOperator,
} from '../../components/timeline/data_providers/data_provider';
-import { KueryFilterQuery, SerializedFilterQuery } from '../model';
+import { KueryFilterQuery, SerializedFilterQuery } from '../types';
import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model';
import { TimelineNonEcsData } from '../../graphql/types';
diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/store/types.ts
new file mode 100644
index 00000000000000..2c679ba41116ee
--- /dev/null
+++ b/x-pack/plugins/siem/public/store/types.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export type KueryFilterQueryKind = 'kuery' | 'lucene';
+
+export interface KueryFilterQuery {
+ kind: KueryFilterQueryKind;
+ expression: string;
+}
+
+export interface SerializedFilterQuery {
+ kuery: KueryFilterQuery | null;
+ serializedQuery: string;
+}
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
index d298f1cc7cbc66..a8cc6dc6804102 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { flow, set, omit } from 'lodash/fp';
+import { flow, omit } from 'lodash/fp';
+import set from 'set-value';
import { SearchResponse } from 'elasticsearch';
import { Logger } from '../../../../../../../src/core/server';
@@ -55,8 +56,11 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => {
}
const omitDottedFields = omit(errantFields.map(field => field.name));
- const setNestedFields = errantFields.map(field => set(field.name, field.value));
- const setTimestamp = set('@timestamp', new Date(timestamp).toISOString());
+ const setNestedFields = errantFields.map(field => (_anomaly: Anomaly) =>
+ set(_anomaly, field.name, field.value)
+ );
+ const setTimestamp = (_anomaly: Anomaly) =>
+ set(_anomaly, '@timestamp', new Date(timestamp).toISOString());
return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly);
};
diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts
index b063404f68e4fb..65a810ff94a1f5 100644
--- a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts
+++ b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts
@@ -6,7 +6,7 @@
import { SavedObjectMigrationFn } from 'src/core/server';
-export const migrateToKibana660: SavedObjectMigrationFn = doc => {
+export const migrateToKibana660: SavedObjectMigrationFn = doc => {
if (!doc.attributes.hasOwnProperty('disabledFeatures')) {
doc.attributes.disabledFeatures = [];
}
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 8974f0b5b4d582..a3011ab5bdfa9e 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8225,11 +8225,8 @@
"xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成",
"xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント",
"xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去",
- "xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー",
"xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成",
"xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース",
- "xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除",
- "xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除",
"xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明",
"xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...",
"xpack.ingestManager.agentConfigList.nameColumnTitle": "名前",
@@ -8313,7 +8310,6 @@
"xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」",
"xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません",
"xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション",
- "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー",
"xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除",
"xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明",
"xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集",
@@ -8321,7 +8317,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム",
- "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示",
"xpack.ingestManager.configDetails.subTabs.datasouces": "データソース",
"xpack.ingestManager.configDetails.subTabs.settings": "設定",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル",
@@ -15904,7 +15899,6 @@
"xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ",
"xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "アラートの作成",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "タイプ",
- "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "編集",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index d36a62f15aee9e..e373d05a7d8516 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8228,11 +8228,8 @@
"xpack.ingestManager.agentConfigList.addButton": "创建代理配置",
"xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理",
"xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选",
- "xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置",
"xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源",
"xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源",
- "xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}",
- "xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置",
"xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述",
"xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……",
"xpack.ingestManager.agentConfigList.nameColumnTitle": "名称",
@@ -8316,7 +8313,6 @@
"xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”",
"xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”",
"xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作",
- "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源",
"xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源",
"xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述",
"xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源",
@@ -8324,7 +8320,6 @@
"xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间",
"xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包",
"xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数",
- "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源",
"xpack.ingestManager.configDetails.subTabs.datasouces": "数据源",
"xpack.ingestManager.configDetails.subTabs.settings": "设置",
"xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件",
@@ -15909,7 +15904,6 @@
"xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型",
"xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "创建告警",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型",
- "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "编辑",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称",
"xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标记",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx
index 72c22f46f217e7..8406987e4ed9df 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx
@@ -190,5 +190,25 @@ describe('alert_form', () => {
const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]');
expect(alertTypeSelectOptions.exists()).toBeTruthy();
});
+
+ it('should update throttle value', async () => {
+ const newThrottle = 17;
+ await setup();
+ const throttleField = wrapper.find('[data-test-subj="throttleInput"]');
+ expect(throttleField.exists()).toBeTruthy();
+ throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } });
+ const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]');
+ expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle);
+ });
+
+ it('should unset throttle value', async () => {
+ const newThrottle = '';
+ await setup();
+ const throttleField = wrapper.find('[data-test-subj="throttleInput"]');
+ expect(throttleField.exists()).toBeTruthy();
+ throttleField.at(1).simulate('change', { target: { value: newThrottle } });
+ const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]');
+ expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle);
+ });
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
index 66aa02e1930a3e..a51ebc31267853 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx
@@ -245,10 +245,6 @@ describe('alerts_list component with items', () => {
expect(wrapper.find('EuiBasicTable')).toHaveLength(1);
expect(wrapper.find('EuiTableRow')).toHaveLength(2);
});
- it('renders edit button for registered alert types', async () => {
- await setup();
- expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBeGreaterThan(0);
- });
});
describe('alerts_list component empty with show only capability', () => {
@@ -442,8 +438,4 @@ describe('alerts_list with show only capability', () => {
expect(wrapper.find('EuiTableRow')).toHaveLength(2);
// TODO: check delete button
});
- it('not renders edit button for non registered alert types', async () => {
- await setup();
- expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBe(0);
- });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
index 1103d7c3921a7e..2d9cfcdbda89f8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx
@@ -24,7 +24,7 @@ import { isEmpty } from 'lodash';
import { AlertsContextProvider } from '../../../context/alerts_context';
import { useAppDependencies } from '../../../app_context';
import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types';
-import { AlertAdd, AlertEdit } from '../../alert_form';
+import { AlertAdd } from '../../alert_form';
import { BulkOperationPopover } from '../../common/components/bulk_operation_popover';
import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons';
import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions';
@@ -85,8 +85,6 @@ export const AlertsList: React.FunctionComponent = () => {
data: [],
totalItemCount: 0,
});
- const [editedAlertItem, setEditedAlertItem] = useState(undefined);
- const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false);
const [alertsToDelete, setAlertsToDelete] = useState([]);
useEffect(() => {
@@ -162,11 +160,6 @@ export const AlertsList: React.FunctionComponent = () => {
}
}
- async function editItem(alertTableItem: AlertTableItem) {
- setEditedAlertItem(alertTableItem);
- setEditFlyoutVisibility(true);
- }
-
const alertsTableColumns = [
{
field: 'name',
@@ -219,27 +212,6 @@ export const AlertsList: React.FunctionComponent = () => {
truncateText: false,
'data-test-subj': 'alertsTableCell-interval',
},
- {
- name: '',
- width: '50px',
- render(item: AlertTableItem) {
- if (!canSave || !alertTypeRegistry.has(item.alertTypeId)) {
- return;
- }
- return (
- editItem(item)}
- >
-
-
- );
- },
- },
{
name: '',
width: '40px',
@@ -453,14 +425,6 @@ export const AlertsList: React.FunctionComponent = () => {
addFlyoutVisible={alertFlyoutVisible}
setAddFlyoutVisibility={setAlertFlyoutVisibility}
/>
- {editFlyoutVisible && editedAlertItem ? (
-
- ) : null}
);
diff --git a/x-pack/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts
index 72d498056d6b3c..00baa39044a557 100644
--- a/x-pack/plugins/uptime/common/constants/index.ts
+++ b/x-pack/plugins/uptime/common/constants/index.ts
@@ -11,6 +11,6 @@ export { CONTEXT_DEFAULTS } from './context_defaults';
export * from './capabilities';
export * from './settings_defaults';
export { PLUGIN } from './plugin';
-export { QUERY, STATES } from './query';
+export { QUERY } from './query';
export * from './ui';
export * from './rest_api';
diff --git a/x-pack/plugins/uptime/common/constants/query.ts b/x-pack/plugins/uptime/common/constants/query.ts
index d728f114aae76e..21574f1d8b27e0 100644
--- a/x-pack/plugins/uptime/common/constants/query.ts
+++ b/x-pack/plugins/uptime/common/constants/query.ts
@@ -25,10 +25,3 @@ export const QUERY = {
'error.type',
],
};
-
-export const STATES = {
- // Number of results returned for a states query
- LEGACY_STATES_QUERY_SIZE: 10,
- // The maximum number of monitors that should be supported
- MAX_MONITORS: 35000,
-};
diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts
index d21259fad77a66..8612d71dfe9394 100644
--- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts
+++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts
@@ -6,7 +6,7 @@
import { get, sortBy } from 'lodash';
import { QueryContext } from './query_context';
-import { QUERY, STATES } from '../../../../common/constants';
+import { QUERY } from '../../../../common/constants';
import {
Check,
Histogram,
@@ -314,7 +314,7 @@ const getHistogramForMonitors = async (
by_id: {
terms: {
field: 'monitor.id',
- size: STATES.LEGACY_STATES_QUERY_SIZE,
+ size: queryContext.size,
},
aggs: {
histogram: {
diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts
index 4f17f9db674839..19879f5761ab2c 100644
--- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts
+++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts
@@ -39,6 +39,7 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
}
it('should work with a filterQuery', async () => {
@@ -53,6 +54,21 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
+ });
+ it('should work with a filterQuery in KQL format', async () => {
+ const searchBody = getElasticsearchMetricQuery(
+ getSearchParams('avg'),
+ undefined,
+ '"agent.hostname":"foo"'
+ );
+ const result = await client.search({
+ index,
+ body: searchBody,
+ });
+ expect(result.error).to.not.be.ok();
+ expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
});
describe('querying with a groupBy parameter', () => {
@@ -65,6 +81,7 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
}
it('should work with a filterQuery', async () => {
@@ -79,6 +96,7 @@ export default function({ getService }: FtrProviderContext) {
});
expect(result.error).to.not.be.ok();
expect(result.hits).to.be.ok();
+ expect(result.aggregations).to.be.ok();
});
});
});
diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts
index bbc766df34dcfe..10857caab98e27 100644
--- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts
+++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts
@@ -91,12 +91,11 @@ export default ({ getService }: FtrProviderContext) => {
model_plot_config: { enabled: true },
},
expected: {
- responseCode: 403,
+ responseCode: 404,
responseBody: {
- statusCode: 403,
- error: 'Forbidden',
- message:
- '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]',
+ statusCode: 404,
+ error: 'Not Found',
+ message: 'Not Found',
},
},
},
diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts
index 6a57db1687868f..a5cb68d7821267 100644
--- a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts
+++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts
@@ -197,8 +197,8 @@ export default ({ getService }: FtrProviderContext) => {
requestBody: {},
// Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic.
expected: {
- responseCode: 403,
- error: 'Forbidden',
+ responseCode: 404,
+ error: 'Not Found',
},
},
];
diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index 23ddd3b63a2eff..c42fc95c1bc7f2 100644
--- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -84,10 +84,9 @@ export default ({ getService }: FtrProviderContext) => {
startDatafeed: false,
},
expected: {
- responseCode: 403,
- error: 'Forbidden',
- message:
- '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]',
+ responseCode: 404,
+ error: 'Not Found',
+ message: 'Not Found',
},
},
];
diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.js b/x-pack/test/functional/apps/dashboard/reporting/index.ts
similarity index 94%
rename from x-pack/test/functional/apps/dashboard/reporting/index.js
rename to x-pack/test/functional/apps/dashboard/reporting/index.ts
index 99be084d80d74c..796e15b4e270f4 100644
--- a/x-pack/test/functional/apps/dashboard/reporting/index.js
+++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts
@@ -5,9 +5,10 @@
*/
import expect from '@kbn/expect';
-import path from 'path';
import fs from 'fs';
+import path from 'path';
import { promisify } from 'util';
+import { FtrProviderContext } from '../../../ftr_provider_context';
import { checkIfPngsMatch } from './lib/compare_pngs';
const writeFileAsync = promisify(fs.writeFile);
@@ -15,7 +16,7 @@ const mkdirAsync = promisify(fs.mkdir);
const REPORTS_FOLDER = path.resolve(__dirname, 'reports');
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const log = getService('log');
@@ -85,14 +86,14 @@ export default function({ getService, getPageObjects }) {
describe('Preserve Layout', () => {
it('matches baseline report', async function() {
- const writeSessionReport = async (name, rawPdf, reportExt) => {
+ const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => {
const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session');
await mkdirAsync(sessionDirectory, { recursive: true });
const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`);
await writeFileAsync(sessionReportPath, rawPdf);
return sessionReportPath;
};
- const getBaselineReportPath = (fileName, reportExt) => {
+ const getBaselineReportPath = (fileName: string, reportExt: string) => {
const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline');
const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`);
log.debug(`getBaselineReportPath (${fullPath})`);
diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts
similarity index 90%
rename from x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js
rename to x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts
index 13c97a7fce7858..b2eb645c8372c0 100644
--- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js
+++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts
@@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import path from 'path';
-import fs from 'fs';
import { promisify } from 'bluebird';
+import fs from 'fs';
+import path from 'path';
import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs';
-const mkdirAsync = promisify(fs.mkdir);
+const mkdirAsync = promisify(fs.mkdir);
-export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) {
+export async function checkIfPngsMatch(
+ actualpngPath: string,
+ baselinepngPath: string,
+ screenshotsDirectory: string,
+ log: any
+) {
log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`);
// Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be
// stored.
diff --git a/x-pack/test/functional/apps/discover/reporting.js b/x-pack/test/functional/apps/discover/reporting.ts
similarity index 96%
rename from x-pack/test/functional/apps/discover/reporting.js
rename to x-pack/test/functional/apps/discover/reporting.ts
index 4aa005fc2db553..7a33e7f5135d48 100644
--- a/x-pack/test/functional/apps/discover/reporting.js
+++ b/x-pack/test/functional/apps/discover/reporting.ts
@@ -5,8 +5,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const log = getService('log');
const esArchiver = getService('esArchiver');
const browser = getService('browser');
diff --git a/x-pack/test/functional/apps/visualize/reporting.js b/x-pack/test/functional/apps/visualize/reporting.ts
similarity index 94%
rename from x-pack/test/functional/apps/visualize/reporting.js
rename to x-pack/test/functional/apps/visualize/reporting.ts
index bc252e1ad4134b..5ef954e334d815 100644
--- a/x-pack/test/functional/apps/visualize/reporting.js
+++ b/x-pack/test/functional/apps/visualize/reporting.ts
@@ -5,8 +5,9 @@
*/
import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
-export default function({ getService, getPageObjects }) {
+export default function({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const log = getService('log');
diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json
index 3fe4f828ba128b..d22e3cd3fecdda 100644
--- a/x-pack/test/functional/es_archives/fleet/agents/data.json
+++ b/x-pack/test/functional/es_archives/fleet/agents/data.json
@@ -11,8 +11,8 @@
"shared_id": "agent1_filebeat",
"config_id": "1",
"type": "PERMANENT",
- "local_metadata": "{}",
- "user_provided_metadata": "{}"
+ "local_metadata": {},
+ "user_provided_metadata": {}
}
}
}
@@ -30,8 +30,8 @@
"active": true,
"shared_id": "agent2_filebeat",
"type": "PERMANENT",
- "local_metadata": "{}",
- "user_provided_metadata": "{}"
+ "local_metadata": {},
+ "user_provided_metadata": {}
}
}
}
@@ -49,8 +49,8 @@
"active": true,
"shared_id": "agent3_metricbeat",
"type": "PERMANENT",
- "local_metadata": "{}",
- "user_provided_metadata": "{}"
+ "local_metadata": {},
+ "user_provided_metadata": {}
}
}
}
@@ -68,8 +68,8 @@
"active": true,
"shared_id": "agent4_metricbeat",
"type": "PERMANENT",
- "local_metadata": "{}",
- "user_provided_metadata": "{}"
+ "local_metadata": {},
+ "user_provided_metadata": {}
}
}
}
diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json
index 5d5d373797d4cf..409cc3c689eafc 100644
--- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json
+++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json
@@ -227,7 +227,7 @@
"type": "date"
},
"local_metadata": {
- "type": "text"
+ "type": "flattened"
},
"shared_id": {
"type": "keyword"
@@ -239,7 +239,7 @@
"type": "date"
},
"user_provided_metadata": {
- "type": "text"
+ "type": "flattened"
},
"version": {
"type": "keyword"
diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts
index 782d57adea7707..4b8c2944ef190f 100644
--- a/x-pack/test/functional/page_objects/index.ts
+++ b/x-pack/test/functional/page_objects/index.ts
@@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page';
import { GrokDebuggerPageProvider } from './grok_debugger_page';
// @ts-ignore not ts yet
import { WatcherPageProvider } from './watcher_page';
-// @ts-ignore not ts yet
import { ReportingPageProvider } from './reporting_page';
// @ts-ignore not ts yet
import { AccountSettingProvider } from './accountsetting_page';
diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.ts
similarity index 84%
rename from x-pack/test/functional/page_objects/reporting_page.js
rename to x-pack/test/functional/page_objects/reporting_page.ts
index b24ba8cf95d1ce..2c20519a8d2140 100644
--- a/x-pack/test/functional/page_objects/reporting_page.js
+++ b/x-pack/test/functional/page_objects/reporting_page.ts
@@ -4,25 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import http, { IncomingMessage } from 'http';
+import { FtrProviderContext } from 'test/functional/ftr_provider_context';
import { parse } from 'url';
-import http from 'http';
-/*
- * NOTE: Reporting is a service, not an app. The page objects that are
- * important for generating reports belong to the apps that integrate with the
- * Reporting service. Eventually, this file should be dissolved across the
- * apps that need it for testing their integration.
- * Issue: https://github.com/elastic/kibana/issues/52927
- */
-export function ReportingPageProvider({ getService, getPageObjects }) {
+export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const log = getService('log');
const testSubjects = getService('testSubjects');
const browser = getService('browser');
- const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']);
+ const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript
class ReportingPage {
- async forceSharedItemsContainerSize({ width }) {
+ async forceSharedItemsContainerSize({ width }: { width: number }) {
await browser.execute(`
var el = document.querySelector('[data-shared-items-container]');
el.style.flex="none";
@@ -30,7 +24,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
`);
}
- async getReportURL(timeout) {
+ async getReportURL(timeout: number) {
log.debug('getReportURL');
const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout);
@@ -48,7 +42,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
`);
}
- getResponse(url) {
+ getResponse(url: string): Promise {
log.debug(`getResponse for ${url}`);
const auth = 'test_user:changeme'; // FIXME not sure why there is no config that can be read for this
const headers = {
@@ -62,29 +56,30 @@ export function ReportingPageProvider({ getService, getPageObjects }) {
hostname: parsedUrl.hostname,
path: parsedUrl.path,
port: parsedUrl.port,
- responseType: 'arraybuffer',
headers,
},
- res => {
+ (res: IncomingMessage) => {
resolve(res);
}
)
- .on('error', e => {
+ .on('error', (e: Error) => {
log.error(e);
reject(e);
});
});
}
- async getRawPdfReportData(url) {
- const data = []; // List of Buffer objects
+ async getRawPdfReportData(url: string): Promise {
+ const data: Buffer[] = []; // List of Buffer objects
log.debug(`getRawPdfReportData for ${url}`);
return new Promise(async (resolve, reject) => {
const response = await this.getResponse(url).catch(reject);
- response.on('data', chunk => data.push(chunk));
- response.on('end', () => resolve(Buffer.concat(data)));
+ if (response) {
+ response.on('data', (chunk: Buffer) => data.push(chunk));
+ response.on('end', () => resolve(Buffer.concat(data)));
+ }
});
}
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
index 597f1ad9119b09..9e99c60b4dcb7e 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts
@@ -126,193 +126,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
]);
});
- it('should edit an alert', async () => {
- const createdAlert = await createAlert({
- alertTypeId: '.index-threshold',
- name: generateUniqueKey(),
- params: {
- aggType: 'count',
- termSize: 5,
- thresholdComparator: '>',
- timeWindowSize: 5,
- timeWindowUnit: 'm',
- groupBy: 'all',
- threshold: [1000, 5000],
- index: ['.kibana_1'],
- timeField: 'alert',
- },
- });
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
-
- const searchResults = await pageObjects.triggersActionsUI.getAlertsList();
- expect(searchResults).to.eql([
- {
- name: createdAlert.name,
- tagsText: 'foo, bar',
- alertType: 'Index threshold',
- interval: '1m',
- },
- ]);
- const editLink = await testSubjects.findAll('alertsTableCell-editLink');
- await editLink[0].click();
-
- const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`;
- await testSubjects.setValue('alertNameInput', updatedAlertName, { clearWithKeyboard: true });
-
- await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)');
-
- const toastTitle = await pageObjects.common.closeToast();
- expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`);
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(updatedAlertName);
-
- const searchResultsAfterEdit = await pageObjects.triggersActionsUI.getAlertsList();
- expect(searchResultsAfterEdit).to.eql([
- {
- name: updatedAlertName,
- tagsText: 'foo, bar',
- alertType: 'Index threshold',
- interval: '1m',
- },
- ]);
- });
-
- it('should set an alert throttle', async () => {
- const alertName = `edit throttle ${generateUniqueKey()}`;
- const createdAlert = await createAlert({
- alertTypeId: '.index-threshold',
- name: alertName,
- params: {
- aggType: 'count',
- termSize: 5,
- thresholdComparator: '>',
- timeWindowSize: 5,
- timeWindowUnit: 'm',
- groupBy: 'all',
- threshold: [1000, 5000],
- index: ['.kibana_1'],
- timeField: 'alert',
- },
- });
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
-
- const searchResults = await pageObjects.triggersActionsUI.getAlertsList();
- expect(searchResults).to.eql([
- {
- name: createdAlert.name,
- tagsText: 'foo, bar',
- alertType: 'Index threshold',
- interval: '1m',
- },
- ]);
-
- const editLink = await testSubjects.findAll('alertsTableCell-editLink');
- await editLink[0].click();
-
- await testSubjects.setValue('throttleInput', '1', { clearWithKeyboard: true });
-
- await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)');
-
- expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`);
-
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
- await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click();
- const throttleInput = await testSubjects.find('throttleInput');
- expect(await throttleInput.getAttribute('value')).to.eql('1');
- });
-
- it('should unset an alert throttle', async () => {
- const alertName = `edit throttle ${generateUniqueKey()}`;
- const createdAlert = await createAlert({
- alertTypeId: '.index-threshold',
- name: alertName,
- throttle: '10m',
- params: {
- aggType: 'count',
- termSize: 5,
- thresholdComparator: '>',
- timeWindowSize: 5,
- timeWindowUnit: 'm',
- groupBy: 'all',
- threshold: [1000, 5000],
- index: ['.kibana_1'],
- timeField: 'alert',
- },
- });
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
-
- const searchResults = await pageObjects.triggersActionsUI.getAlertsList();
- expect(searchResults).to.eql([
- {
- name: createdAlert.name,
- tagsText: 'foo, bar',
- alertType: 'Index threshold',
- interval: '1m',
- },
- ]);
-
- const editLink = await testSubjects.findAll('alertsTableCell-editLink');
- await editLink[0].click();
-
- const throttleInputToUnsetValue = await testSubjects.find('throttleInput');
-
- expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql('10');
- await throttleInputToUnsetValue.click();
- await throttleInputToUnsetValue.clearValueWithKeyboard();
-
- expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql('');
-
- await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)');
-
- expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`);
-
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
- await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click();
- const throttleInput = await testSubjects.find('throttleInput');
- expect(await throttleInput.getAttribute('value')).to.eql('');
- });
-
- it('should reset alert when canceling an edit', async () => {
- const createdAlert = await createAlert({
- alertTypeId: '.index-threshold',
- name: generateUniqueKey(),
- params: {
- aggType: 'count',
- termSize: 5,
- thresholdComparator: '>',
- timeWindowSize: 5,
- timeWindowUnit: 'm',
- groupBy: 'all',
- threshold: [1000, 5000],
- index: ['.kibana_1'],
- timeField: 'alert',
- },
- });
- await pageObjects.common.navigateToApp('triggersActions');
- await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name);
-
- const editLink = await testSubjects.findAll('alertsTableCell-editLink');
- await editLink[0].click();
-
- const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`;
- await testSubjects.setValue('alertNameInput', updatedAlertName);
-
- await testSubjects.click('cancelSaveEditedAlertButton');
- await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]');
-
- const editLinkPostCancel = await testSubjects.findAll('alertsTableCell-editLink');
- await editLinkPostCancel[0].click();
-
- const nameInputAfterCancel = await testSubjects.find('alertNameInput');
- const textAfterCancel = await nameInputAfterCancel.getAttribute('value');
- expect(textAfterCancel).to.eql(createdAlert.name);
- });
-
it('should search for tags', async () => {
const createdAlert = await createAlert();
await pageObjects.common.navigateToApp('triggersActions');
diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts
index c5f3e65581df97..9622715e87e55f 100644
--- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts
+++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts
@@ -40,7 +40,7 @@ export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger
} catch (ex) {
logger.info(`log event error: ${ex}`);
await context.core.savedObjects.client.create('event_log_test', {}, { id });
- logger.info(`created saved object`);
+ logger.info(`created saved object ${id}`);
}
eventLogger.logEvent(event);
logger.info(`logged`);
diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
index e5b840b335846f..f3a3d58336b1db 100644
--- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts
@@ -19,7 +19,9 @@ export default function({ getService }: FtrProviderContext) {
const log = getService('log');
const retry = getService('retry');
- describe('Event Log public API', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/64723
+ // FLAKY: https://github.com/elastic/kibana/issues/64812
+ describe.skip('Event Log public API', () => {
it('should allow querying for events by Saved Object', async () => {
const id = uuid.v4();
@@ -203,6 +205,7 @@ export default function({ getService }: FtrProviderContext) {
kibana: {
saved_objects: [
{
+ rel: 'primary',
namespace: 'default',
type: 'event_log_test',
id,
diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts
index 31668e8345275e..361d80aaedd419 100644
--- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts
@@ -101,7 +101,7 @@ export default function({ getService }: FtrProviderContext) {
const eventId = uuid.v4();
const event: IEvent = {
event: { action: 'action1', provider: 'provider4' },
- kibana: { saved_objects: [{ type: 'event_log_test', id: eventId }] },
+ kibana: { saved_objects: [{ rel: 'primary', type: 'event_log_test', id: eventId }] },
};
await logTestEvent(eventId, event);
diff --git a/yarn.lock b/yarn.lock
index ee00ef283f07c0..94e6a0a11aa993 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4798,6 +4798,11 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
+"@types/set-value@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/set-value/-/set-value-2.0.0.tgz#63d386b103926dcf49b50e16e0f6dd49983046be"
+ integrity sha512-k8dCJEC80F/mbsIOZ5Hj3YSzTVVVBwMdtP/M9Rtc2TM4F5etVd+2UG8QUiAUfbXm4fABedL2tBZnrBheY7UwpA==
+
"@types/shot@*":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/shot/-/shot-4.0.0.tgz#7545500c489b65c69b5bc5446ba4fef3bd26af92"
@@ -26910,6 +26915,13 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
+set-value@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90"
+ integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"