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 0000000000000..51492756ef232
--- /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 b73785647f23c..7b624f12ac1df 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 cdf9171a46aed..3d8b5d115c8a2 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 1cc1a1194a537..a9fabb38df869 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 0c415ed1a7fad..1e0b890015993 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 a502c40db0cd8..a3294fb0a087a 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 6d4e252fe7532..3f4090619edbf 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 be51400addbbc..8e2395ee6310d 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 04a0d871cab2d..3969a97fa7789 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 0000000000000..c3998876c9712
--- /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 1bbd6cf67f0ce..1cb89822eb605 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 0fd82ffb2240c..e1df493143b73 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
@@ -43,7 +43,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/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md
index 8c5fe4875aaea..c91c00bc1aa02 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 c25918c6b7328..e29837aecb125 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 1c9492d81c7f6..bafa1932e5e92 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 318afb652999e..0734e178033e2 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 0000000000000..7ed0919f88c61
--- /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 0000000000000..048f195fe1223
--- /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 d0ef2aeb265fe..fb2972735c2b7 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 23fdabe0f3430..4c319873af804 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 18e4b7b26b6ba..f79b1df77f8e1 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 52b59c53b658c..d97ef477c2ee0 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 b92bb209d2607..af06b207889c2 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 466d399f653cd..f7274740ea5fe 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 0000000000000..76a890d26bfa0
--- /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 6bc085dde872e..85f15b4c18b66 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 7ba4613c857d7..4e1f5981d6a41 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 a33e16895078e..acd2c7b5284aa 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 dc1c9d379d508..e8b77a8570291 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/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx
index 5f6b67ee6ad20..7de054f2eaa9c 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 d48aacc1d8c1e..248a3f991d6cb 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 0fdf395e75bca..6f121ceb2d373 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 9829498118cc0..22ed18f75c652 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 7c1d0568cd3d7..4f7945d6dd601 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 5b8582bf821ef..db2fbeb278802 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 aa744324428a4..a58df547fa522 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 698edbf9cd6a8..e21d27a70e02a 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 86560b3ccf7b1..91dea66f06a94 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -699,7 +699,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)
//
@@ -842,6 +845,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 a8eb3a3fe8102..4dba157a6f554 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 fa15406189041..9cdd25d3213ce 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 a6260e782c12f..034af03842ab8 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 4fbdac47fb3b0..86ef69be572a9 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 57f3aa85ad944..3ecdc17cb57f3 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 087b83127079f..eec75b0841133 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 971e820ac6ddf..cd52a09caeaad 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 e93e989034252..9cb55f94537c5 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 1e051880d3f19..72e91eb58c8a9 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 7a16386ea484c..c64f7361a8cf4 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 45fa5e11e2a3d..c8ded51193c92 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 5d94b6516c2ba..df4ba23244b4d 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 34922976f22ff..1e5508b44ee0e 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 94473e35a942d..f6455d0c1e43f 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/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts
new file mode 100644
index 0000000000000..5ea151dffdc8e
--- /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 031a0e3576ccc..9590f9f8c1794 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 b6d13a5604011..c384e41851e15 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/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts
index 101e18f2583e3..3e9262c05efac 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 0e46ef4919626..a564b87f2ca50 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 5c3e36b88879d..16fadc8b06cd5 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 26d8a1d1777c0..35a0018049c33 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 26970dc6b2b0d..bf005301adc07 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/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json
index f487e9262e50e..0a858969c4f6a 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 9c923fe77d035..57fe90a8e876e 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 8cc2c74b60e57..fd149d132031e 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 f79962a324131..66c16d0ddf383 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 47d273b9981e3..c0ff87234c09d 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 6a745931420c0..2bda194a65d13 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 bcfd7bd45a6f5..1a710a6fa4865 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 b7fa25cb6eb9c..0612b5319c15b 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 baf53ef447914..58be6707b0373 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 e77d2ea0fb7c9..beb31d548c670 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/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 a7d4e36d16f2a..bff799798ff6e 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 5deb33ccf10f1..620b663451ea3 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/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts
index 7705956590c16..96121251b133e 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,
@@ -26,7 +24,7 @@ export interface NewAgentConfig {
monitoring_enabled?: Array<'logs' | 'metrics'>;
}
-export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes {
+export interface AgentConfig extends NewAgentConfig {
id: string;
status: AgentConfigStatus;
datasources: string[] | Datasource[];
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 885e0a9316d79..ca61a93d9be93 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/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 0000000000000..1e7a14e350229
--- /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 0f3ddee29fa44..5a06a9a879441 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/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts
index 0d19ecd0cb735..e2fc190e158f9 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/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 39d882f7fdf65..f1e3fea6a0742 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 792389381eaf0..c4d602c2c2081 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 85cc758fc4c46..10b30a5696d83 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 1eee9f6b0c346..a0418c5f256c4 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 0000000000000..d4c39f21a1ea6
--- /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 71ada155373bf..ef88aa5d17f1e 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/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
index 1ea162252c741..3dcc19bc4a5ae 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/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx
index 05d150fd9ae23..70d8e7d6882f8 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/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 359c06a6a9ebc..48729448b2ea5 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 3c61d270b1bcf..c8d8064e60e38 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 1461449f3c1c8..d73f83e75c0e4 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 eabcdfa7a24ab..949ae1f43448e 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 50cd1ad8bd53a..e684fe8b3b5d6 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 5f35ef650a08c..0000000000000
--- 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 97a46f4a3e176..0000000000000
--- 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 074c40759f8d8..9df79aa9e0908 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 fe14f472341af..73fd144b9c7f8 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 e4f3677d0fe88..06635e663361d 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 2008b326a539c..02471b935c97c 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 2b3e976a77ea7..44963722f8afc 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 3ab51b5fa3f2b..1308fa3b7ca60 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 73b8019a31eaa..04a1c3865f22d 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 181f192520d0d..ed0af8545f012 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 beccf0dc46eb4..d176905c65120 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 c72fa0fec24d7..e91edf9cc0183 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 e80308cc9acdb..4cc330d40efd7 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 3d238723b7438..583fba1a4a999 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 0000000000000..9c1d11b71984d
--- /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 e23f963b2c40d..f224e37e04467 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 0000000000000..0992e3c361fcf
--- /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 1ddad047da722..3b59853d0ab62 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 4473d68d3c646..2498c36967a53 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 946e1c240be31..48deb3ee86820 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 b2bca241c468c..abbb270149955 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 da1e192bf2412..83a118b795192 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 9263b975b20e7..94cc57b53b4e2 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 7100a5f8eaabc..eac294c5f244a 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 795c579462b69..00fcefb2c379f 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 ffce2d3b2af81..9992f43387c89 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 dee890445f9a3..c7c80638e4c37 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 f2f5ec3078e62..ec29f14a0ff64 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 937993f1d8f71..31a421c2e31bf 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 1c91b69801648..83b30d336ccd4 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 a6efcbc0d3ffb..49bcf12043d7c 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 55b170487d95a..7f5fff4dc3147 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 1e5c0b4a6655c..29b9b01754027 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 0ea8320e966bd..ffe2eff9f3ca7 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 c9f1bfd4d44e4..4cd93df6d9bf4 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 ce4f8125d77af..6a71b2a0caf41 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 1fe1023e28ab9..542c2bb12d8e5 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 63e9aeb61bad0..b08e5fa688b4b 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 0e0ae7b924e17..b81e4a4fc73c2 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 32578fc739f26..ba22bf72cc18c 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 c3b6a484d8787..2cc58c02dbfcf 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 d6ba435155c60..36284a70fb97d 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 dcf87b3ad1ef1..ddbc99c88a877 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 f32273e3e7f76..62b5e7c7aec4a 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 653a8da74a105..bc66c88b082a3 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 1f9a33c191764..c997d5325296a 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 216703f08f069..c04bd504ad8c0 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 22a738a340b25..f82928ffeddd2 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 70b85fc97ebfa..5086603fa8403 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 55f110e9a8291..9859adf062485 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 6e4cc40efeed7..180ecbb797339 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 0f4d92cabaa7a..fcdad66d65251 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 960db293f1124..17e3ad2f8de08 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 1a005a76547f5..0ea6533fc122a 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 58fd319589ea3..f6859e72d71b3 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 1e7167547a6de..271c185860b07 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 9d70dae969362..8ec92dfa4ef77 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 4cc6d85cd947a..d7541f3e09e6c 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 ab1bde48e7ebf..96bc22ca7e6f2 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 c15d417d10289..f21f97dc8d15f 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 b60d6f5113e06..0555997941baa 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 e56ebc24bdae1..fe6fc37eaf81e 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 9a77453b65d6a..d6b7d70c1aa77 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 96724c2a88045..c945818a83e8a 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 516264149fac7..c555ba322fa2b 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 6e5dca7d54e5b..3b6f58479a2f2 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 0000000000000..3d48e44e26eaa
--- /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 f256b6cb8f2d5..8a44b5ab607bf 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 0000000000000..3b9864be6df53
--- /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 9cf673081d320..2dc0f4fe7a821 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 0000000000000..217cad30bfdbb
--- /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 e68851dc3582b..051802cc41b5b 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 7d509c4e27167..e0e4495d47c34 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/spaces/server/saved_objects/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts
index b063404f68e4f..65a810ff94a1f 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 8974f0b5b4d58..81dc44f3a4cb4 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 ファイル",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index d36a62f15aee9..e06edb45de8fa 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 文件",
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 c5f3e65581df9..9622715e87e55 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 d7bbc29bd861e..f3a3d58336b1d 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
@@ -205,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 31668e8345275..361d80aaedd41 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);