diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md index f288573cd7abb5..c06c3c6f684922 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md @@ -9,5 +9,5 @@ Get field list by providing an index patttern (or spec) Signature: ```typescript -getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md index 32bf6fc13b02c2..aec84866b9e585 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md @@ -9,5 +9,5 @@ Get field list by providing { pattern } Signature: ```typescript -getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 57bb98de09ebdd..34df8656e91759 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -26,8 +26,8 @@ export declare class IndexPatternsService | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | -| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | -| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | | [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md deleted file mode 100644 index ba2efcc9b75ca1..00000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [getCapabilitiesForRollupIndices](./kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md) - -## getCapabilitiesForRollupIndices() function - -Signature: - -```typescript -export declare function getCapabilitiesForRollupIndices(indices: { - [key: string]: any; -}): { - [key: string]: any; -}; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| indices | {
[key: string]: any;
} | | - -Returns: - -`{ - [key: string]: any; -}` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index f0989097a727db..addd29916d81df 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -15,8 +15,6 @@ getFieldsForWildcard(options: { fieldCapsOptions?: { allow_no_indices: boolean; }; - type?: string; - rollupIndex?: string; }): Promise; ``` @@ -24,7 +22,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
type?: string;
rollupIndex?: string;
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allow_no_indices: boolean;
};
} | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md index 6528b1c213ccad..e7c331bad64e84 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 4b4e861aef7846..653adda6f2ac8c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -28,7 +28,6 @@ | Function | Description | | --- | --- | -| [getCapabilitiesForRollupIndices(indices)](./kibana-plugin-plugins-data-server.getcapabilitiesforrollupindices.md) | | | [getDefaultSearchParams(uiSettingsClient)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | | [getShardTimeout(config)](./kibana-plugin-plugins-data-server.getshardtimeout.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | | @@ -77,7 +76,6 @@ | [esQuery](./kibana-plugin-plugins-data-server.esquery.md) | | | [fieldFormats](./kibana-plugin-plugins-data-server.fieldformats.md) | | | [indexPatterns](./kibana-plugin-plugins-data-server.indexpatterns.md) | | -| [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) | | | [search](./kibana-plugin-plugins-data-server.search.md) | | | [UI\_SETTINGS](./kibana-plugin-plugins-data-server.ui_settings.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md deleted file mode 100644 index 2880e2d0d8f2cc..00000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [mergeCapabilitiesWithFields](./kibana-plugin-plugins-data-server.mergecapabilitieswithfields.md) - -## mergeCapabilitiesWithFields variable - -Signature: - -```typescript -mergeCapabilitiesWithFields: (rollupIndexCapabilities: { - [key: string]: any; -}, fieldsFromFieldCapsApi: { - [key: string]: any; -}, previousFields?: FieldDescriptor[]) => FieldDescriptor[] -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 8546ec51a15366..660644ae73255f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,7 +12,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -31,7 +31,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }` diff --git a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap b/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap deleted file mode 100644 index cc4e75a7a0fb47..00000000000000 --- a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap +++ /dev/null @@ -1,142 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: link bins 1`] = ` -Array [ - Array [ - Map { - "kibana" => Project { - "allDependencies": Object { - "bar": "link:packages/bar", - }, - "devDependencies": Object {}, - "isSinglePackageJsonProject": true, - "json": Object { - "dependencies": Object { - "bar": "link:packages/bar", - }, - "name": "kibana", - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/package.json", - "path": "/packages/kbn-pm/src/commands", - "productionDependencies": Object { - "bar": "link:packages/bar", - }, - "scripts": Object {}, - "targetLocation": "/packages/kbn-pm/src/commands/target", - "version": "1.0.0", - }, - "bar" => Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isSinglePackageJsonProject": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - }, - Map { - "kibana" => Array [ - Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isSinglePackageJsonProject": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - ], - "bar" => Array [], - }, - ], -] -`; - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: script 1`] = ` -Array [ - Array [ - Object { - "args": Array [], - "debug": undefined, - "pkg": Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isSinglePackageJsonProject": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - "script": "kbn:bootstrap", - }, - ], -] -`; - -exports[`does not run installer if no deps in package: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], -] -`; - -exports[`handles "frozen-lockfile": install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [ - "--frozen-lockfile", - ], - ], -] -`; - -exports[`handles dependencies of dependencies: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], -] -`; diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts deleted file mode 100644 index dbd5278d283b6c..00000000000000 --- a/packages/kbn-pm/src/commands/bootstrap.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('../utils/scripts'); -jest.mock('../utils/link_project_executables'); -jest.mock('../utils/validate_dependencies'); - -import { resolve } from 'path'; - -import { ToolingLogCollectingWriter } from '@kbn/dev-utils/tooling_log'; - -import { absolutePathSnapshotSerializer, stripAnsiSnapshotSerializer } from '../test_helpers'; -import { linkProjectExecutables } from '../utils/link_project_executables'; -import { IPackageJson } from '../utils/package_json'; -import { Project } from '../utils/project'; -import { buildProjectGraph } from '../utils/projects'; -import { installInDir, runScriptInPackageStreaming } from '../utils/scripts'; -import { BootstrapCommand } from './bootstrap'; -import { Kibana } from '../utils/kibana'; -import { log } from '../utils/log'; - -const mockInstallInDir = installInDir as jest.Mock; -const mockRunScriptInPackageStreaming = runScriptInPackageStreaming as jest.Mock; -const mockLinkProjectExecutables = linkProjectExecutables as jest.Mock; - -const logWriter = new ToolingLogCollectingWriter('debug'); -log.setLogLevel('silent'); -log.setWriters([logWriter]); -beforeEach(() => { - logWriter.messages.length = 0; -}); - -const createProject = (packageJson: IPackageJson, path = '.') => { - const project = new Project( - { - name: 'kibana', - version: '1.0.0', - ...packageJson, - }, - resolve(__dirname, path) - ); - - return project; -}; -expect.addSnapshotSerializer(absolutePathSnapshotSerializer); -expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); - -afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); -}); - -test('handles dependencies of dependencies', async () => { - const kibana = createProject({ - dependencies: { - bar: 'link:packages/bar', - }, - }); - const foo = createProject( - { - dependencies: { - bar: 'link:../bar', - }, - name: 'foo', - }, - 'packages/foo' - ); - const bar = createProject( - { - dependencies: { - baz: 'link:../baz', - }, - name: 'bar', - }, - 'packages/bar' - ); - const baz = createProject( - { - name: 'baz', - }, - 'packages/baz' - ); - - const projects = new Map([ - ['kibana', kibana], - ['foo', foo], - ['bar', bar], - ['baz', baz], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - ] - `); -}); - -test('does not run installer if no deps in package', async () => { - const kibana = createProject({ - dependencies: { - bar: 'link:packages/bar', - }, - }); - // bar has no dependencies - const bar = createProject( - { - name: 'bar', - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - ] - `); -}); - -test('handles "frozen-lockfile"', async () => { - const kibana = createProject({ - dependencies: { - foo: '2.2.0', - }, - }); - - const projects = new Map([['kibana', kibana]]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: { - 'frozen-lockfile': true, - }, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); -}); - -test('calls "kbn:bootstrap" scripts and links executables after installing deps', async () => { - const kibana = createProject({ - dependencies: { - bar: 'link:packages/bar', - }, - }); - const bar = createProject( - { - name: 'bar', - scripts: { - 'kbn:bootstrap': 'node ./bar.js', - }, - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockLinkProjectExecutables.mock.calls).toMatchSnapshot('link bins'); - expect(mockRunScriptInPackageStreaming.mock.calls).toMatchSnapshot('script'); -}); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index c47edfb9cf63df..d17b597eb6648c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -52,8 +52,10 @@ EXPOSE 5601 {{/ubi}} RUN for iter in {1..10}; do \ - # update microdnf to have exclusion feature for dnf configuration - {{packageManager}} update microdnf --setopt=tsflags=nodocs -y && \ + {{#ubi}} + # update microdnf to have exclusion feature for dnf configuration + {{packageManager}} update microdnf --setopt=tsflags=nodocs -y && \ + {{/ubi}} {{packageManager}} update --setopt=tsflags=nodocs -y && \ {{packageManager}} install --setopt=tsflags=nodocs -y \ fontconfig freetype shadow-utils libnss3.so {{#ubi}}findutils{{/ubi}} && \ diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 284a6b4b696aeb..aae9b89cdc61fe 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -215,13 +215,13 @@ export class IndexPatternsService { * Get field list by providing { pattern } * @param options */ - getFieldsForWildcard = async (options: GetFieldsOptions) => { + getFieldsForWildcard = async (options: GetFieldsOptions = {}) => { const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); return this.apiClient.getFieldsForWildcard({ pattern: options.pattern, metaFields, type: options.type, - rollupIndex: options.rollupIndex, + params: options.params || {}, }); }; @@ -231,13 +231,13 @@ export class IndexPatternsService { */ getFieldsForIndexPattern = async ( indexPattern: IndexPattern | IndexPatternSpec, - options?: GetFieldsOptions + options: GetFieldsOptions = {} ) => this.getFieldsForWildcard({ - type: indexPattern.type, - rollupIndex: indexPattern?.typeMeta?.params?.rollup_index, - ...options, pattern: indexPattern.title as string, + ...options, + type: indexPattern.type, + params: indexPattern.typeMeta && indexPattern.typeMeta.params, }); /** @@ -374,10 +374,10 @@ export class IndexPatternsService { try { spec.fields = isFieldRefreshRequired ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { - pattern: title as string, + pattern: title, metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), type, - rollupIndex: typeMeta?.params?.rollupIndex, + params: typeMeta && typeMeta.params, }) : spec.fields; } catch (err) { diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index b381cc0963333f..3387bc3b3c19e4 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -86,22 +86,15 @@ export interface SavedObjectsClientCommon { } export interface GetFieldsOptions { - pattern: string; + pattern?: string; type?: string; + params?: any; lookBack?: boolean; metaFields?: string[]; - rollupIndex?: string; -} - -export interface GetFieldsOptionsTimePattern { - pattern: string; - metaFields: string[]; - lookBack: number; - interval: string; } export interface IIndexPatternsApiClient { - getFieldsForTimePattern: (options: GetFieldsOptionsTimePattern) => Promise; + getFieldsForTimePattern: (options: GetFieldsOptions) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; } diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts index 8c48ee44fba9c4..37ee80c2c29e4a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts @@ -32,12 +32,7 @@ describe('IndexPatternsApiClient', () => { test('uses the right URI to fetch fields for time patterns', async function () { const expectedPath = '/api/index_patterns/_fields_for_time_pattern'; - await indexPatternsApiClient.getFieldsForTimePattern({ - pattern: 'blah', - metaFields: [], - lookBack: 5, - interval: '', - }); + await indexPatternsApiClient.getFieldsForTimePattern(); expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); }); @@ -45,7 +40,15 @@ describe('IndexPatternsApiClient', () => { test('uses the right URI to fetch fields for wildcard', async function () { const expectedPath = '/api/index_patterns/_fields_for_wildcard'; - await indexPatternsApiClient.getFieldsForWildcard({ pattern: 'blah' }); + await indexPatternsApiClient.getFieldsForWildcard(); + + expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); + }); + + test('uses the right URI to fetch fields for wildcard given a type', async function () { + const expectedPath = '/api/index_patterns/rollup/_fields_for_wildcard'; + + await indexPatternsApiClient.getFieldsForWildcard({ type: 'rollup' }); expect(fetchSpy).toHaveBeenCalledWith(expectedPath, expect.any(Object)); }); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index ca0f35d6612b2e..377a3f7f91a50a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -19,11 +19,7 @@ import { HttpSetup } from 'src/core/public'; import { IndexPatternMissingIndices } from '../../../common/index_patterns/lib'; -import { - GetFieldsOptions, - IIndexPatternsApiClient, - GetFieldsOptionsTimePattern, -} from '../../../common/index_patterns/types'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../../../common/index_patterns/types'; const API_BASE_URL: string = `/api/index_patterns/`; @@ -52,7 +48,7 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { return API_BASE_URL + path.filter(Boolean).map(encodeURIComponent).join('/'); } - getFieldsForTimePattern(options: GetFieldsOptionsTimePattern) { + getFieldsForTimePattern(options: GetFieldsOptions = {}) { const { pattern, lookBack, metaFields } = options; const url = this._getUrl(['_fields_for_time_pattern']); @@ -64,12 +60,27 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { }).then((resp: any) => resp.fields); } - getFieldsForWildcard({ pattern, metaFields, type, rollupIndex }: GetFieldsOptions) { - return this._request(this._getUrl(['_fields_for_wildcard']), { - pattern, - meta_fields: metaFields, - type, - rollup_index: rollupIndex, - }).then((resp: any) => resp.fields); + getFieldsForWildcard(options: GetFieldsOptions = {}) { + const { pattern, metaFields, type, params } = options; + + let url; + let query; + + if (type) { + url = this._getUrl([type, '_fields_for_wildcard']); + query = { + pattern, + meta_fields: metaFields, + params: JSON.stringify(params), + }; + } else { + url = this._getUrl(['_fields_for_wildcard']); + query = { + pattern, + meta_fields: metaFields, + }; + } + + return this._request(url, query).then((resp: any) => resp.fields); } } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index f911378ce97b22..ac8c9bec30d17d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1339,9 +1339,9 @@ export class IndexPatternsService { // (undocumented) getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; - getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts - getFieldsForWildcard: (options: GetFieldsOptions) => Promise; + getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; getIdsWithTitle: (refresh?: boolean) => Promise { - const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; - const fieldCapsResponse = await getFieldCapabilities( - this.elasticsearchClient, - pattern, - metaFields, - { - allow_no_indices: fieldCapsOptions - ? fieldCapsOptions.allow_no_indices - : this.allowNoIndices, - } - ); - if (type === 'rollup' && rollupIndex) { - const rollupFields: FieldDescriptor[] = []; - const rollupIndexCapabilities = getCapabilitiesForRollupIndices( - ( - await this.elasticsearchClient.rollup.getRollupIndexCaps({ - index: rollupIndex, - }) - ).body - )[rollupIndex].aggs; - const fieldCapsResponseObj = keyBy(fieldCapsResponse, 'name'); - - // Keep meta fields - metaFields!.forEach( - (field: string) => - fieldCapsResponseObj[field] && rollupFields.push(fieldCapsResponseObj[field]) - ); - - return mergeCapabilitiesWithFields( - rollupIndexCapabilities, - fieldCapsResponseObj, - rollupFields - ); - } - return fieldCapsResponse; + const { pattern, metaFields, fieldCapsOptions } = options; + return await getFieldCapabilities(this.elasticsearchClient, pattern, metaFields, { + allow_no_indices: fieldCapsOptions ? fieldCapsOptions.allow_no_indices : this.allowNoIndices, + }); } /** diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js b/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js deleted file mode 100644 index d675702ae54e93..00000000000000 --- a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { jobs } from './jobs'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/index.ts b/src/plugins/data/server/index_patterns/fetcher/lib/index.ts index b2fd3a1a09a251..20e74d2b1a579d 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/index.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/index.ts @@ -20,5 +20,3 @@ export { getFieldCapabilities, shouldReadFieldFromDocValues } from './field_capabilities'; export { resolveTimePattern } from './resolve_time_pattern'; export { createNoMatchingIndicesError } from './errors'; -export * from './merge_capabilities_with_fields'; -export * from './map_capabilities'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts deleted file mode 100644 index 61871748340128..00000000000000 --- a/src/plugins/data/server/index_patterns/fetcher/lib/map_capabilities.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mergeJobConfigurations } from './jobs_compatibility'; - -export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) { - const indexNames = Object.keys(indices); - const capabilities = {} as { [key: string]: any }; - - indexNames.forEach((index) => { - try { - capabilities[index] = mergeJobConfigurations(indices[index].rollup_jobs); - } catch (e) { - capabilities[index] = { - error: e.message, - }; - } - }); - - return capabilities; -} diff --git a/src/plugins/data/server/index_patterns/index.ts b/src/plugins/data/server/index_patterns/index.ts index 3305b1bb9a92f1..683d1c445fd72e 100644 --- a/src/plugins/data/server/index_patterns/index.ts +++ b/src/plugins/data/server/index_patterns/index.ts @@ -17,11 +17,5 @@ * under the License. */ export * from './utils'; -export { - IndexPatternsFetcher, - FieldDescriptor, - shouldReadFieldFromDocValues, - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from './fetcher'; +export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues } from './fetcher'; export { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns_service'; diff --git a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts index 21a3bf6e73e611..2dc6f40c5a6f1f 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts @@ -17,30 +17,13 @@ * under the License. */ -import { ElasticsearchClient } from 'kibana/server'; -import { - GetFieldsOptions, - IIndexPatternsApiClient, - GetFieldsOptionsTimePattern, -} from '../../common/index_patterns/types'; -import { IndexPatternsFetcher } from './fetcher'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../../common/index_patterns/types'; export class IndexPatternsApiServer implements IIndexPatternsApiClient { - esClient: ElasticsearchClient; - constructor(elasticsearchClient: ElasticsearchClient) { - this.esClient = elasticsearchClient; + async getFieldsForTimePattern(options: GetFieldsOptions = {}) { + throw new Error('IndexPatternsApiServer - getFieldsForTimePattern not defined'); } - async getFieldsForWildcard({ pattern, metaFields, type, rollupIndex }: GetFieldsOptions) { - const indexPatterns = new IndexPatternsFetcher(this.esClient); - return await indexPatterns.getFieldsForWildcard({ - pattern, - metaFields, - type, - rollupIndex, - }); - } - async getFieldsForTimePattern(options: GetFieldsOptionsTimePattern) { - const indexPatterns = new IndexPatternsFetcher(this.esClient); - return await indexPatterns.getFieldsForTimePattern(options); + async getFieldsForWildcard(options: GetFieldsOptions = {}) { + throw new Error('IndexPatternsApiServer - getFieldsForWildcard not defined'); } } diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index af2d4d6a73e0f8..d665e3715fa72a 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -17,14 +17,7 @@ * under the License. */ -import { - CoreSetup, - CoreStart, - Plugin, - Logger, - SavedObjectsClientContract, - ElasticsearchClient, -} from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin, Logger, SavedObjectsClientContract } from 'kibana/server'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; @@ -36,8 +29,7 @@ import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient + savedObjectsClient: SavedObjectsClientContract ) => Promise; } @@ -58,17 +50,14 @@ export class IndexPatternsService implements Plugin { + indexPatternsServiceFactory: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); return new IndexPatternsCommonService({ uiSettings: new UiSettingsServerToCommon(uiSettingsClient), savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient), + apiClient: new IndexPatternsApiServer(), fieldFormats: formats, onError: (error) => { logger.error(error); diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index f8af52954fc61e..041eb235d01e08 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -42,15 +42,13 @@ export function registerRoutes(http: HttpServiceSetup) { meta_fields: schema.oneOf([schema.string(), schema.arrayOf(schema.string())], { defaultValue: [], }), - type: schema.maybe(schema.string()), - rollup_index: schema.maybe(schema.string()), }), }, }, async (context, request, response) => { const { asCurrentUser } = context.core.elasticsearch.client; const indexPatterns = new IndexPatternsFetcher(asCurrentUser); - const { pattern, meta_fields: metaFields, type, rollup_index: rollupIndex } = request.query; + const { pattern, meta_fields: metaFields } = request.query; let parsedFields: string[] = []; try { @@ -63,8 +61,6 @@ export function registerRoutes(http: HttpServiceSetup) { const fields = await indexPatterns.getFieldsForWildcard({ pattern, metaFields: parsedFields, - type, - rollupIndex, }); return response.ok({ diff --git a/src/plugins/data/server/search/aggs/aggs_service.test.ts b/src/plugins/data/server/search/aggs/aggs_service.test.ts index e58420f6c2f071..cb4239cc339c45 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { KibanaRequest, ElasticsearchClient } from 'src/core/server'; +import { KibanaRequest } from 'src/core/server'; import { coreMock } from '../../../../../core/server/mocks'; import { expressionsPluginMock } from '../../../../../plugins/expressions/server/mocks'; @@ -63,8 +63,7 @@ describe('AggsService - server', () => { expect(start).toHaveProperty('asScopedToClient'); const contract = await start.asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient + savedObjects.getScopedClient({} as KibanaRequest) ); expect(contract).toHaveProperty('calculateAutoTimeExpression'); expect(contract).toHaveProperty('createAggConfigs'); @@ -75,10 +74,7 @@ describe('AggsService - server', () => { service.setup(setupDeps); const start = await service .start(startDeps) - .asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient - ); + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); expect(start.types.get('terms').name).toBe('terms'); }); @@ -87,10 +83,7 @@ describe('AggsService - server', () => { service.setup(setupDeps); const start = await service .start(startDeps) - .asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient - ); + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length); @@ -110,10 +103,7 @@ describe('AggsService - server', () => { const start = await service .start(startDeps) - .asScopedToClient( - savedObjects.getScopedClient({} as KibanaRequest), - {} as ElasticsearchClient - ); + .asScopedToClient(savedObjects.getScopedClient({} as KibanaRequest)); const aggTypes = getAggTypes(); expect(start.types.getAll().buckets.length).toBe(aggTypes.buckets.length + 1); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index c23f748b1eeb53..c805c8af6694cf 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -19,11 +19,7 @@ import { pick } from 'lodash'; -import { - UiSettingsServiceStart, - SavedObjectsClientContract, - ElasticsearchClient, -} from 'src/core/server'; +import { UiSettingsServiceStart, SavedObjectsClientContract } from 'src/core/server'; import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; import { AggsCommonService, @@ -69,10 +65,7 @@ export class AggsService { public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { return { - asScopedToClient: async ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ) => { + asScopedToClient: async (savedObjectsClient: SavedObjectsClientContract) => { const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); @@ -89,9 +82,8 @@ export class AggsService { types, } = this.aggsCommonService.start({ getConfig, - getIndexPattern: ( - await indexPatterns.indexPatternsServiceFactory(savedObjectsClient, elasticsearchClient) - ).get, + getIndexPattern: (await indexPatterns.indexPatternsServiceFactory(savedObjectsClient)) + .get, isDefaultTimezone, }); diff --git a/src/plugins/data/server/search/aggs/types.ts b/src/plugins/data/server/search/aggs/types.ts index 2c28c970cbb843..1b21d948b25d90 100644 --- a/src/plugins/data/server/search/aggs/types.ts +++ b/src/plugins/data/server/search/aggs/types.ts @@ -17,14 +17,11 @@ * under the License. */ -import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AggsCommonSetup, AggsStart as Start } from '../../../common'; export type AggsSetup = AggsCommonSetup; export interface AggsStart { - asScopedToClient: ( - savedObjectsClient: SavedObjectsClientContract, - elasticsearchClient: ElasticsearchClient - ) => Promise; + asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => Promise; } diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6394c37c993b37..04ee0e95c7f08f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -149,11 +149,7 @@ export class SearchService implements Plugin { { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { return { - aggs: this.aggsService.start({ - fieldFormats, - uiSettings, - indexPatterns, - }), + aggs: this.aggsService.start({ fieldFormats, uiSettings, indexPatterns }), getSearchStrategy: this.getSearchStrategy, search: this.search.bind(this), searchSource: { @@ -161,8 +157,7 @@ export class SearchService implements Plugin { const esClient = elasticsearch.client.asScoped(request); const savedObjectsClient = savedObjects.getScopedClient(request); const scopedIndexPatterns = await indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - esClient.asCurrentUser + savedObjectsClient ); const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 06c8a053e5ad2e..fba86098a76fa3 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -15,8 +15,7 @@ import { CoreStart } from 'src/core/server'; import { CoreStart as CoreStart_2 } from 'kibana/server'; import { DatatableColumn } from 'src/plugins/expressions'; import { Duration } from 'moment'; -import { ElasticsearchClient } from 'src/core/server'; -import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; +import { ElasticsearchClient } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; @@ -361,15 +360,6 @@ export type Filter = { query?: any; }; -// Warning: (ae-missing-release-tag) "getCapabilitiesForRollupIndices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function getCapabilitiesForRollupIndices(indices: { - [key: string]: any; -}): { - [key: string]: any; -}; - // Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -669,7 +659,7 @@ export const indexPatterns: { // // @public (undocumented) export class IndexPatternsFetcher { - constructor(elasticsearchClient: ElasticsearchClient_2, allowNoIndices?: boolean); + constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices?: boolean); getFieldsForTimePattern(options: { pattern: string; metaFields: string[]; @@ -682,8 +672,6 @@ export class IndexPatternsFetcher { fieldCapsOptions?: { allow_no_indices: boolean; }; - type?: string; - rollupIndex?: string; }): Promise; } @@ -698,7 +686,7 @@ export class IndexPatternsService implements Plugin_3 Promise; + indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2) => Promise; }; } @@ -800,15 +788,6 @@ export interface KueryNode { type: keyof NodeTypes; } -// Warning: (ae-missing-release-tag) "mergeCapabilitiesWithFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const mergeCapabilitiesWithFields: (rollupIndexCapabilities: { - [key: string]: any; -}, fieldsFromFieldCapsApi: { - [key: string]: any; -}, previousFields?: FieldDescriptor[]) => FieldDescriptor[]; - // Warning: (ae-missing-release-tag) "METRIC_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -912,7 +891,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick) => Promise; }; search: ISearchStart>; }; @@ -1168,22 +1147,22 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:256:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:257:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:272:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:280:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:234:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:249:5 - (ae-forgotten-export) The symbol "getTotalLoaded" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:5 - (ae-forgotten-export) The symbol "toSnakeCase" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:255:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:265:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:278:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:91:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 91d66ce8cbf81a..57d1d715151b74 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -35,7 +35,7 @@ export function createTheme( { token: '', foreground: euiTheme.euiColorDarkestShade, - background: euiTheme.euiColorEmptyShade, + background: euiTheme.euiFormBackgroundColor, }, { token: 'invalid', foreground: euiTheme.euiColorAccent }, { token: 'emphasis', fontStyle: 'italic' }, @@ -94,7 +94,7 @@ export function createTheme( ], colors: { 'editor.foreground': euiTheme.euiColorDarkestShade, - 'editor.background': euiTheme.euiColorEmptyShade, + 'editor.background': euiTheme.euiFormBackgroundColor, 'editorLineNumber.foreground': euiTheme.euiColorDarkShade, 'editorLineNumber.activeForeground': euiTheme.euiColorDarkShade, 'editorIndentGuide.background': euiTheme.euiColorLightShade, diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json new file mode 100644 index 00000000000000..d211a70f12df33 --- /dev/null +++ b/src/plugins/security_oss/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*"], + "references": [{ "path": "../../core/tsconfig.json" }] +} diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 5a853972d34a82..2ac3de510f8aee 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -8,7 +8,13 @@ To integrate with the telemetry services for usage collection of your feature, t ## Creating and Registering Usage Collector -All you need to provide is a `type` for organizing your fields, `schema` field to define the expected types of usage fields reported, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it. +Your usage collector needs to provide +- a `type` for organizing your fields, +- `schema` field to define the expected types of usage fields reported, +- a `fetch` method for returning your usage data, and +- an `isReady` method (that returns true or false) for letting the telemetry service know if it needs to wait for any asynchronous action (initialization of clients or other services) before calling the `fetch` method. + +Then you need to make the Telemetry service aware of the collector by registering it. 1. Make sure `usageCollection` is in your optional Plugins: @@ -62,6 +68,8 @@ All you need to provide is a `type` for organizing your fields, `schema` field t total: 'long', }, }, + isReady: () => isCollectorFetchReady, // Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. + fetch: async (collectorFetchContext: CollectorFetchContext) => { // query ES or saved objects and get some data @@ -84,6 +92,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t Some background: - `MY_USAGE_TYPE` can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector. + +- `isReady` (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the `fetch` method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its `fetch` method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns `true` for `isReady` and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your `fetch` method can run without the need of any previous dependencies, then you can return true for `isReady` as shown in the example below. + - The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection. In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. The `fetch` method also exposes the saved objects client that will have the correct scope when the collectors' `fetch` method is called. @@ -154,7 +165,7 @@ If any of your properties is an array, the schema definition must follow the con ```ts export const myCollector = makeUsageCollector({ type: 'my_working_collector', - isReady: () => true, + isReady: () => true, // `fetch` doesn't require any validation for dependencies to be met fetch() { return { my_greeting: 'hello', diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 73febc0183fc55..c04b087d4adf56 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -72,7 +72,7 @@ export interface CollectorOptions { type: string; init?: Function; /** - * Method to return `true`/`false` to confirm if the collector is ready for the `fetch` method to be called. + * Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. */ isReady: () => Promise | boolean; /** @@ -101,6 +101,7 @@ export class Collector { * @param {Function} options.init (optional) - initialization function * @param {Function} options.fetch - function to query data * @param {Function} options.formatForBulkUpload - optional + * @param {Function} options.isReady - method that returns a boolean or Promise of a boolean to indicate the collector is ready to report data * @param {Function} options.rest - optional other properties */ constructor( diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index dc49e280a2bb73..b52188129f77f1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -62,12 +62,10 @@ export async function getFields( let indexPatternString = indexPattern; if (!indexPatternString) { - const [{ savedObjects, elasticsearch }, { data }] = await framework.core.getStartServices(); + const [{ savedObjects }, { data }] = await framework.core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(request); - const clusterClient = elasticsearch.client.asScoped(request).asCurrentUser; const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - clusterClient + savedObjectsClient ); const defaultIndexPattern = await indexPatternsService.getDefault(); indexPatternString = get(defaultIndexPattern, 'title', ''); diff --git a/tasks/config/run.js b/tasks/config/run.js index 7814f4aa0e2241..0a1bb9617e1f94 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -17,8 +17,6 @@ * under the License. */ -import { getFunctionalTestGroupRunConfigs } from '../function_test_groups'; - const { version } = require('../../package.json'); const KIBANA_INSTALL_DIR = process.env.KIBANA_INSTALL_DIR || @@ -239,9 +237,5 @@ module.exports = function () { 'test:jest_integration' ), test_projects: gruntTaskWithGithubChecks('Project tests', 'test:projects'), - - ...getFunctionalTestGroupRunConfigs({ - kibanaInstallDir: KIBANA_INSTALL_DIR, - }), }; }; diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 7dafc03cfab032..0b456dcb0da13b 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -29,44 +29,6 @@ const TEST_TAGS = safeLoad(JOBS_YAML) .JOB.filter((id) => id.startsWith('kibana-ciGroup')) .map((id) => id.replace(/^kibana-/, '')); -const getDefaultArgs = (tag) => { - return [ - 'scripts/functional_tests', - '--include-tag', - tag, - '--config', - 'test/functional/config.js', - '--config', - 'test/ui_capabilities/newsfeed_err/config.ts', - // '--config', 'test/functional/config.firefox.js', - '--bail', - '--debug', - '--config', - 'test/new_visualize_flow/config.ts', - '--config', - 'test/security_functional/config.ts', - ]; -}; - -export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) { - return { - // include a run task for each test group - ...TEST_TAGS.reduce( - (acc, tag) => ({ - ...acc, - [`functionalTests_${tag}`]: { - cmd: process.execPath, - args: [ - ...getDefaultArgs(tag), - ...(!!process.env.CODE_COVERAGE ? [] : ['--kibana-install-dir', kibanaInstallDir]), - ], - }, - }), - {} - ), - }; -} - grunt.registerTask( 'functionalTests:ensureAllTestsInCiGroup', 'Check that all of the functional tests are in a CI group', diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index ca3e26be5d74b9..e29f75b8065744 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -350,7 +350,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider if (timefield) { await this.selectTimeFieldOption(timefield); } - await new Promise((r) => setTimeout(r, 5000 * 60)); await (await this.getCreateIndexPatternButton()).click(); }); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/test/plugin_functional/plugins/data_search/server/plugin.ts b/test/plugin_functional/plugins/data_search/server/plugin.ts index ca22e82188403a..e016ef56802f3b 100644 --- a/test/plugin_functional/plugins/data_search/server/plugin.ts +++ b/test/plugin_functional/plugins/data_search/server/plugin.ts @@ -58,16 +58,14 @@ export class DataSearchTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const service = await data.search.searchSource.asScoped(req); - const clusterClient = elasticsearch.client.asScoped(req).asCurrentUser; const savedObjectsClient = savedObjects.getScopedClient(req); // Since the index pattern ID can change on each test run, we need // to look it up on the fly and insert it into the request. const indexPatterns = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - clusterClient + savedObjectsClient ); const ids = await indexPatterns.getIds(); // @ts-expect-error Force overwriting the request diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index 7dc5e975c528e6..a54502b7402115 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -36,34 +36,12 @@ export class IndexPatternsTestPlugin public setup(core: CoreSetup) { const router = core.http.createRouter(); - router.post( - { - path: '/api/index-patterns-plugin/create', - validate: { - body: schema.object({}, { unknowns: 'allow' }), - }, - }, - async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); - const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); - const ids = await service.createAndSave(req.body); - return res.ok({ body: ids }); - } - ); - router.get( { path: '/api/index-patterns-plugin/get-all', validate: false }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ids = await service.getIds(); return res.ok({ body: ids }); } @@ -80,12 +58,9 @@ export class IndexPatternsTestPlugin }, async (context, req, res) => { const id = (req.params as Record).id; - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); return res.ok({ body: ip.toSpec() }); } @@ -101,13 +76,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); const ip = await service.get(id); await service.updateSavedObject(ip); return res.ok(); @@ -124,13 +96,10 @@ export class IndexPatternsTestPlugin }, }, async (context, req, res) => { - const [{ savedObjects, elasticsearch }, { data }] = await core.getStartServices(); + const [{ savedObjects }, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const savedObjectsClient = savedObjects.getScopedClient(req); - const service = await data.indexPatterns.indexPatternsServiceFactory( - savedObjectsClient, - elasticsearch.client.asScoped(req).asCurrentUser - ); + const service = await data.indexPatterns.indexPatternsServiceFactory(savedObjectsClient); await service.delete(id); return res.ok(); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 6d8f65fa38777b..2c846dc7803115 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -25,30 +25,13 @@ export default function ({ getService }: PluginFunctionalProviderContext) { // skipping the tests as it deletes index patterns created by other test causing unexpected failures // https://github.com/elastic/kibana/issues/79886 - describe('index patterns', function () { + describe.skip('index patterns', function () { let indexPatternId = ''; - it('can create an index pattern', async () => { - const title = 'shakes*'; - const fieldFormats = { bytes: { id: 'bytes' } }; - const body = await ( - await supertest - .post('/api/index-patterns-plugin/create') - .set('kbn-xsrf', 'anything') - .send({ title, fieldFormats }) - .expect(200) - ).body; - - indexPatternId = body.id; - expect(body.id).not.empty(); - expect(body.title).to.equal(title); - expect(body.fields.length).to.equal(15); - expect(body.fieldFormatMap).to.eql(fieldFormats); - }); - it('can get all ids', async () => { const body = await (await supertest.get('/api/index-patterns-plugin/get-all').expect(200)) .body; + indexPatternId = body[0]; expect(body.length > 0).to.equal(true); }); diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 2542d7032e83bf..f9e9d40cd8b0d2 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -3,7 +3,13 @@ source test/scripts/jenkins_test_setup_oss.sh if [[ -z "$CODE_COVERAGE" ]]; then - checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; + echo " -> Running functional and api tests" + + checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --include-tag "ciGroup$CI_GROUP" if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then source test/scripts/jenkins_build_kbn_sample_panel_action.sh @@ -21,7 +27,6 @@ else cd "kibana${CI_GROUP}" echo " -> running tests from the clone folder" - #yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}"; node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; if [[ -d target/kibana-coverage/functional ]]; then diff --git a/tsconfig.json b/tsconfig.json index 3554027d4e3203..00b33bd0b44511 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", "src/plugins/newsfeed/**/*", + "src/plugins/security_oss/**/*", "src/plugins/share/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", @@ -34,6 +35,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 47655da68f2a50..55d63f516b998f 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -9,6 +9,7 @@ { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, + { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 28eb94405abbb6..521637a1b0f7f4 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -109,16 +109,16 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { def functionalTestProcess(String name, Closure closure) { return { - withFunctionalTestEnv(["JOB=${name}"], closure) + notifyOnError { + withFunctionalTestEnv(["JOB=${name}"], closure) + } } } def functionalTestProcess(String name, String script) { return functionalTestProcess(name) { - notifyOnError { - retryable(name) { - runbld(script, "Execute ${name}") - } + retryable(name) { + runbld(script, "Execute ${name}") } } } diff --git a/x-pack/plugins/canvas/public/components/expression/expression.scss b/x-pack/plugins/canvas/public/components/expression/expression.scss index 1635446a74012c..da95eca2b4f612 100644 --- a/x-pack/plugins/canvas/public/components/expression/expression.scss +++ b/x-pack/plugins/canvas/public/components/expression/expression.scss @@ -45,4 +45,5 @@ .canvasExpression__settings { padding: $euiSizeM $euiSize; border-top: $euiBorderThin; + background-color: $euiColorEmptyShade; } diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss index 41bc718dcfec1a..c46a2ec7a1e220 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.scss @@ -6,7 +6,7 @@ position: absolute; top: $euiSize * -1.25; left: 50%; - background-color: $euiColorLightestShade; + background-color: $euiFormBackgroundColor; margin: 0; border-radius: $euiBorderRadius $euiBorderRadius 0 0; diff --git a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss index 34a9a12aac3e62..0ed47a761cd4fb 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss +++ b/x-pack/plugins/canvas/public/components/toolbar/tray/tray.scss @@ -4,11 +4,7 @@ } .canvasTray__panel { - background-color: $euiPageBackgroundColor; + background-color: $euiFormBackgroundColor; border-radius: 0; - - &.canvasTray__panel--holdingExpression { - background-color: $euiColorEmptyShade; - } } diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 5bd15ce411002b..72703843f4bab1 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -207,7 +207,7 @@ describe('callEnterpriseSearchConfigAPI', () => { callEnterpriseSearchConfigAPI(mockDependencies); jest.advanceTimersByTime(150); expect(mockDependencies.log.warn).toHaveBeenCalledWith( - 'Enterprise Search access check took over 100ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.' + 'Enterprise Search access check took over 100ms. Please ensure your Enterprise Search server is responding normally and not adversely impacting Kibana load speeds.' ); // Timeout diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index dcc696f6d01e25..325d7b0ce48f93 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -38,7 +38,7 @@ export const callEnterpriseSearchConfigAPI = async ({ }: IParams): Promise => { if (!config.host) return {}; - const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.`; + const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is responding normally and not adversely impacting Kibana load speeds.`; const TIMEOUT_MESSAGE = `Exceeded ${config.accessCheckTimeout}ms timeout while checking ${config.host}. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses.`; const CONNECTION_ERROR = 'Could not perform access check to Enterprise Search'; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts index b5f5ad2530a123..095c0ac2b6ab14 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts @@ -45,6 +45,7 @@ describe('log settings routes', () => { }); it('creates a request to enterprise search', () => { + mockRouter.callRoute({}); expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/log_settings', }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 4bda56ca5087c8..d82c7b092c38af 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -357,7 +357,7 @@ describe('Datatable Visualization', () => { datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ dataType: 'string', - isBucketed: true, + isBucketed: false, // <= make them metrics label: 'label', }); @@ -365,6 +365,7 @@ describe('Datatable Visualization', () => { { layers: [layer] }, frame.datasourceLayers ) as Ast; + const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns'); expect(tableArgs).toHaveLength(1); @@ -372,5 +373,61 @@ describe('Datatable Visualization', () => { columnIds: ['c', 'b'], }); }); + + it('returns no expression if the metric dimension is not defined', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: true, // move it from the metric to the break down by side + label: 'label', + }); + + const expression = datatableVisualization.toExpression( + { layers: [layer] }, + frame.datasourceLayers + ); + + expect(expression).toEqual(null); + }); + }); + + describe('#getErrorMessages', () => { + it('returns undefined if the datasource is missing a metric dimension', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: true, // move it from the metric to the break down by side + label: 'label', + }); + + const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame); + + expect(error).not.toBeDefined(); + }); + + it('returns undefined if the metric dimension is defined', () => { + const datasource = createMockDatasource('test'); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]); + datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({ + dataType: 'string', + isBucketed: false, // keep it a metric + label: 'label', + }); + + const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame); + + expect(error).not.toBeDefined(); + }); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 1464ae6988a2df..e0f6ae31719caa 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -6,7 +6,13 @@ import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; -import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types'; +import { + SuggestionRequest, + Visualization, + VisualizationSuggestion, + Operation, + DatasourcePublicAPI, +} from '../types'; import { LensIconChartDatatable } from '../assets/chart_datatable'; export interface LayerState { @@ -128,16 +134,13 @@ export const datatableVisualization: Visualization }, getConfiguration({ state, frame, layerId }) { - const layer = state.layers.find((l) => l.layerId === layerId); - if (!layer) { + const { sortedColumns, datasource } = + getDataSourceAndSortedColumns(state, frame.datasourceLayers, layerId) || {}; + + if (!sortedColumns) { return { groups: [] }; } - const datasource = frame.datasourceLayers[layer.layerId]; - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - return { groups: [ { @@ -146,7 +149,9 @@ export const datatableVisualization: Visualization defaultMessage: 'Break down by', }), layerId: state.layers[0].layerId, - accessors: sortedColumns.filter((c) => datasource.getOperationForColumnId(c)?.isBucketed), + accessors: sortedColumns.filter( + (c) => datasource!.getOperationForColumnId(c)?.isBucketed + ), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, dataTestSubj: 'lnsDatatable_column', @@ -158,7 +163,7 @@ export const datatableVisualization: Visualization }), layerId: state.layers[0].layerId, accessors: sortedColumns.filter( - (c) => !datasource.getOperationForColumnId(c)?.isBucketed + (c) => !datasource!.getOperationForColumnId(c)?.isBucketed ), supportsMoreColumns: true, filterOperations: (op) => !op.isBucketed, @@ -194,14 +199,19 @@ export const datatableVisualization: Visualization }; }, - toExpression(state, datasourceLayers, { title, description } = {}): Ast { - const layer = state.layers[0]; - const datasource = datasourceLayers[layer.layerId]; - const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); - // When we add a column it could be empty, and therefore have no order - const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); - const operations = sortedColumns - .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) + toExpression(state, datasourceLayers, { title, description } = {}): Ast | null { + const { sortedColumns, datasource } = + getDataSourceAndSortedColumns(state, datasourceLayers, state.layers[0].layerId) || {}; + + if ( + sortedColumns?.length && + sortedColumns.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed).length === 0 + ) { + return null; + } + + const operations = sortedColumns! + .map((columnId) => ({ columnId, operation: datasource!.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); return { @@ -232,4 +242,24 @@ export const datatableVisualization: Visualization ], }; }, + + getErrorMessages(state, frame) { + return undefined; + }, }; + +function getDataSourceAndSortedColumns( + state: DatatableVisualizationState, + datasourceLayers: Record, + layerId: string +) { + const layer = state.layers.find((l: LayerState) => l.layerId === layerId); + if (!layer) { + return undefined; + } + const datasource = datasourceLayers[layer.layerId]; + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + return { datasource, sortedColumns }; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 8b0334ab98c146..28ad6c531e2554 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -6,7 +6,7 @@ import { SavedObjectReference } from 'kibana/public'; import { Ast } from '@kbn/interpreter/common'; -import { Datasource, DatasourcePublicAPI, Visualization } from '../../types'; +import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types'; import { buildExpression } from './expression_helpers'; import { Document } from '../../persistence/saved_object_store'; import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; @@ -91,3 +91,29 @@ export async function persistedStateToExpression( datasourceLayers, }); } + +export const validateDatasourceAndVisualization = ( + currentDataSource: Datasource | null, + currentDatasourceState: unknown | null, + currentVisualization: Visualization | null, + currentVisualizationState: unknown | undefined, + frameAPI: FramePublicAPI +): + | Array<{ + shortMessage: string; + longMessage: string; + }> + | undefined => { + const datasourceValidationErrors = currentDatasourceState + ? currentDataSource?.getErrorMessages(currentDatasourceState) + : undefined; + + const visualizationValidationErrors = currentVisualizationState + ? currentVisualization?.getErrorMessages(currentVisualizationState, frameAPI) + : undefined; + + if (datasourceValidationErrors || visualizationValidationErrors) { + return [...(datasourceValidationErrors || []), ...(visualizationValidationErrors || [])]; + } + return undefined; +}; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index 63ee02ac0404dd..201c91ee916764 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -34,6 +34,7 @@ import { import { prependDatasourceExpression } from './expression_helpers'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +import { validateDatasourceAndVisualization } from './state_helpers'; const MAX_SUGGESTIONS_DISPLAYED = 5; @@ -61,11 +62,28 @@ const PreviewRenderer = ({ withLabel, ExpressionRendererComponent, expression, + hasError, }: { withLabel: boolean; - expression: string; + expression: string | null | undefined; ExpressionRendererComponent: ReactExpressionRendererType; + hasError: boolean; }) => { + const onErrorMessage = ( +
+ +
+ ); return (
- { - return ( -
- -
- ); - }} - /> + {!expression || hasError ? ( + onErrorMessage + ) : ( + { + return onErrorMessage; + }} + /> + )}
); }; @@ -112,6 +120,7 @@ const SuggestionPreview = ({ expression?: Ast | null; icon: IconType; title: string; + error?: boolean; }; ExpressionRenderer: ReactExpressionRendererType; selected: boolean; @@ -129,11 +138,12 @@ const SuggestionPreview = ({ data-test-subj="lnsSuggestion" onClick={onSelect} > - {preview.expression ? ( + {preview.expression || preview.error ? ( ) : ( @@ -170,47 +180,81 @@ export function SuggestionPanel({ ? stagedPreview.visualization.activeId : activeVisualizationId; - const { suggestions, currentStateExpression } = useMemo(() => { - const newSuggestions = getSuggestions({ - datasourceMap, - datasourceStates: currentDatasourceStates, - visualizationMap, - activeVisualizationId: currentVisualizationId, - visualizationState: currentVisualizationState, - }) - .map((suggestion) => ({ - ...suggestion, - previewExpression: preparePreviewExpression( - suggestion, - visualizationMap[suggestion.visualizationId], - datasourceMap, - currentDatasourceStates, - frame - ), - })) - .filter((suggestion) => !suggestion.hide) - .slice(0, MAX_SUGGESTIONS_DISPLAYED); - - const newStateExpression = - currentVisualizationState && currentVisualizationId - ? preparePreviewExpression( - { visualizationState: currentVisualizationState }, - visualizationMap[currentVisualizationId], + const { suggestions, currentStateExpression, currentStateError } = useMemo( + () => { + const newSuggestions = getSuggestions({ + datasourceMap, + datasourceStates: currentDatasourceStates, + visualizationMap, + activeVisualizationId: currentVisualizationId, + visualizationState: currentVisualizationState, + }) + .filter((suggestion) => !suggestion.hide) + .filter( + ({ + visualizationId, + visualizationState: suggestionVisualizationState, + datasourceState: suggestionDatasourceState, + datasourceId: suggetionDatasourceId, + }) => { + return ( + validateDatasourceAndVisualization( + suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, + suggestionDatasourceState, + visualizationMap[visualizationId], + suggestionVisualizationState, + frame + ) == null + ); + } + ) + .slice(0, MAX_SUGGESTIONS_DISPLAYED) + .map((suggestion) => ({ + ...suggestion, + previewExpression: preparePreviewExpression( + suggestion, + visualizationMap[suggestion.visualizationId], datasourceMap, currentDatasourceStates, frame - ) - : undefined; + ), + })); + + const validationErrors = validateDatasourceAndVisualization( + activeDatasourceId ? datasourceMap[activeDatasourceId] : null, + activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state, + currentVisualizationId ? visualizationMap[currentVisualizationId] : null, + currentVisualizationState, + frame + ); - return { suggestions: newSuggestions, currentStateExpression: newStateExpression }; + const newStateExpression = + currentVisualizationState && currentVisualizationId && !validationErrors + ? preparePreviewExpression( + { visualizationState: currentVisualizationState }, + visualizationMap[currentVisualizationId], + datasourceMap, + currentDatasourceStates, + frame + ) + : undefined; + + return { + suggestions: newSuggestions, + currentStateExpression: newStateExpression, + currentStateError: validationErrors, + }; + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - currentDatasourceStates, - currentVisualizationState, - currentVisualizationId, - datasourceMap, - visualizationMap, - ]); + [ + currentDatasourceStates, + currentVisualizationState, + currentVisualizationId, + activeDatasourceId, + datasourceMap, + visualizationMap, + ] + ); const context: ExecutionContextSearch = useMemo( () => ({ @@ -305,6 +349,7 @@ export function SuggestionPanel({ {currentVisualizationId && ( { expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); + it('should show an error message if validation on datasource does not pass', () => { + mockDatasource.getErrorMessages.mockReturnValue([ + { shortMessage: 'An error occurred', longMessage: 'An long description here' }, + ]); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should show an error message if validation on visualization does not pass', () => { + mockDatasource.getErrorMessages.mockReturnValue(undefined); + mockDatasource.getLayers.mockReturnValue(['first']); + mockVisualization.getErrorMessages.mockReturnValue([ + { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, + ]); + mockVisualization.toExpression.mockReturnValue('vis'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + + it('should show an error message if validation on both datasource and visualization do not pass', () => { + mockDatasource.getErrorMessages.mockReturnValue([ + { shortMessage: 'An error occurred', longMessage: 'An long description here' }, + ]); + mockDatasource.getLayers.mockReturnValue(['first']); + mockVisualization.getErrorMessages.mockReturnValue([ + { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, + ]); + mockVisualization.toExpression.mockReturnValue('vis'); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + instance = mount( + {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock, data: dataMock }} + /> + ); + + // EuiFlexItem duplicates internally the attribute, so we need to filter only the most inner one here + expect( + instance.find('[data-test-subj="configuration-failure-more-errors"]').last().text() + ).toEqual(' +1 error'); + expect(instance.find(expressionRendererMock)).toHaveLength(0); + }); + it('should show an error message if the expression fails to parse', () => { mockDatasource.toExpression.mockReturnValue('|||'); mockDatasource.getLayers.mockReturnValue(['first']); @@ -487,7 +613,7 @@ describe('workspace_panel', () => { /> ); - expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); + expect(instance.find('[data-test-subj="expression-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index c4235a5514a545..e79060fb773292 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -9,7 +9,16 @@ import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n/react'; import { Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiTextColor, + EuiButtonEmpty, + EuiLink, + EuiTitle, +} from '@elastic/eui'; import { CoreStart, CoreSetup } from 'kibana/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; import { @@ -42,6 +51,7 @@ import { import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { DropIllustration } from '../../../assets/drop_illustration'; import { getOriginalRequestErrorMessage } from '../../error_helper'; +import { validateDatasourceAndVisualization } from '../state_helpers'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -66,7 +76,7 @@ export interface WorkspacePanelProps { } interface WorkspaceState { - expressionBuildError: string | undefined; + expressionBuildError?: Array<{ shortMessage: string; longMessage: string }>; expandError: boolean; } @@ -124,26 +134,58 @@ export function WorkspacePanel({ ); const [localState, setLocalState] = useState({ - expressionBuildError: undefined as string | undefined, + expressionBuildError: undefined, expandError: false, }); const activeVisualization = activeVisualizationId ? visualizationMap[activeVisualizationId] : null; + + // Note: mind to all these eslint disable lines: the frameAPI will change too frequently + // and to prevent race conditions it is ok to leave them there. + + const configurationValidationError = useMemo( + () => + validateDatasourceAndVisualization( + activeDatasourceId ? datasourceMap[activeDatasourceId] : null, + activeDatasourceId && datasourceStates[activeDatasourceId]?.state, + activeVisualization, + visualizationState, + framePublicAPI + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [activeVisualization, visualizationState, activeDatasourceId, datasourceMap, datasourceStates] + ); + const expression = useMemo( () => { - try { - return buildExpression({ - visualization: activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - datasourceLayers: framePublicAPI.datasourceLayers, - }); - } catch (e) { - // Most likely an error in the expression provided by a datasource or visualization - setLocalState((s) => ({ ...s, expressionBuildError: e.toString() })); + if (!configurationValidationError) { + try { + return buildExpression({ + visualization: activeVisualization, + visualizationState, + datasourceMap, + datasourceStates, + datasourceLayers: framePublicAPI.datasourceLayers, + }); + } catch (e) { + const buildMessages = activeVisualization?.getErrorMessages( + visualizationState, + framePublicAPI + ); + const defaultMessage = { + shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', { + defaultMessage: 'An unexpected error occurred while preparing the chart', + }), + longMessage: e.toString(), + }; + // Most likely an error in the expression provided by a datasource or visualization + setLocalState((s) => ({ + ...s, + expressionBuildError: buildMessages ?? [defaultMessage], + })); + } } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -256,7 +298,7 @@ export function WorkspacePanel({ timefilter={plugins.data.query.timefilter.timefilter} onEvent={onEvent} setLocalState={setLocalState} - localState={localState} + localState={{ ...localState, configurationValidationError }} ExpressionRendererComponent={ExpressionRendererComponent} /> ); @@ -304,7 +346,9 @@ export const InnerVisualizationWrapper = ({ timefilter: TimefilterContract; onEvent: (event: ExpressionRendererEvent) => void; setLocalState: (dispatch: (prevState: WorkspaceState) => WorkspaceState) => void; - localState: WorkspaceState; + localState: WorkspaceState & { + configurationValidationError?: Array<{ shortMessage: string; longMessage: string }>; + }; ExpressionRendererComponent: ReactExpressionRendererType; }) => { const autoRefreshFetch$ = useMemo(() => timefilter.getAutoRefreshFetch$(), [timefilter]); @@ -326,6 +370,66 @@ export const InnerVisualizationWrapper = ({ ] ); + if (localState.configurationValidationError) { + let showExtraErrors = null; + if (localState.configurationValidationError.length > 1) { + if (localState.expandError) { + showExtraErrors = localState.configurationValidationError + .slice(1) + .map(({ longMessage }) => ( + + {longMessage} + + )); + } else { + showExtraErrors = ( + + { + setLocalState((prevState: WorkspaceState) => ({ + ...prevState, + expandError: !prevState.expandError, + })); + }} + > + {i18n.translate('xpack.lens.editorFrame.configurationFailureMoreErrors', { + defaultMessage: ` +{errors} {errors, plural, one {error} other {errors}}`, + values: { errors: localState.configurationValidationError.length - 1 }, + })} + + + ); + } + } + + return ( + + + + + + + + + + + + + {localState.configurationValidationError[0].longMessage} + + {showExtraErrors} + + ); + } + if (localState.expressionBuildError) { return ( @@ -338,10 +442,11 @@ export const InnerVisualizationWrapper = ({ defaultMessage="An error occurred in the expression" /> - {localState.expressionBuildError} + {localState.expressionBuildError[0].longMessage} ); } + return (
{ const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage; + return ( 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 7e85ce5ecef714..5ab410a1c0af25 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -53,6 +53,7 @@ export function createMockVisualization(): jest.Mocked { setDimension: jest.fn(), removeDimension: jest.fn(), + getErrorMessages: jest.fn((_state, _frame) => undefined), }; } @@ -92,6 +93,7 @@ export function createMockDatasource(id: string): DatasourceMock { // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called publicAPIMock, + getErrorMessages: jest.fn((_state) => undefined), }; } 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 1609ff1dbc80eb..a3f48b162475a3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -614,4 +614,178 @@ describe('IndexPattern Data Source', () => { }); }); }); + + describe('#getErrorMessages', () => { + it('should detect a missing reference in a layer', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); + expect(messages).toHaveLength(1); + expect(messages![0]).toEqual({ + shortMessage: 'Invalid reference.', + longMessage: 'Field "bytes" has an invalid reference.', + }); + }); + + it('should detect and batch missing references in a layer', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'memory', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); + expect(messages).toHaveLength(1); + expect(messages![0]).toEqual({ + shortMessage: 'Invalid references.', + longMessage: 'Fields "bytes", "memory" have invalid reference.', + }); + }); + + it('should detect and batch missing references in multiple layers', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'bytes', + }, + col2: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'memory', + }, + }, + }, + second: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'string', + isBucketed: false, + label: 'Foo', + operationType: 'count', // <= invalid + sourceField: 'source', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + const messages = indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState); + expect(messages).toHaveLength(2); + expect(messages).toEqual([ + { + shortMessage: 'Invalid references on Layer 1.', + longMessage: 'Layer 1 has invalid references in fields "bytes", "memory".', + }, + { + shortMessage: 'Invalid reference on Layer 2.', + longMessage: 'Layer 2 has an invalid reference in field "source".', + }, + ]); + }); + + it('should return no errors if all references are satified', () => { + const state = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + dataType: 'number', + isBucketed: false, + label: 'Foo', + operationType: 'document', + sourceField: 'bytes', + }, + }, + }, + }, + currentIndexPatternId: '1', + }; + expect( + indexPatternDatasource.getErrorMessages(state as IndexPatternPrivateState) + ).not.toBeDefined(); + }); + + it('should return no errors with layers with no columns', () => { + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + isFirstExistenceFetch: false, + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, + currentIndexPatternId: '1', + }; + expect(indexPatternDatasource.getErrorMessages(state)).not.toBeDefined(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index edc984f5e8016a..0d822927808084 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -39,7 +39,12 @@ import { getDatasourceSuggestionsForVisualizeField, } from './indexpattern_suggestions'; -import { isDraggedField, normalizeOperationDataType } from './utils'; +import { + getInvalidFieldReferencesForLayer, + getInvalidReferences, + isDraggedField, + normalizeOperationDataType, +} from './utils'; import { LayerPanel } from './layerpanel'; import { IndexPatternColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; @@ -49,6 +54,7 @@ import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/pub import { deleteColumn } from './state_helpers'; import { Datasource, StateSetter } from '../index'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import { FieldBasedIndexPatternColumn } from './operations/definitions/column_types'; import { Dragging } from '../drag_drop/providers'; export { OperationType, IndexPatternColumn } from './operations'; @@ -335,6 +341,84 @@ export function getIndexPatternDatasource({ }, getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, + + getErrorMessages(state) { + if (!state) { + return; + } + const invalidLayers = getInvalidReferences(state); + + if (invalidLayers.length === 0) { + return; + } + + const realIndex = Object.values(state.layers) + .map((layer, i) => { + const filteredIndex = invalidLayers.indexOf(layer); + if (filteredIndex > -1) { + return [filteredIndex, i + 1]; + } + }) + .filter(Boolean) as Array<[number, number]>; + const invalidFieldsPerLayer: string[][] = getInvalidFieldReferencesForLayer( + invalidLayers, + state.indexPatterns + ); + const originalLayersList = Object.keys(state.layers); + + return realIndex.map(([filteredIndex, layerIndex]) => { + const fieldsWithBrokenReferences: string[] = invalidFieldsPerLayer[filteredIndex].map( + (columnId) => { + const column = invalidLayers[filteredIndex].columns[ + columnId + ] as FieldBasedIndexPatternColumn; + return column.sourceField; + } + ); + + if (originalLayersList.length === 1) { + return { + shortMessage: i18n.translate( + 'xpack.lens.indexPattern.dataReferenceFailureShortSingleLayer', + { + defaultMessage: 'Invalid {fields, plural, one {reference} other {references}}.', + values: { + fields: fieldsWithBrokenReferences.length, + }, + } + ), + longMessage: i18n.translate( + 'xpack.lens.indexPattern.dataReferenceFailureLongSingleLayer', + { + defaultMessage: `{fieldsLength, plural, one {Field} other {Fields}} "{fields}" {fieldsLength, plural, one {has an} other {have}} invalid reference.`, + values: { + fields: fieldsWithBrokenReferences.join('", "'), + fieldsLength: fieldsWithBrokenReferences.length, + }, + } + ), + }; + } + return { + shortMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureShort', { + defaultMessage: + 'Invalid {fieldsLength, plural, one {reference} other {references}} on Layer {layer}.', + values: { + layer: layerIndex, + fieldsLength: fieldsWithBrokenReferences.length, + }, + }), + longMessage: i18n.translate('xpack.lens.indexPattern.dataReferenceFailureLong', { + defaultMessage: `Layer {layer} has {fieldsLength, plural, one {an invalid} other {invalid}} {fieldsLength, plural, one {reference} other {references}} in {fieldsLength, plural, one {field} other {fields}} "{fields}".`, + values: { + layer: layerIndex, + fields: fieldsWithBrokenReferences.join('", "'), + fieldsLength: fieldsWithBrokenReferences.length, + }, + }), + }; + }); + }, }; return indexPatternDatasource; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index df6bde0ba1a359..d3d65617f2253f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -5,7 +5,7 @@ */ import { DataType } from '../types'; -import { IndexPatternPrivateState, IndexPattern } from './types'; +import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from './types'; import { DraggedField } from './indexpattern'; import { BaseIndexPatternColumn, @@ -43,7 +43,11 @@ export function isDraggedField(fieldCandidate: unknown): fieldCandidate is Dragg } export function hasInvalidReference(state: IndexPatternPrivateState) { - return Object.values(state.layers).some((layer) => { + return getInvalidReferences(state).length > 0; +} + +export function getInvalidReferences(state: IndexPatternPrivateState) { + return Object.values(state.layers).filter((layer) => { return layer.columnOrder.some((columnId) => { const column = layer.columns[columnId]; return ( @@ -58,19 +62,39 @@ export function hasInvalidReference(state: IndexPatternPrivateState) { }); } +export function getInvalidFieldReferencesForLayer( + layers: IndexPatternLayer[], + indexPatternMap: Record +) { + return layers.map((layer) => { + return layer.columnOrder.filter((columnId) => { + const column = layer.columns[columnId]; + return ( + hasField(column) && + fieldIsInvalid( + column.sourceField, + column.operationType, + indexPatternMap[layer.indexPatternId] + ) + ); + }); + }); +} + export function fieldIsInvalid( sourceField: string | undefined, operationType: OperationType | undefined, indexPattern: IndexPattern ) { const operationDefinition = operationType && operationDefinitionMap[operationType]; + return Boolean( sourceField && operationDefinition && !indexPattern.fields.some( (field) => field.name === sourceField && - operationDefinition.input === 'field' && + operationDefinition?.input === 'field' && operationDefinition.getPossibleOperationForField(field) !== undefined ) ); diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts index 80c7a174b32646..5ee33f9b4b3dd1 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts @@ -193,4 +193,28 @@ describe('metric_visualization', () => { `); }); }); + + describe('#getErrorMessages', () => { + it('returns undefined if no error is raised', () => { + const datasource: DatasourcePublicAPI = { + ...createMockDatasource('l1').publicAPIMock, + getOperationForColumnId(_: string) { + return { + id: 'a', + dataType: 'number', + isBucketed: false, + label: 'shazm', + }; + }, + }; + const frame = { + ...mockFrame(), + datasourceLayers: { l1: datasource }, + }; + + const error = metricVisualization.getErrorMessages(exampleState(), frame); + + expect(error).not.toBeDefined(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 77d189ce53d012..b75ac89d7e4d86 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -115,4 +115,9 @@ export const metricVisualization: Visualization = { removeDimension({ prevState }) { return { ...prevState, accessor: undefined }; }, + + getErrorMessages(state, frame) { + // Is it possible to break it? + return undefined; + }, }; diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts new file mode 100644 index 00000000000000..628d42d3de6670 --- /dev/null +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getPieVisualization } from './visualization'; +import { PieVisualizationState } from './types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; +import { DatasourcePublicAPI, FramePublicAPI } from '../types'; +import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; + +jest.mock('../id_generator'); + +const LAYER_ID = 'l1'; + +const pieVisualization = getPieVisualization({ + paletteService: chartPluginMock.createPaletteRegistry(), +}); + +function exampleState(): PieVisualizationState { + return { + shape: 'pie', + layers: [ + { + layerId: LAYER_ID, + groups: [], + metric: undefined, + numberDisplay: 'percent', + categoryDisplay: 'default', + legendDisplay: 'default', + nestedLegend: false, + }, + ], + }; +} + +function mockFrame(): FramePublicAPI { + return { + ...createMockFramePublicAPI(), + addNewLayer: () => LAYER_ID, + datasourceLayers: { + [LAYER_ID]: createMockDatasource(LAYER_ID).publicAPIMock, + }, + }; +} + +// Just a basic bootstrap here to kickstart the tests +describe('pie_visualization', () => { + describe('#getErrorMessages', () => { + it('returns undefined if no error is raised', () => { + const datasource: DatasourcePublicAPI = { + ...createMockDatasource('l1').publicAPIMock, + getOperationForColumnId(_: string) { + return { + id: 'a', + dataType: 'number', + isBucketed: false, + label: 'shazm', + }; + }, + }; + const frame = { + ...mockFrame(), + datasourceLayers: { l1: datasource }, + }; + + const error = pieVisualization.getErrorMessages(exampleState(), frame); + + expect(error).not.toBeDefined(); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 791480162b7fac..62e99396edbc72 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -233,4 +233,9 @@ export const getPieVisualization = ({ domElement ); }, + + getErrorMessages(state, frame) { + // not possible to break it? + return undefined; + }, }); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 6696a9328c8371..27ab8f258bba85 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -181,6 +181,7 @@ export interface Datasource { getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; getPublicAPI: (props: PublicAPIProps) => DatasourcePublicAPI; + getErrorMessages: (state: T) => Array<{ shortMessage: string; longMessage: string }> | undefined; /** * uniqueLabels of dimensions exposed for aria-labels of dragged dimensions */ @@ -571,6 +572,14 @@ export interface Visualization { state: T, datasourceLayers: Record ) => Ast | string | null; + /** + * The frame will call this function on all visualizations at few stages (pre-build/build error) in order + * to provide more context to the error and show it to the user + */ + getErrorMessages: ( + state: T, + frame: FramePublicAPI + ) => Array<{ shortMessage: string; longMessage: string }> | undefined; } export interface LensFilterEvent { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 4dde646ab64a5d..7c49afa53af3ea 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -407,4 +407,219 @@ describe('xy_visualization', () => { expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); }); + + describe('#getErrorMessages', () => { + it("should not return an error when there's only one dimension (X or Y)", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it("should not return an error when there's only one dimension on multiple layers (same axis everywhere)", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it('should not return an error when mixing different valid configurations in multiple layers', () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: ['a'], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it("should not return an error when there's only one splitAccessor dimension configured", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).not.toBeDefined(); + }); + it('should return an error when there are multiple layers, one axis configured for each layer (but different axis from each other)', () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: ['a'], + }, + ], + }, + createMockFramePublicAPI() + ) + ).toEqual([ + { + shortMessage: 'Missing Vertical axis.', + longMessage: 'Layer 1 requires a field for the Vertical axis.', + }, + ]); + }); + it('should return an error with batched messages for the same error with multiple layers', () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + { + layerId: 'third', + seriesType: 'area', + xAccessor: undefined, + accessors: [], + splitAccessor: 'a', + }, + ], + }, + createMockFramePublicAPI() + ) + ).toEqual([ + { + shortMessage: 'Missing Vertical axis.', + longMessage: 'Layers 2, 3 require a field for the Vertical axis.', + }, + ]); + }); + it("should return an error when some layers are complete but other layers aren't", () => { + expect( + xyVisualization.getErrorMessages( + { + ...exampleState(), + layers: [ + { + layerId: 'first', + seriesType: 'area', + xAccessor: 'a', + accessors: [], + }, + { + layerId: 'second', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + { + layerId: 'third', + seriesType: 'area', + xAccessor: 'a', + accessors: ['a'], + }, + ], + }, + createMockFramePublicAPI() + ) + ).toEqual([ + { + shortMessage: 'Missing Vertical axis.', + longMessage: 'Layer 1 requires a field for the Vertical axis.', + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index c41d8e977297b2..c7f775586ca0da 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -174,29 +174,16 @@ export const getXyVisualization = ({ groups: [ { groupId: 'x', - groupLabel: isHorizontal - ? i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { - defaultMessage: 'Vertical axis', - }) - : i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { - defaultMessage: 'Horizontal axis', - }), + groupLabel: getAxisName('x', { isHorizontal }), accessors: layer.xAccessor ? [layer.xAccessor] : [], filterOperations: isBucketed, suggestedPriority: 1, supportsMoreColumns: !layer.xAccessor, - required: !layer.seriesType.includes('percentage'), dataTestSubj: 'lnsXY_xDimensionPanel', }, { groupId: 'y', - groupLabel: isHorizontal - ? i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { - defaultMessage: 'Horizontal axis', - }) - : i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { - defaultMessage: 'Vertical axis', - }), + groupLabel: getAxisName('y', { isHorizontal }), accessors: sortedAccessors, filterOperations: isNumericMetric, supportsMoreColumns: true, @@ -309,8 +296,117 @@ export const getXyVisualization = ({ toExpression: (state, layers, attributes) => toExpression(state, layers, paletteService, attributes), toPreviewExpression: (state, layers) => toPreviewExpression(state, layers, paletteService), + + getErrorMessages(state, frame) { + // Data error handling below here + const hasNoAccessors = ({ accessors }: LayerConfig) => + accessors == null || accessors.length === 0; + const hasNoSplitAccessor = ({ splitAccessor, seriesType }: LayerConfig) => + seriesType.includes('percentage') && splitAccessor == null; + + const errors: Array<{ + shortMessage: string; + longMessage: string; + }> = []; + + // check if the layers in the state are compatible with this type of chart + if (state && state.layers.length > 1) { + // Order is important here: Y Axis is fundamental to exist to make it valid + const checks: Array<[string, (layer: LayerConfig) => boolean]> = [ + ['Y', hasNoAccessors], + ['Break down', hasNoSplitAccessor], + ]; + + // filter out those layers with no accessors at all + const filteredLayers = state.layers.filter( + ({ accessors, xAccessor, splitAccessor }: LayerConfig) => + accessors.length > 0 || xAccessor != null || splitAccessor != null + ); + for (const [dimension, criteria] of checks) { + const result = validateLayersForDimension(dimension, filteredLayers, criteria); + if (!result.valid) { + errors.push(result.payload); + } + } + } + + return errors.length ? errors : undefined; + }, }); +function validateLayersForDimension( + dimension: string, + layers: LayerConfig[], + missingCriteria: (layer: LayerConfig) => boolean +): + | { valid: true } + | { + valid: false; + payload: { shortMessage: string; longMessage: string }; + } { + // Multiple layers must be consistent: + // * either a dimension is missing in ALL of them + // * or should not miss on any + if (layers.every(missingCriteria) || !layers.some(missingCriteria)) { + return { valid: true }; + } + // otherwise it's an error and it has to be reported + const layerMissingAccessors = layers.reduce((missing: number[], layer, i) => { + if (missingCriteria(layer)) { + missing.push(i); + } + return missing; + }, []); + + return { + valid: false, + payload: getMessageIdsForDimension(dimension, layerMissingAccessors, isHorizontalChart(layers)), + }; +} + +function getAxisName(axis: 'x' | 'y', { isHorizontal }: { isHorizontal: boolean }) { + const vertical = i18n.translate('xpack.lens.xyChart.verticalAxisLabel', { + defaultMessage: 'Vertical axis', + }); + const horizontal = i18n.translate('xpack.lens.xyChart.horizontalAxisLabel', { + defaultMessage: 'Horizontal axis', + }); + if (axis === 'x') { + return isHorizontal ? vertical : horizontal; + } + return isHorizontal ? horizontal : vertical; +} + +// i18n ids cannot be dynamically generated, hence the function below +function getMessageIdsForDimension(dimension: string, layers: number[], isHorizontal: boolean) { + const layersList = layers.map((i: number) => i + 1).join(', '); + switch (dimension) { + case 'Break down': + return { + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureSplitShort', { + defaultMessage: `Missing {axis}.`, + values: { axis: 'Break down by axis' }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureSplitLong', { + defaultMessage: `{layers, plural, one {Layer} other {Layers}} {layersList} {layers, plural, one {requires} other {require}} a field for the {axis}.`, + values: { layers: layers.length, layersList, axis: 'Break down by axis' }, + }), + }; + case 'Y': + return { + shortMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureYShort', { + defaultMessage: `Missing {axis}.`, + values: { axis: getAxisName('y', { isHorizontal }) }, + }), + longMessage: i18n.translate('xpack.lens.xyVisualization.dataFailureYLong', { + defaultMessage: `{layers, plural, one {Layer} other {Layers}} {layersList} {layers, plural, one {requires} other {require}} a field for the {axis}.`, + values: { layers: layers.length, layersList, axis: getAxisName('y', { isHorizontal }) }, + }), + }; + } + return { shortMessage: '', longMessage: '' }; +} + function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { return { layerId, diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 100b2afcc97cea..73163cb70ada9a 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -98,7 +98,7 @@ class AnnotationsTableUI extends Component { if (dataCounts.processed_record_count > 0) { // Load annotations for the selected job. ml.annotations - .getAnnotations({ + .getAnnotations$({ jobIds: [job.job_id], earliestMs: null, latestMs: null, diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js index 11e196b1c8e3f6..b19328f89fbe47 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.test.js @@ -24,7 +24,7 @@ jest.mock('../../../services/ml_api_service', () => { return { ml: { annotations: { - getAnnotations: jest.fn().mockReturnValue(mockAnnotations$), + getAnnotations$: jest.fn().mockReturnValue(mockAnnotations$), }, }, }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index c3bdacde5abd87..f6889c9a6f24c2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -392,7 +392,7 @@ export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, return new Promise((resolve) => { ml.annotations - .getAnnotations({ + .getAnnotations$({ jobIds, earliestMs: timeRange.earliestMs, latestMs: timeRange.latestMs, diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts index f9e19ba6f757ea..d028bacb49a778 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/annotations.ts @@ -13,7 +13,7 @@ import { http, http$ } from '../http_service'; import { basePath } from './index'; export const annotations = { - getAnnotations(obj: { + getAnnotations$(obj: { jobIds: string[]; earliestMs: number; latestMs: number; @@ -30,6 +30,23 @@ export const annotations = { }); }, + getAnnotations(obj: { + jobIds: string[]; + earliestMs: number | null; + latestMs: number | null; + maxAnnotations: number; + fields?: FieldToBucket[]; + detectorIndex?: number; + entities?: any[]; + }) { + const body = JSON.stringify(obj); + return http({ + path: `${basePath()}/annotations`, + method: 'POST', + body, + }); + }, + indexAnnotation(obj: Annotation) { const body = JSON.stringify(obj); return http({ diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss index 4399327c55dca7..0c38d8e7ca1718 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/_timeseriesexplorer_annotations.scss @@ -76,3 +76,25 @@ $mlAnnotationRectDefaultFillOpacity: 0.05; .mlAnnotationHidden { display: none; } + +// context annotation marker +.mlContextAnnotationRect { + stroke: $euiColorFullShade; + stroke-width: $mlAnnotationBorderWidth; + stroke-opacity: $mlAnnotationRectDefaultStrokeOpacity; + transition: stroke-opacity $euiAnimSpeedFast; + + fill: $euiColorFullShade; + fill-opacity: $mlAnnotationRectDefaultFillOpacity; + transition: fill-opacity $euiAnimSpeedFast; + + shape-rendering: geometricPrecision; +} + +.mlContextAnnotationRect-isBlur { + stroke-opacity: $mlAnnotationRectDefaultStrokeOpacity / 2; + transition: stroke-opacity $euiAnimSpeedFast; + + fill-opacity: $mlAnnotationRectDefaultFillOpacity / 2; + transition: fill-opacity $euiAnimSpeedFast; +} diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts index 9a06f6d6b8e035..04b666b4fc6848 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.d.ts @@ -6,6 +6,7 @@ import d3 from 'd3'; +import React from 'react'; import { Annotation } from '../../../../../common/types/annotations'; import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; import { ChartTooltipService } from '../../../components/chart_tooltip'; @@ -19,6 +20,33 @@ interface State { annotation: Annotation | null; } -export interface TimeseriesChart extends React.Component { +interface TimeseriesChartProps { + annotation: object; + autoZoomDuration: number; + bounds: object; + contextAggregationInterval: object; + contextChartData: any[]; + contextForecastData: any[]; + contextChartSelected: any; + detectorIndex: number; + focusAggregationInterval: object; + focusAnnotationData: Annotation[]; + focusChartData: any[]; + focusForecastData: any[]; + modelPlotEnabled: boolean; + renderFocusChartOnly: boolean; + selectedJob: CombinedJob; + showForecast: boolean; + showModelBounds: boolean; + svgWidth: number; + swimlaneData: any[]; + zoomFrom: object; + zoomTo: object; + zoomFromFocusLoaded: object; + zoomToFocusLoaded: object; + tooltipService: object; +} + +declare class TimeseriesChart extends React.Component { focusXScale: d3.scale.Ordinal<{}, number>; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 448d39db3e4441..3169ecfd1bbc76 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -25,6 +25,7 @@ import { annotation$ } from '../../../services/annotations_service'; import { formatValue } from '../../../formatters/format_value'; import { LINE_CHART_ANOMALY_RADIUS, + ANNOTATION_SYMBOL_HEIGHT, MULTI_BUCKET_SYMBOL_SIZE, SCHEDULED_EVENT_SYMBOL_HEIGHT, drawLineChartDots, @@ -48,6 +49,7 @@ import { renderAnnotations, highlightFocusChartAnnotation, unhighlightFocusChartAnnotation, + ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; const focusZoomPanelHeight = 25; @@ -57,6 +59,8 @@ const contextChartHeight = 60; const contextChartLineTopMargin = 3; const chartSpacing = 25; const swimlaneHeight = 30; +const ctxAnnotationMargin = 2; +const annotationHeight = ANNOTATION_SYMBOL_HEIGHT + ctxAnnotationMargin * 2; const margin = { top: 10, right: 10, bottom: 15, left: 40 }; const ZOOM_INTERVAL_OPTIONS = [ @@ -80,9 +84,16 @@ const anomalyGrayScale = d3.scale .domain([3, 25, 50, 75, 100]) .range(['#dce7ed', '#b0c5d6', '#b1a34e', '#b17f4e', '#c88686']); -function getSvgHeight() { +function getSvgHeight(showAnnotations) { + const adjustedAnnotationHeight = showAnnotations ? annotationHeight : 0; return ( - focusHeight + contextChartHeight + swimlaneHeight + chartSpacing + margin.top + margin.bottom + focusHeight + + contextChartHeight + + swimlaneHeight + + adjustedAnnotationHeight + + chartSpacing + + margin.top + + margin.bottom ); } @@ -225,7 +236,12 @@ class TimeseriesChartIntl extends Component { } componentDidUpdate(prevProps) { - if (this.props.renderFocusChartOnly === false || prevProps.svgWidth !== this.props.svgWidth) { + if ( + this.props.renderFocusChartOnly === false || + prevProps.svgWidth !== this.props.svgWidth || + prevProps.showAnnotations !== this.props.showAnnotations || + prevProps.annotationData !== this.props.annotationData + ) { this.renderChart(); this.drawContextChartSelection(); } @@ -246,6 +262,7 @@ class TimeseriesChartIntl extends Component { modelPlotEnabled, selectedJob, svgWidth, + showAnnotations, } = this.props; const createFocusChart = this.createFocusChart.bind(this); @@ -254,7 +271,7 @@ class TimeseriesChartIntl extends Component { const focusYAxis = this.focusYAxis; const focusYScale = this.focusYScale; - const svgHeight = getSvgHeight(); + const svgHeight = getSvgHeight(showAnnotations); // Clear any existing elements from the visualization, // then build the svg elements for the bubble chart. @@ -367,7 +384,13 @@ class TimeseriesChartIntl extends Component { // Draw each of the component elements. createFocusChart(focus, this.vizWidth, focusHeight); - drawContextElements(context, this.vizWidth, contextChartHeight, swimlaneHeight); + drawContextElements( + context, + this.vizWidth, + contextChartHeight, + swimlaneHeight, + annotationHeight + ); } contextChartInitialized = false; @@ -947,10 +970,19 @@ class TimeseriesChartIntl extends Component { } drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) { - const { bounds, contextChartData, contextForecastData, modelPlotEnabled } = this.props; - + const { + bounds, + contextChartData, + contextForecastData, + modelPlotEnabled, + annotationData, + showAnnotations, + } = this.props; const data = contextChartData; + const showFocusChartTooltip = this.showFocusChartTooltip.bind(this); + const hideFocusChartTooltip = this.props.tooltipService.hide.bind(this.props.tooltipService); + this.contextXScale = d3.time .scale() .range([0, cxtWidth]) @@ -997,20 +1029,26 @@ class TimeseriesChartIntl extends Component { .domain([chartLimits.min, chartLimits.max]); const borders = cxtGroup.append('g').attr('class', 'axis'); + const brushChartHeight = showAnnotations + ? cxtChartHeight + swlHeight + annotationHeight + : cxtChartHeight + swlHeight; // Add borders left and right. + borders.append('line').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', brushChartHeight); borders .append('line') - .attr('x1', 0) + .attr('x1', cxtWidth) .attr('y1', 0) - .attr('x2', 0) - .attr('y2', cxtChartHeight + swlHeight); + .attr('x2', cxtWidth) + .attr('y2', brushChartHeight); + + // Add bottom borders borders .append('line') - .attr('x1', cxtWidth) - .attr('y1', 0) + .attr('x1', 0) + .attr('y1', brushChartHeight) .attr('x2', cxtWidth) - .attr('y2', cxtChartHeight + swlHeight); + .attr('y2', brushChartHeight); // Add x axis. const timeBuckets = getTimeBucketsFromCache(); @@ -1065,6 +1103,61 @@ class TimeseriesChartIntl extends Component { cxtGroup.append('path').datum(data).attr('class', 'values-line').attr('d', contextValuesLine); drawLineChartDots(data, cxtGroup, contextValuesLine, 1); + // Add annotation markers to the context area + cxtGroup.append('g').classed('mlContextAnnotations', true); + + const [contextXRangeStart, contextXRangeEnd] = this.contextXScale.range(); + const ctxAnnotations = cxtGroup + .select('.mlContextAnnotations') + .selectAll('g.mlContextAnnotation') + .data(showAnnotations && annotationData ? annotationData : [], (d) => d._id || ''); + + ctxAnnotations.enter().append('g').classed('mlContextAnnotation', true); + + const ctxAnnotationRects = ctxAnnotations + .selectAll('.mlContextAnnotationRect') + .data((d) => [d]); + + ctxAnnotationRects + .enter() + .append('rect') + .attr('rx', ctxAnnotationMargin) + .attr('ry', ctxAnnotationMargin) + .on('mouseover', function (d) { + showFocusChartTooltip(d, this); + }) + .on('mouseout', () => hideFocusChartTooltip()) + .classed('mlContextAnnotationRect', true); + + ctxAnnotationRects + .attr('x', (d) => { + const date = moment(d.timestamp); + let xPos = this.contextXScale(date); + + if (xPos - ANNOTATION_SYMBOL_HEIGHT <= contextXRangeStart) { + xPos = 0; + } + if (xPos + ANNOTATION_SYMBOL_HEIGHT >= contextXRangeEnd) { + xPos = contextXRangeEnd - ANNOTATION_SYMBOL_HEIGHT; + } + + return xPos; + }) + .attr('y', cxtChartHeight + swlHeight + 2) + .attr('height', ANNOTATION_SYMBOL_HEIGHT) + .attr('width', (d) => { + const start = this.contextXScale(moment(d.timestamp)) + 1; + const end = + typeof d.end_timestamp !== 'undefined' + ? this.contextXScale(moment(d.end_timestamp)) - 1 + : start + ANNOTATION_MIN_WIDTH; + const width = Math.max(ANNOTATION_MIN_WIDTH, end - start); + return width; + }); + + ctxAnnotations.classed('mlAnnotationHidden', !showAnnotations); + ctxAnnotationRects.exit().remove(); + // Create the path elements for the forecast value line and bounds area. if (contextForecastData !== undefined) { cxtGroup diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts index 0b541d54ee7b3e..bd86d07dcd8b7d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_annotations.ts @@ -90,7 +90,7 @@ const ANNOTATION_DEFAULT_LEVEL = 1; const ANNOTATION_LEVEL_HEIGHT = 28; const ANNOTATION_UPPER_RECT_MARGIN = 0; const ANNOTATION_UPPER_TEXT_MARGIN = -7; -const ANNOTATION_MIN_WIDTH = 2; +export const ANNOTATION_MIN_WIDTH = 2; const ANNOTATION_RECT_BORDER_RADIUS = 2; const ANNOTATION_TEXT_VERTICAL_OFFSET = 26; const ANNOTATION_TEXT_RECT_VERTICAL_OFFSET = 12; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx new file mode 100644 index 00000000000000..89e7d292dbdf2b --- /dev/null +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlTooltipComponent } from '../../../components/chart_tooltip'; +import { TimeseriesChart } from './timeseries_chart'; +import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; +import { extractErrorMessage } from '../../../../../common/util/errors'; +import { Annotation } from '../../../../../common/types/annotations'; +import { useMlKibana, useNotifications } from '../../../contexts/kibana'; +import { getBoundsRoundedToInterval } from '../../../util/time_buckets'; +import { ANNOTATION_EVENT_USER } from '../../../../../common/constants/annotations'; +import { getControlsForDetector } from '../../get_controls_for_detector'; + +interface TimeSeriesChartWithTooltipsProps { + bounds: any; + detectorIndex: number; + renderFocusChartOnly: boolean; + selectedJob: CombinedJob; + selectedEntities: Record; + showAnnotations: boolean; + showForecast: boolean; + showModelBounds: boolean; + chartProps: any; + lastRefresh: number; + contextAggregationInterval: any; +} +export const TimeSeriesChartWithTooltips: FC = ({ + bounds, + detectorIndex, + renderFocusChartOnly, + selectedJob, + selectedEntities, + showAnnotations, + showForecast, + showModelBounds, + chartProps, + lastRefresh, + contextAggregationInterval, +}) => { + const { toasts: toastNotifications } = useNotifications(); + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + + const [annotationData, setAnnotationData] = useState([]); + + const showAnnotationErrorToastNotification = useCallback((error?: string) => { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.ml.timeSeriesExplorer.mlSingleMetricViewerChart.annotationsErrorTitle', + { + defaultMessage: 'An error occurred fetching annotations', + } + ), + ...(error ? { text: extractErrorMessage(error) } : {}), + }); + }, []); + + useEffect(() => { + let unmounted = false; + const entities = getControlsForDetector(detectorIndex, selectedEntities, selectedJob.job_id); + const nonBlankEntities = Array.isArray(entities) + ? entities.filter((entity) => entity.fieldValue !== null) + : undefined; + const searchBounds = getBoundsRoundedToInterval(bounds, contextAggregationInterval, false); + + /** + * Loads the full list of annotations for job without any aggs or time boundaries + * used to indicate existence of annotations that are beyond the selected time + * in the time series brush area + */ + const loadAnnotations = async (jobId: string) => { + try { + const resp = await mlApiServices.annotations.getAnnotations({ + jobIds: [jobId], + earliestMs: searchBounds.min.valueOf(), + latestMs: searchBounds.max.valueOf(), + maxAnnotations: ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, + fields: [ + { + field: 'event', + missing: ANNOTATION_EVENT_USER, + }, + ], + detectorIndex, + entities: nonBlankEntities, + }); + if (!unmounted) { + if (Array.isArray(resp.annotations[jobId])) { + setAnnotationData(resp.annotations[jobId]); + } + } + } catch (error) { + showAnnotationErrorToastNotification(error); + } + }; + + loadAnnotations(selectedJob.job_id); + + return () => { + unmounted = true; + }; + }, [ + selectedJob.job_id, + detectorIndex, + lastRefresh, + selectedEntities, + bounds, + contextAggregationInterval, + ]); + + return ( +
+ + {(tooltipService) => ( + + )} + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts index cb66b8d53e6609..530ba567ed9f78 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -6,7 +6,7 @@ import { FC } from 'react'; -import { getDateFormatTz, TimeRangeBounds } from '../explorer/explorer_utils'; +import { TimeRangeBounds } from '../explorer/explorer_utils'; declare const TimeSeriesExplorer: FC<{ appStateHandler: (action: string, payload: any) => void; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 5e452dab2f883a..720c1377d4035d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -47,12 +47,10 @@ import { import { AnnotationFlyout } from '../components/annotations/annotation_flyout'; import { AnnotationsTable } from '../components/annotations/annotations_table'; import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; -import { MlTooltipComponent } from '../components/chart_tooltip'; import { ForecastingModal } from './components/forecasting_modal/forecasting_modal'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; import { SelectInterval } from '../components/controls/select_interval/select_interval'; import { SelectSeverity } from '../components/controls/select_severity/select_severity'; -import { TimeseriesChart } from './components/timeseries_chart/timeseries_chart'; import { TimeseriesexplorerNoChartData } from './components/timeseriesexplorer_no_chart_data'; import { TimeSeriesExplorerPage } from './timeseriesexplorer_page'; @@ -83,6 +81,7 @@ import { import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings'; import { getControlsForDetector } from './get_controls_for_detector'; import { SeriesControls } from './components/series_controls'; +import { TimeSeriesChartWithTooltips } from './components/timeseries_chart/timeseries_chart_with_tooltip'; // Used to indicate the chart is being plotted across // all partition field values, where the cardinality of the field cannot be @@ -175,6 +174,7 @@ export class TimeSeriesExplorer extends React.Component { this.resizeRef.current !== null ? this.resizeRef.current.offsetWidth - containerPadding : 0, }); }; + unmounted = false; /** * Subject for listening brush time range selection. @@ -877,6 +877,7 @@ export class TimeSeriesExplorer extends React.Component { componentWillUnmount() { this.subscriptions.unsubscribe(); this.resizeChecker.destroy(); + this.unmounted = true; } render() { @@ -957,7 +958,6 @@ export class TimeSeriesExplorer extends React.Component { isEqual(this.previousChartProps.focusForecastData, chartProps.focusForecastData) && isEqual(this.previousChartProps.focusChartData, chartProps.focusChartData) && isEqual(this.previousChartProps.focusAnnotationData, chartProps.focusAnnotationData) && - this.previousShowAnnotations === showAnnotations && this.previousShowForecast === showForecast && this.previousShowModelBounds === showModelBounds && this.props.previousRefresh === lastRefresh @@ -966,7 +966,6 @@ export class TimeSeriesExplorer extends React.Component { } this.previousChartProps = chartProps; - this.previousShowAnnotations = showAnnotations; this.previousShowForecast = showForecast; this.previousShowModelBounds = showModelBounds; @@ -1134,23 +1133,19 @@ export class TimeSeriesExplorer extends React.Component {
)}
-
- - {(tooltipService) => ( - - )} - -
+ {focusAnnotationError !== undefined && ( <> { return { type: this.type, - rollupIndex: this.rollupIndex, + params: { + rollup_index: this.rollupIndex, + }, }; }; } diff --git a/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js new file mode 100644 index 00000000000000..e97606c1fadfba --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { jobs } from './jobs'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js similarity index 65% rename from src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js rename to x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js index 39ebd9595eeafc..c03b7c33abe0a1 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/fixtures/jobs.js +++ b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ export const jobs = [ diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js b/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js similarity index 81% rename from src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js rename to x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js index e3c93ac1f86160..a67f67de859f5b 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/__tests__/jobs_compatibility.js +++ b/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js @@ -1,22 +1,8 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ - import expect from '@kbn/expect'; import { areJobsCompatible, mergeJobConfigurations } from '../jobs_compatibility'; import { jobs } from './fixtures'; diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts b/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts similarity index 79% rename from src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts rename to x-pack/plugins/rollup/server/lib/jobs_compatibility.ts index f21de8907ee245..f5f54cf9a54e86 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/jobs_compatibility.ts +++ b/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts @@ -1,20 +1,7 @@ /* - * 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. + * 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 { isEqual } from 'lodash'; diff --git a/x-pack/plugins/rollup/server/lib/map_capabilities.ts b/x-pack/plugins/rollup/server/lib/map_capabilities.ts new file mode 100644 index 00000000000000..233c6d1dd4b4b6 --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/map_capabilities.ts @@ -0,0 +1,24 @@ +/* + * 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 { mergeJobConfigurations } from './jobs_compatibility'; + +export function getCapabilitiesForRollupIndices(indices: { [key: string]: any }) { + const indexNames = Object.keys(indices); + const capabilities = {} as { [key: string]: any }; + + indexNames.forEach((index) => { + try { + capabilities[index] = mergeJobConfigurations(indices[index].rollup_jobs); + } catch (e) { + capabilities[index] = { + error: e.message, + }; + } + }); + + return capabilities; +} diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts b/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts similarity index 70% rename from src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts rename to x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts index dd69f4b7ff0078..51111e9e45d0ab 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/merge_capabilities_with_fields.ts +++ b/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts @@ -1,30 +1,20 @@ /* - * 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. + * 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. */ // Merge rollup capabilities information with field information -import { FieldDescriptor } from '../index_patterns_fetcher'; +export interface Field { + name?: string; + [key: string]: any; +} export const mergeCapabilitiesWithFields = ( rollupIndexCapabilities: { [key: string]: any }, fieldsFromFieldCapsApi: { [key: string]: any }, - previousFields: FieldDescriptor[] = [] + previousFields: Field[] = [] ) => { const rollupFields = [...previousFields]; const rollupFieldNames: string[] = []; diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index dcf6629d353974..f439ac555aed93 100644 --- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts @@ -6,11 +6,8 @@ import { keyBy, isString } from 'lodash'; import { ILegacyScopedClusterClient } from 'src/core/server'; import { ReqFacade } from '../../../../../../src/plugins/vis_type_timeseries/server'; - -import { - mergeCapabilitiesWithFields, - getCapabilitiesForRollupIndices, -} from '../../../../../../src/plugins/data/server'; +import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields'; +import { getCapabilitiesForRollupIndices } from '../map_capabilities'; const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 51920af7c8cbc5..fe193150fc1cae 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -36,7 +36,8 @@ import { registerRollupSearchStrategy } from './lib/search_strategies'; import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; import { isEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; -import { getCapabilitiesForRollupIndices } from '../../../../src/plugins/data/server'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; interface RollupContext { client: ILegacyScopedClusterClient; @@ -106,6 +107,7 @@ export class RollupPlugin implements Plugin { isEsError, formatEsError, getCapabilitiesForRollupIndices, + mergeCapabilitiesWithFields, }, sharedImports: { IndexPatternsFetcher, diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts new file mode 100644 index 00000000000000..7bf525ca4aa984 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerFieldsForWildcardRoute } from './register_fields_for_wildcard_route'; + +export function registerIndexPatternsRoutes(dependencies: RouteDependencies) { + registerFieldsForWildcardRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts new file mode 100644 index 00000000000000..df9907fbf731a1 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts @@ -0,0 +1,142 @@ +/* + * 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 { keyBy } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { Field } from '../../../lib/merge_capabilities_with_fields'; +import { RouteDependencies } from '../../../types'; +import type { IndexPatternsFetcher as IndexPatternsFetcherType } from '../../../../../../../src/plugins/data/server'; + +const parseMetaFields = (metaFields: string | string[]) => { + let parsedFields: string[] = []; + if (typeof metaFields === 'string') { + parsedFields = JSON.parse(metaFields); + } else { + parsedFields = metaFields; + } + return parsedFields; +}; + +const getFieldsForWildcardRequest = async ( + context: any, + request: any, + response: any, + IndexPatternsFetcher: typeof IndexPatternsFetcherType +) => { + const { asCurrentUser } = context.core.elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser); + const { pattern, meta_fields: metaFields } = request.query; + + let parsedFields: string[] = []; + try { + parsedFields = parseMetaFields(metaFields); + } catch (error) { + return response.badRequest({ + body: error, + }); + } + + try { + const fields = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedFields, + }); + + return response.ok({ + body: { fields }, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + return response.notFound(); + } +}; + +/** + * Get list of fields for rollup index pattern, in the format of regular index pattern fields + */ +export const registerFieldsForWildcardRoute = ({ + router, + license, + lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices, mergeCapabilitiesWithFields }, + sharedImports: { IndexPatternsFetcher }, +}: RouteDependencies) => { + const querySchema = schema.object({ + pattern: schema.string(), + meta_fields: schema.arrayOf(schema.string(), { + defaultValue: [], + }), + params: schema.string({ + validate(value) { + try { + const params = JSON.parse(value); + const keys = Object.keys(params); + const { rollup_index: rollupIndex } = params; + + if (!rollupIndex) { + return '[request query.params]: "rollup_index" is required'; + } else if (keys.length > 1) { + const invalidParams = keys.filter((key) => key !== 'rollup_index'); + return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; + } + } catch (err) { + return '[request query.params]: expected JSON string'; + } + }, + }), + }); + + router.get( + { + path: '/api/index_patterns/rollup/_fields_for_wildcard', + validate: { + query: querySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { params, meta_fields: metaFields } = request.query; + + try { + // Make call and use field information from response + const { payload } = await getFieldsForWildcardRequest( + context, + request, + response, + IndexPatternsFetcher + ); + const fields = payload.fields; + const parsedParams = JSON.parse(params); + const rollupIndex = parsedParams.rollup_index; + const rollupFields: Field[] = []; + const fieldsFromFieldCapsApi: { [key: string]: any } = keyBy(fields, 'name'); + const rollupIndexCapabilities = getCapabilitiesForRollupIndices( + await context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { + indexPattern: rollupIndex, + }) + )[rollupIndex].aggs; + + // Keep meta fields + metaFields.forEach( + (field: string) => + fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) + ); + + const mergedRollupFields = mergeCapabilitiesWithFields( + rollupIndexCapabilities, + fieldsFromFieldCapsApi, + rollupFields + ); + return response.ok({ body: { fields: mergedRollupFields } }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/index.ts b/x-pack/plugins/rollup/server/routes/index.ts index 322003c0ee325c..b25480855b4a21 100644 --- a/x-pack/plugins/rollup/server/routes/index.ts +++ b/x-pack/plugins/rollup/server/routes/index.ts @@ -6,11 +6,13 @@ import { RouteDependencies } from '../types'; +import { registerIndexPatternsRoutes } from './api/index_patterns'; import { registerIndicesRoutes } from './api/indices'; import { registerJobsRoutes } from './api/jobs'; import { registerSearchRoutes } from './api/search'; export function registerApiRoutes(dependencies: RouteDependencies) { + registerIndexPatternsRoutes(dependencies); registerIndicesRoutes(dependencies); registerJobsRoutes(dependencies); registerSearchRoutes(dependencies); diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 89e13e69c4da29..b167806cf8d5df 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -8,7 +8,6 @@ import { IRouter } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { getCapabilitiesForRollupIndices } from 'src/plugins/data/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -16,6 +15,8 @@ import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; import { isEsError } from './shared_imports'; import { formatEsError } from './lib/format_es_error'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; export interface Dependencies { indexManagement?: IndexManagementPluginSetup; @@ -32,6 +33,7 @@ export interface RouteDependencies { isEsError: typeof isEsError; formatEsError: typeof formatEsError; getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; + mergeCapabilitiesWithFields: typeof mergeCapabilitiesWithFields; }; sharedImports: { IndexPatternsFetcher: typeof IndexPatternsFetcher; diff --git a/x-pack/test/api_integration/apis/management/rollup/constants.js b/x-pack/test/api_integration/apis/management/rollup/constants.js index 0313434cf716c4..fe899c4c10c880 100644 --- a/x-pack/test/api_integration/apis/management/rollup/constants.js +++ b/x-pack/test/api_integration/apis/management/rollup/constants.js @@ -5,7 +5,7 @@ */ export const API_BASE_PATH = '/api/rollup'; -export const INDEX_PATTERNS_EXTENSION_BASE_PATH = '/api/index_patterns'; +export const INDEX_PATTERNS_EXTENSION_BASE_PATH = '/api/index_patterns/rollup'; export const ROLLUP_INDEX_NAME = 'rollup_index'; export const INDEX_TO_ROLLUP_MAPPINGS = { properties: { diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index 0a93e8b8bd1e37..357b952e7e66d4 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -26,6 +26,7 @@ export default function ({ getService }) { describe('query params validation', () => { let uri; let body; + let params; it('"pattern" is required', async () => { uri = `${BASE_URI}`; @@ -35,17 +36,62 @@ export default function ({ getService }) { ); }); + it('"params" is required', async () => { + params = { pattern: 'foo' }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain( + '[request query.params]: expected value of type [string]' + ); + }); + + it('"params" must be a valid JSON string', async () => { + params = { pattern: 'foo', params: 'foobarbaz' }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: expected JSON string'); + }); + + it('"params" requires a "rollup_index" property', async () => { + params = { pattern: 'foo', params: JSON.stringify({}) }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: "rollup_index" is required'); + }); + + it('"params" only accepts a "rollup_index" property', async () => { + params = { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'my_index', someProp: 'bar' }), + }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain('[request query.params]: someProp is not allowed'); + }); + + it('"meta_fields" must be an Array', async () => { + params = { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'bar' }), + meta_fields: 'stringValue', + }; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; + ({ body } = await supertest.get(uri).expect(400)); + expect(body.message).to.contain( + '[request query.meta_fields]: could not parse array value from json input' + ); + }); + it('should return 404 the rollup index to query does not exist', async () => { uri = `${BASE_URI}?${stringify( { pattern: 'foo', - type: 'rollup', - rollup_index: 'bar', + params: JSON.stringify({ rollup_index: 'bar' }), }, { sort: false } )}`; ({ body } = await supertest.get(uri).expect(404)); - expect(body.message).to.contain('No indices match pattern "foo"'); + expect(body.message).to.contain('[index_not_found_exception] no such index [bar]'); }); }); @@ -59,8 +105,7 @@ export default function ({ getService }) { // Query for wildcard const params = { pattern: indexName, - type: 'rollup', - rollup_index: rollupIndex, + params: JSON.stringify({ rollup_index: rollupIndex }), }; const uri = `${BASE_URI}?${stringify(params, { sort: false })}`; const { body } = await supertest.get(uri).expect(200);