diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 516f5e71577e6d..bea2f4e74297e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,8 @@ # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui +/x-pack/test/functional/apps/apm/ @elastic/apm-ui +/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui # Beats /x-pack/legacy/plugins/beats_management/ @elastic/beats diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 152a0e2c488712..5fd3ef5e8ff4bf 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -141,6 +141,39 @@ function addBar(foos, foo) { } ``` +### Avoid `any` whenever possible + +Since TypeScript 3.0 and the introduction of the +[`unknown` type](https://mariusschulz.com/blog/the-unknown-type-in-typescript) there are rarely any +reasons to use `any` as a type. Nearly all places of former `any` usage can be replace by either a +generic or `unknown` (in cases the type is really not known). + +You should always prefer using those mechanisms over using `any`, since they are stricter typed and +less likely to introduce bugs in the future due to insufficient types. + +If you’re not having `any` in your plugin or are starting a new plugin, you should enable the +[`@typescript-eslint/no-explicit-any`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md) +linting rule for your plugin via the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. + +### Avoid non-null assertions + +You should try avoiding non-null assertions (`!.`) wherever possible. By using them you tell +TypeScript, that something is not null even though by it’s type it could be. Usage of non-null +assertions is most often a side-effect of you actually checked that the variable is not `null` +but TypeScript doesn’t correctly carry on that information till the usage of the variable. + +In most cases it’s possible to replace the non-null assertion by structuring your code/checks slightly different +or using [user defined type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) +to properly tell TypeScript what type a variable has. + +Using non-null assertion increases the risk for future bugs. In case the condition under which we assumed that the +variable can’t be null has changed (potentially even due to changes in compeltely different files), the non-null +assertion would now wrongly disable proper type checking for us. + +If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the +[`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) +linting rule for you plugin in the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. + ### Return/throw early from functions To avoid deep nesting of if-statements, always return a function's value as early diff --git a/config/kibana.yml b/config/kibana.yml index 7d49fb37e03208..9525a6423d90a0 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -18,10 +18,6 @@ # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false -# Specifies the default route when opening Kibana. You can use this setting to modify -# the landing page when opening Kibana. -#server.defaultRoute: /app/kibana - # The maximum payload size in bytes for incoming server requests. #server.maxPayloadBytes: 1048576 diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index fd80951b1c9f25..f20ded78e07434 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -41,6 +41,11 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit `has_reference`:: (Optional, object) Filters to objects that have a relationship with the type and ID combination. +`filter`:: + (Optional, string) The filter is a KQL string with the caveat that if you filter with an attribute from your type saved object. + It should look like that savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object like `updatedAt`, + you will have to define your filter like that savedObjectType.updatedAt > 2018-12-22. + NOTE: As objects change in {kib}, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 80ddb1aea18d15..a4fa3f17d0d94f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 2ad9591426ab26..00a71d25cea38a 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md new file mode 100644 index 00000000000000..82237134e0b22c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md) + +## SavedObjectsFindOptions.filter property + +Signature: + +```typescript +filter?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md index f90c60ebdd0dc1..4c916431d4333f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md @@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | +| [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md) | string | | | [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md new file mode 100644 index 00000000000000..308bebbeaf60b8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md) + +## SavedObjectsFindOptions.filter property + +Signature: + +```typescript +filter?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md index ad81c439d902c0..dfd51d480db926 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md @@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | +| [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md) | string | | | [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) | number | | diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7f9034c48e232f..5b3db22a39ea64 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -256,10 +256,6 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). `server.customResponseHeaders:`:: *Default: `{}`* Header names and values to send on all responses to the client from the Kibana server. -[[server-default]]`server.defaultRoute:`:: *Default: "/app/kibana"* This setting -specifies the default route when opening Kibana. You can use this setting to -modify the landing page when opening Kibana. Supported on {ece}. - `server.host:`:: *Default: "localhost"* This setting specifies the host of the back end server. diff --git a/package.json b/package.json index 6f54c8683410a7..8aff95748560db 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@babel/register": "^7.5.5", "@elastic/charts": "^12.0.2", "@elastic/datemath": "5.0.2", - "@elastic/eui": "14.3.0", + "@elastic/eui": "14.4.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -160,7 +160,6 @@ "expiry-js": "0.1.7", "file-loader": "4.2.0", "font-awesome": "4.7.0", - "fp-ts": "^2.0.5", "getos": "^3.1.0", "glob": "^7.1.2", "glob-all": "^3.1.0", @@ -177,7 +176,6 @@ "https-proxy-agent": "^2.2.2", "inert": "^5.1.0", "inline-style": "^2.0.0", - "io-ts": "^2.0.1", "joi": "^13.5.2", "jquery": "^3.4.1", "js-yaml": "3.13.1", diff --git a/packages/kbn-babel-code-parser/src/can_require.js b/packages/kbn-babel-code-parser/src/can_require.js index e590c249e9806f..4d85910abe6ed6 100644 --- a/packages/kbn-babel-code-parser/src/can_require.js +++ b/packages/kbn-babel-code-parser/src/can_require.js @@ -17,18 +17,18 @@ * under the License. */ -export function canRequire(cwd, entry) { +export function canRequire(entry, cwd = require.resolve.paths(entry) || []) { try { // We will try to test if we can resolve // this entry through the require.resolve // setting as the start looking path the - // given cwd. Require.resolve will keep + // given cwd. That cwd variable could be + // a path or an array of paths + // from where Require.resolve will keep // looking recursively as normal starting - // from that location. + // from those locations. return require.resolve(entry, { - paths: [ - cwd - ] + paths: [].concat(cwd) }); } catch (e) { return false; diff --git a/packages/kbn-babel-code-parser/src/code_parser.js b/packages/kbn-babel-code-parser/src/code_parser.js index 8d76b1032561ac..0f53bd249bb5cc 100644 --- a/packages/kbn-babel-code-parser/src/code_parser.js +++ b/packages/kbn-babel-code-parser/src/code_parser.js @@ -79,7 +79,7 @@ export async function parseEntries(cwd, entries, strategy, results, wasParsed = const sanitizedCwd = cwd || process.cwd(); // Test each entry against canRequire function - const entriesQueue = entries.map(entry => canRequire(sanitizedCwd, entry)); + const entriesQueue = entries.map(entry => canRequire(entry)); while(entriesQueue.length) { // Get the first element in the queue as diff --git a/packages/kbn-babel-code-parser/src/strategies.js b/packages/kbn-babel-code-parser/src/strategies.js index 317ded014210b1..89621bc53bd534 100644 --- a/packages/kbn-babel-code-parser/src/strategies.js +++ b/packages/kbn-babel-code-parser/src/strategies.js @@ -62,8 +62,12 @@ export async function dependenciesParseStrategy(cwd, parseSingleFile, mainEntry, // new dependencies return dependencies.reduce((filteredEntries, entry) => { const absEntryPath = resolve(cwd, dirname(mainEntry), entry); - const requiredPath = canRequire(cwd, absEntryPath); - const requiredRelativePath = canRequire(cwd, entry); + + // NOTE: cwd for following canRequires is absEntryPath + // because we should start looking from there + const requiredPath = canRequire(absEntryPath, absEntryPath); + const requiredRelativePath = canRequire(entry, absEntryPath); + const isRelativeFile = !isAbsolute(entry); const isNodeModuleDep = isRelativeFile && !requiredPath && requiredRelativePath; const isNewEntry = isRelativeFile && requiredPath; diff --git a/packages/kbn-babel-code-parser/src/strategies.test.js b/packages/kbn-babel-code-parser/src/strategies.test.js index 5a84edf560af13..d7caa8b95d4a22 100644 --- a/packages/kbn-babel-code-parser/src/strategies.test.js +++ b/packages/kbn-babel-code-parser/src/strategies.test.js @@ -59,8 +59,8 @@ describe('Code Parser Strategies', () => { cb(null, `require('dep_from_node_modules')`); }); - canRequire.mockImplementation((mockCwd, entry) => { - if (entry === `${mockCwd}dep1/dep_from_node_modules`) { + canRequire.mockImplementation((entry, cwd) => { + if (entry === `${cwd}dep1/dep_from_node_modules`) { return false; } @@ -78,7 +78,7 @@ describe('Code Parser Strategies', () => { cb(null, `require('./relative_dep')`); }); - canRequire.mockImplementation((mockCwd, entry) => { + canRequire.mockImplementation((entry) => { if (entry === `${mockCwd}dep1/relative_dep`) { return `${entry}/index.js`; } diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts index 915c024f2ab48d..448ef0e9cca750 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.d.ts @@ -17,6 +17,8 @@ * under the License. */ +import { JsonObject } from '..'; + /** * WARNING: these typings are incomplete */ @@ -30,15 +32,6 @@ export interface KueryParseOptions { startRule: string; } -type JsonValue = null | boolean | number | string | JsonObject | JsonArray; - -interface JsonObject { - [key: string]: JsonValue; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface JsonArray extends Array {} - export function fromKueryExpression( expression: string, parseOptions?: KueryParseOptions diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/packages/kbn-es-query/src/kuery/functions/is.js index 0338671e9b3fe4..690f98b08ba827 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ b/packages/kbn-es-query/src/kuery/functions/is.js @@ -32,7 +32,6 @@ export function buildNodeParams(fieldName, value, isPhrase = false) { if (_.isUndefined(value)) { throw new Error('value is a required argument'); } - const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value); const isPhraseNode = literal.buildNode(isPhrase); @@ -42,7 +41,7 @@ export function buildNodeParams(fieldName, value, isPhrase = false) { } export function toElasticsearchQuery(node, indexPattern = null, config = {}) { - const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node; + const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; const fieldName = ast.toElasticsearchQuery(fieldNameArg); const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; const type = isPhraseArg.value ? 'phrase' : 'best_fields'; diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/packages/kbn-es-query/src/kuery/index.d.ts index 9d797406420d41..b01a8914f68ef3 100644 --- a/packages/kbn-es-query/src/kuery/index.d.ts +++ b/packages/kbn-es-query/src/kuery/index.d.ts @@ -18,3 +18,13 @@ */ export * from './ast'; +export { nodeTypes } from './node_types'; + +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonObject { + [key: string]: JsonValue; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface JsonArray extends Array {} diff --git a/packages/kbn-es-query/src/kuery/index.js b/packages/kbn-es-query/src/kuery/index.js index 84c6a205b42ce6..08fa9829d4a566 100644 --- a/packages/kbn-es-query/src/kuery/index.js +++ b/packages/kbn-es-query/src/kuery/index.js @@ -19,5 +19,5 @@ export * from './ast'; export * from './filter_migration'; -export * from './node_types'; +export { nodeTypes } from './node_types'; export * from './errors'; diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/packages/kbn-es-query/src/kuery/node_types/index.d.ts new file mode 100644 index 00000000000000..0d1f2c28e39f08 --- /dev/null +++ b/packages/kbn-es-query/src/kuery/node_types/index.d.ts @@ -0,0 +1,76 @@ +/* + * 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. + */ + +/** + * WARNING: these typings are incomplete + */ + +import { JsonObject, JsonValue } from '..'; + +type FunctionName = + | 'is' + | 'and' + | 'or' + | 'not' + | 'range' + | 'exists' + | 'geoBoundingBox' + | 'geoPolygon'; + +interface FunctionTypeBuildNode { + type: 'function'; + function: FunctionName; + // TODO -> Need to define a better type for DSL query + arguments: any[]; +} + +interface FunctionType { + buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue; +} + +interface LiteralType { + buildNode: ( + value: null | boolean | number | string + ) => { type: 'literal'; value: null | boolean | number | string }; + toElasticsearchQuery: (node: any) => null | boolean | number | string; +} + +interface NamedArgType { + buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any }; + toElasticsearchQuery: (node: any) => string; +} + +interface WildcardType { + buildNode: (value: string) => { type: 'wildcard'; value: string }; + test: (node: any, string: string) => boolean; + toElasticsearchQuery: (node: any) => string; + toQueryStringQuery: (node: any) => string; + hasLeadingWildcard: (node: any) => boolean; +} + +interface NodeTypes { + function: FunctionType; + literal: LiteralType; + namedArg: NamedArgType; + wildcard: WildcardType; +} + +export const nodeTypes: NodeTypes; diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index afd9f8e4a38209..f24b0ed1681aab 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -19,7 +19,7 @@ import Url from 'url'; -import React, { Component, createRef, Fragment } from 'react'; +import React, { Component, createRef } from 'react'; import * as Rx from 'rxjs'; import { @@ -376,7 +376,7 @@ class HeaderUI extends Component { ]; return ( - +
@@ -407,11 +407,13 @@ class HeaderUI extends Component { isLocked={isLocked} onIsLockedUpdate={onIsLockedUpdate} > - - - + - +
); } diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index f4b1c1d49cd27f..68eb6a54f48a3c 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -64,7 +64,11 @@ export class HeaderBreadcrumbs extends Component { public render() { return ( - + ); } diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index 2dc2b2ef06094f..33221522fa83ca 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -48,7 +48,7 @@ export class NotificationsService { public setup({ uiSettings }: SetupDeps): NotificationsSetup { const notificationSetup = { toasts: this.toasts.setup({ uiSettings }) }; - this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => { + this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe((error: Error) => { notificationSetup.toasts.addDanger({ title: i18n.translate('core.notifications.unableUpdateUISettingNotificationMessageTitle', { defaultMessage: 'Unable to update UI setting', diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b2d730d7fa4670..102e77b564a6d6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -752,7 +752,7 @@ export class SavedObjectsClient { }[]) => Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } @@ -775,6 +775,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // (undocumented) + filter?: string; + // (undocumented) hasReference?: { type: string; id: string; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index dc13d001643a31..cf0300157aece3 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -297,6 +297,7 @@ export class SavedObjectsClient { searchFields: 'search_fields', sortField: 'sort_field', type: 'type', + filter: 'filter', }; const renamedQuery = renameKeys(renameMap, options); diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 685ce51bc7d29c..dbf35ff4e134d7 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -56,6 +56,7 @@ export { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, SavedObjectsErrorHelpers, + SavedObjectsCacheIndexPatterns, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts new file mode 100644 index 00000000000000..e3aeca42d1cf07 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; + +const mockGetFieldsForWildcard = jest.fn(); +const mockIndexPatternsService: jest.Mock = jest.fn().mockImplementation(() => ({ + getFieldsForWildcard: mockGetFieldsForWildcard, + getFieldsForTimePattern: jest.fn(), +})); + +describe('SavedObjectsRepository', () => { + let cacheIndexPatterns: SavedObjectsCacheIndexPatterns; + + const fields = [ + { + aggregatable: true, + name: 'config.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'foo.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'bar.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'baz.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'dashboard.otherField', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'hiddenType.someField', + searchable: true, + type: 'string', + }, + ]; + + beforeEach(() => { + cacheIndexPatterns = new SavedObjectsCacheIndexPatterns(); + jest.clearAllMocks(); + }); + + it('setIndexPatterns should return an error object when indexPatternsService is undefined', async () => { + try { + await cacheIndexPatterns.setIndexPatterns('test-index'); + } catch (error) { + expect(error.message).toMatch('indexPatternsService is not defined'); + } + }); + + it('setIndexPatterns should return an error object if getFieldsForWildcard is not defined', async () => { + mockGetFieldsForWildcard.mockImplementation(() => { + throw new Error('something happen'); + }); + try { + cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); + await cacheIndexPatterns.setIndexPatterns('test-index'); + } catch (error) { + expect(error.message).toMatch('Index Pattern Error - something happen'); + } + }); + + it('setIndexPatterns should return empty array when getFieldsForWildcard is returning null or undefined', async () => { + mockGetFieldsForWildcard.mockImplementation(() => null); + cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); + await cacheIndexPatterns.setIndexPatterns('test-index'); + expect(cacheIndexPatterns.getIndexPatterns()).toEqual(undefined); + }); + + it('setIndexPatterns should return index pattern when getFieldsForWildcard is returning fields', async () => { + mockGetFieldsForWildcard.mockImplementation(() => fields); + cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); + await cacheIndexPatterns.setIndexPatterns('test-index'); + expect(cacheIndexPatterns.getIndexPatterns()).toEqual({ fields, title: 'test-index' }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts new file mode 100644 index 00000000000000..e96cf996f504c3 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts @@ -0,0 +1,82 @@ +/* + * 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 { FieldDescriptor } from 'src/legacy/server/index_patterns/service/index_patterns_service'; +import { IndexPatternsService } from 'src/legacy/server/index_patterns'; + +export interface SavedObjectsIndexPatternField { + name: string; + type: string; + aggregatable: boolean; + searchable: boolean; +} + +export interface SavedObjectsIndexPattern { + fields: SavedObjectsIndexPatternField[]; + title: string; +} + +export class SavedObjectsCacheIndexPatterns { + private _indexPatterns: SavedObjectsIndexPattern | undefined = undefined; + private _indexPatternsService: IndexPatternsService | undefined = undefined; + + public setIndexPatternsService(indexPatternsService: IndexPatternsService) { + this._indexPatternsService = indexPatternsService; + } + + public getIndexPatternsService() { + return this._indexPatternsService; + } + + public getIndexPatterns(): SavedObjectsIndexPattern | undefined { + return this._indexPatterns; + } + + public async setIndexPatterns(index: string) { + await this._getIndexPattern(index); + } + + private async _getIndexPattern(index: string) { + try { + if (this._indexPatternsService == null) { + throw new TypeError('indexPatternsService is not defined'); + } + const fieldsDescriptor: FieldDescriptor[] = await this._indexPatternsService.getFieldsForWildcard( + { + pattern: index, + } + ); + + this._indexPatterns = + fieldsDescriptor && Array.isArray(fieldsDescriptor) && fieldsDescriptor.length > 0 + ? { + fields: fieldsDescriptor.map(field => ({ + aggregatable: field.aggregatable, + name: field.name, + searchable: field.searchable, + type: field.type, + })), + title: index, + } + : undefined; + } catch (err) { + throw new Error(`Index Pattern Error - ${err.message}`); + } + } +} diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts new file mode 100644 index 00000000000000..73a0804512ed10 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -0,0 +1,457 @@ +/* + * 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 { fromKueryExpression } from '@kbn/es-query'; + +import { + validateFilterKueryNode, + getSavedObjectTypeIndexPatterns, + validateConvertFilterToKueryNode, +} from './filter_utils'; +import { SavedObjectsIndexPattern } from './cache_index_patterns'; + +const mockIndexPatterns: SavedObjectsIndexPattern = { + fields: [ + { + name: 'updatedAt', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.title', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'bar.foo', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'bar.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'hiddentype.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + ], + title: 'mock', +}; + +describe('Filter Utils', () => { + describe('#validateConvertFilterToKueryNode', () => { + test('Validate a simple filter', () => { + expect( + validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockIndexPatterns) + ).toEqual(fromKueryExpression('foo.title: "best"')); + }); + test('Assemble filter kuery node saved object attributes with one saved object type', () => { + expect( + validateConvertFilterToKueryNode( + ['foo'], + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', + mockIndexPatterns + ) + ).toEqual( + fromKueryExpression( + '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' + ) + ); + }); + + test('Assemble filter with one type kuery node saved object attributes with multiple saved object type', () => { + expect( + validateConvertFilterToKueryNode( + ['foo', 'bar'], + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', + mockIndexPatterns + ) + ).toEqual( + fromKueryExpression( + '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' + ) + ); + }); + + test('Assemble filter with two types kuery node saved object attributes with multiple saved object type', () => { + expect( + validateConvertFilterToKueryNode( + ['foo', 'bar'], + '(bar.updatedAt: 5678654567 OR foo.updatedAt: 5678654567) and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or bar.attributes.description :*)', + mockIndexPatterns + ) + ).toEqual( + fromKueryExpression( + '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' + ) + ); + }); + + test('Lets make sure that we are throwing an exception if we get an error', () => { + expect(() => { + validateConvertFilterToKueryNode( + ['foo', 'bar'], + 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', + mockIndexPatterns + ); + }).toThrowErrorMatchingInlineSnapshot( + `"This key 'updatedAt' need to be wrapped by a saved object type like foo,bar: Bad Request"` + ); + }); + + test('Lets make sure that we are throwing an exception if we are using hiddentype with types', () => { + expect(() => { + validateConvertFilterToKueryNode([], 'hiddentype.title: "title"', mockIndexPatterns); + }).toThrowErrorMatchingInlineSnapshot(`"This type hiddentype is not allowed: Bad Request"`); + }); + }); + + describe('#validateFilterKueryNode', () => { + test('Validate filter query through KueryNode - happy path', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updatedAt', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if key is not wrapper by a saved object type', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: "This key 'updatedAt' need to be wrapped by a saved object type like foo", + isSavedObjectAttr: true, + key: 'updatedAt', + type: null, + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if key of a saved object type is not wrapped with attributes', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updatedAt', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: + "This key 'foo.bytes' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: + "This key 'foo.description' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if filter is not using an allowed type', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: 'This type bar is not allowed', + isSavedObjectAttr: true, + key: 'bar.updatedAt', + type: 'bar', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: "This key 'foo.updatedAt33' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.updatedAt33', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: + "This key 'foo.attributes.header' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.attributes.header', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + }); + + describe('#getSavedObjectTypeIndexPatterns', () => { + test('Get index patterns related to your type', () => { + const indexPatternsFilterByType = getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns); + + expect(indexPatternsFilterByType).toEqual([ + { + name: 'updatedAt', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.title', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts new file mode 100644 index 00000000000000..2397971e66966f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -0,0 +1,190 @@ +/* + * 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 { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; +import { get, set } from 'lodash'; + +import { SavedObjectsIndexPattern, SavedObjectsIndexPatternField } from './cache_index_patterns'; +import { SavedObjectsErrorHelpers } from './errors'; + +export const validateConvertFilterToKueryNode = ( + types: string[], + filter: string, + indexPattern: SavedObjectsIndexPattern | undefined +): KueryNode => { + if (filter && filter.length > 0 && indexPattern) { + const filterKueryNode = fromKueryExpression(filter); + + const typeIndexPatterns = getSavedObjectTypeIndexPatterns(types, indexPattern); + const validationFilterKuery = validateFilterKueryNode( + filterKueryNode, + types, + typeIndexPatterns, + filterKueryNode.type === 'function' && ['is', 'range'].includes(filterKueryNode.function) + ); + + if (validationFilterKuery.length === 0) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'If we have a filter options defined, we should always have validationFilterKuery defined too' + ); + } + + if (validationFilterKuery.some(obj => obj.error != null)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + validationFilterKuery + .filter(obj => obj.error != null) + .map(obj => obj.error) + .join('\n') + ); + } + + validationFilterKuery.forEach(item => { + const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.'); + const existingKueryNode: KueryNode = + path.length === 0 ? filterKueryNode : get(filterKueryNode, path); + if (item.isSavedObjectAttr) { + existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; + const itemType = types.filter(t => t === item.type); + if (itemType.length === 1) { + set( + filterKueryNode, + path, + nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'type', itemType[0]), + existingKueryNode, + ]) + ); + } + } else { + existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.replace( + '.attributes', + '' + ); + set(filterKueryNode, path, existingKueryNode); + } + }); + return filterKueryNode; + } + return null; +}; + +export const getSavedObjectTypeIndexPatterns = ( + types: string[], + indexPattern: SavedObjectsIndexPattern | undefined +): SavedObjectsIndexPatternField[] => { + return indexPattern != null + ? indexPattern.fields.filter( + ip => + !ip.name.includes('.') || (ip.name.includes('.') && types.includes(ip.name.split('.')[0])) + ) + : []; +}; + +interface ValidateFilterKueryNode { + astPath: string; + error: string; + isSavedObjectAttr: boolean; + key: string; + type: string | null; +} + +export const validateFilterKueryNode = ( + astFilter: KueryNode, + types: string[], + typeIndexPatterns: SavedObjectsIndexPatternField[], + storeValue: boolean = false, + path: string = 'arguments' +): ValidateFilterKueryNode[] => { + return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode( + ast, + types, + typeIndexPatterns, + ast.type === 'function' && ['is', 'range'].includes(ast.function), + `${myPath}.arguments` + ), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + return [ + ...kueryNode, + { + astPath: splitPath.slice(0, splitPath.length - 1).join('.'), + error: hasFilterKeyError(ast.value, types, typeIndexPatterns), + isSavedObjectAttr: isSavedObjectAttr(ast.value, typeIndexPatterns), + key: ast.value, + type: getType(ast.value), + }, + ]; + } + return kueryNode; + }, []); +}; + +const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null); + +export const isSavedObjectAttr = ( + key: string, + typeIndexPatterns: SavedObjectsIndexPatternField[] +) => { + const splitKey = key.split('.'); + if (splitKey.length === 1 && typeIndexPatterns.some(tip => tip.name === splitKey[0])) { + return true; + } else if (splitKey.length > 1 && typeIndexPatterns.some(tip => tip.name === splitKey[1])) { + return true; + } + return false; +}; + +export const hasFilterKeyError = ( + key: string, + types: string[], + typeIndexPatterns: SavedObjectsIndexPatternField[] +): string | null => { + if (!key.includes('.')) { + return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`; + } else if (key.includes('.')) { + const keySplit = key.split('.'); + if (keySplit.length <= 1 || !types.includes(keySplit[0])) { + return `This type ${keySplit[0]} is not allowed`; + } + if ( + (keySplit.length === 2 && typeIndexPatterns.some(tip => tip.name === key)) || + (keySplit.length > 2 && types.includes(keySplit[0]) && keySplit[1] !== 'attributes') + ) { + return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`; + } + if ( + (keySplit.length === 2 && !typeIndexPatterns.some(tip => tip.name === keySplit[1])) || + (keySplit.length > 2 && + !typeIndexPatterns.some( + tip => + tip.name === [...keySplit.slice(0, 1), ...keySplit.slice(2, keySplit.length)].join('.') + )) + ) { + return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`; + } + } + return null; +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index d987737c2ffa09..be78fdde762106 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -26,3 +26,5 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; + +export { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index c35502b719d58c..bc646c8c1d2e14 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -18,6 +18,7 @@ */ import { delay } from 'bluebird'; + import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; import { SavedObjectsErrorHelpers } from './errors'; @@ -272,6 +273,10 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', + cacheIndexPatterns: { + setIndexPatterns: jest.fn(), + getIndexPatterns: () => undefined, + }, mappings, callCluster: callAdminCluster, migrator, @@ -285,7 +290,7 @@ describe('SavedObjectsRepository', () => { getSearchDslNS.getSearchDsl.mockReset(); }); - afterEach(() => {}); + afterEach(() => { }); describe('#create', () => { beforeEach(() => { @@ -993,7 +998,7 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); - it('should return objects in the same order regardless of type', () => {}); + it('should return objects in the same order regardless of type', () => { }); }); describe('#delete', () => { @@ -1154,6 +1159,13 @@ describe('SavedObjectsRepository', () => { } }); + it('requires index pattern to be defined if filter is defined', async () => { + callAdminCluster.mockReturnValue(noNamespaceSearchResults); + expect(savedObjectsRepository.find({ type: 'foo', filter: 'foo.type: hello' })) + .rejects + .toThrowErrorMatchingInlineSnapshot('"options.filter is missing index pattern to work correctly: Bad Request"'); + }); + it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', async () => { callAdminCluster.mockReturnValue(namespacedSearchResults); @@ -1169,6 +1181,8 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, + indexPattern: undefined, + kueryNode: null, }; await savedObjectsRepository.find(relevantOpts); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 3c2a644f003bda..aadb82486cccec 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -19,11 +19,13 @@ import { omit } from 'lodash'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; import { SavedObjectsErrorHelpers } from './errors'; +import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; @@ -45,6 +47,7 @@ import { SavedObjectsFindOptions, SavedObjectsMigrationVersion, } from '../../types'; +import { validateConvertFilterToKueryNode } from './filter_utils'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -74,6 +77,7 @@ export interface SavedObjectsRepositoryOptions { serializer: SavedObjectsSerializer; migrator: KibanaMigrator; allowedTypes: string[]; + cacheIndexPatterns: SavedObjectsCacheIndexPatterns; onBeforeWrite?: (...args: Parameters) => Promise; } @@ -91,11 +95,13 @@ export class SavedObjectsRepository { private _onBeforeWrite: (...args: Parameters) => Promise; private _unwrappedCallCluster: CallCluster; private _serializer: SavedObjectsSerializer; + private _cacheIndexPatterns: SavedObjectsCacheIndexPatterns; constructor(options: SavedObjectsRepositoryOptions) { const { index, config, + cacheIndexPatterns, mappings, callCluster, schema, @@ -106,7 +112,7 @@ export class SavedObjectsRepository { } = options; // It's important that we migrate documents / mark them as up-to-date - // prior to writing them to the index. Otherwise, we'll cause unecessary + // prior to writing them to the index. Otherwise, we'll cause unnecessary // index migrations to run at Kibana startup, and those will probably fail // due to invalidly versioned documents in the index. // @@ -117,6 +123,7 @@ export class SavedObjectsRepository { this._config = config; this._mappings = mappings; this._schema = schema; + this._cacheIndexPatterns = cacheIndexPatterns; if (allowedTypes.length === 0) { throw new Error('Empty or missing types for saved object repository!'); } @@ -126,6 +133,9 @@ export class SavedObjectsRepository { this._unwrappedCallCluster = async (...args: Parameters) => { await migrator.runMigrations(); + if (this._cacheIndexPatterns.getIndexPatterns() == null) { + await this._cacheIndexPatterns.setIndexPatterns(index); + } return callCluster(...args); }; this._schema = schema; @@ -404,9 +414,12 @@ export class SavedObjectsRepository { fields, namespace, type, + filter, }: SavedObjectsFindOptions): Promise> { if (!type) { - throw new TypeError(`options.type must be a string or an array of strings`); + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.type must be a string or an array of strings' + ); } const types = Array.isArray(type) ? type : [type]; @@ -421,13 +434,28 @@ export class SavedObjectsRepository { } if (searchFields && !Array.isArray(searchFields)) { - throw new TypeError('options.searchFields must be an array'); + throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array'); } if (fields && !Array.isArray(fields)) { - throw new TypeError('options.fields must be an array'); + throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array'); } + if (filter && filter !== '' && this._cacheIndexPatterns.getIndexPatterns() == null) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.filter is missing index pattern to work correctly' + ); + } + + const kueryNode = + filter && filter !== '' + ? validateConvertFilterToKueryNode( + allowedTypes, + filter, + this._cacheIndexPatterns.getIndexPatterns() + ) + : null; + const esOptions = { index: this.getIndicesForTypes(allowedTypes), size: perPage, @@ -446,6 +474,8 @@ export class SavedObjectsRepository { sortOrder, namespace, hasReference, + indexPattern: kueryNode != null ? this._cacheIndexPatterns.getIndexPatterns() : undefined, + kueryNode, }), }, }; @@ -769,7 +799,7 @@ export class SavedObjectsRepository { // The internal representation of the saved object that the serializer returns // includes the namespace, and we use this for migrating documents. However, we don't - // want the namespcae to be returned from the repository, as the repository scopes each + // want the namespace to be returned from the repository, as the repository scopes each // method transparently to the specified namespace. private _rawToSavedObject(raw: RawDoc): SavedObject { const savedObject = this._serializer.rawToSavedObject(raw); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index b13d86819716be..75b30580292279 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -18,6 +18,7 @@ */ import { schemaMock } from '../../../schema/schema.mock'; +import { SavedObjectsIndexPattern } from '../cache_index_patterns'; import { getQueryParams } from './query_params'; const SCHEMA = schemaMock.create(); @@ -61,6 +62,41 @@ const MAPPINGS = { }, }, }; +const INDEX_PATTERN: SavedObjectsIndexPattern = { + fields: [ + { + aggregatable: true, + name: 'type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'pending.title', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'saved.title', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'saved.obj.key1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'global.name', + searchable: true, + type: 'string', + }, + ], + title: 'test', +}; // create a type clause to be used within the "should", if a namespace is specified // the clause will ensure the namespace matches; otherwise, the clause will ensure @@ -85,7 +121,7 @@ const createTypeClause = (type: string, namespace?: string) => { describe('searchDsl/queryParams', () => { describe('no parameters', () => { it('searches for all known types without a namespace specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA)).toEqual({ + expect(getQueryParams({ mappings: MAPPINGS, schema: SCHEMA })).toEqual({ query: { bool: { filter: [ @@ -108,7 +144,9 @@ describe('searchDsl/queryParams', () => { describe('namespace', () => { it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace')).toEqual({ + expect( + getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: 'foo-namespace' }) + ).toEqual({ query: { bool: { filter: [ @@ -131,7 +169,9 @@ describe('searchDsl/queryParams', () => { describe('type (singular, namespaced)', () => { it('includes a terms filter for type and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'saved')).toEqual({ + expect( + getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'saved' }) + ).toEqual({ query: { bool: { filter: [ @@ -150,7 +190,9 @@ describe('searchDsl/queryParams', () => { describe('type (singular, global)', () => { it('includes a terms filter for type and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'global')).toEqual({ + expect( + getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'global' }) + ).toEqual({ query: { bool: { filter: [ @@ -169,7 +211,14 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global)', () => { it('includes term filters for types and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -188,7 +237,14 @@ describe('searchDsl/queryParams', () => { describe('namespace, type (plural, namespaced and global)', () => { it('includes a terms filter for type and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -207,7 +263,15 @@ describe('searchDsl/queryParams', () => { describe('search', () => { it('includes a sqs query and all known types without a namespace specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'us*')).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'us*', + }) + ).toEqual({ query: { bool: { filter: [ @@ -239,7 +303,15 @@ describe('searchDsl/queryParams', () => { describe('namespace, search', () => { it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'us*')).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'us*', + }) + ).toEqual({ query: { bool: { filter: [ @@ -271,7 +343,15 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), search', () => { it('includes a sqs query and types without a namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'us*')).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'us*', + }) + ).toEqual({ query: { bool: { filter: [ @@ -299,40 +379,52 @@ describe('searchDsl/queryParams', () => { describe('namespace, type (plural, namespaced and global), search', () => { it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'us*')).toEqual( - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'us*', + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], + minimum_should_match: 1, }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, + }, + ], + must: [ + { + simple_query_string: { + query: 'us*', + lenient: true, + fields: ['*'], }, - ], - }, + }, + ], }, - } - ); + }, + }); }); }); describe('search, searchFields', () => { it('includes all types for field', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'y*', + searchFields: ['title'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -360,7 +452,16 @@ describe('searchDsl/queryParams', () => { }); }); it('supports field boosting', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title^3'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'y*', + searchFields: ['title^3'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -389,7 +490,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title', 'title.raw']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -428,38 +536,52 @@ describe('searchDsl/queryParams', () => { describe('namespace, search, searchFields', () => { it('includes all types for field', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title'])).toEqual( - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'y*', + searchFields: ['title'], + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1, }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, + }, + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: ['pending.title', 'saved.title', 'global.title'], }, - ], - }, + }, + ], }, - } - ); + }, + }); }); it('supports field boosting', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title^3']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'y*', + searchFields: ['title^3'], + }) ).toEqual({ query: { bool: { @@ -489,7 +611,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title', 'title.raw']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -529,7 +658,14 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), search, searchFields', () => { it('includes all types for field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title'], + }) ).toEqual({ query: { bool: { @@ -555,7 +691,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field boosting', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title^3']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title^3'], + }) ).toEqual({ query: { bool: { @@ -581,10 +724,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', [ - 'title', - 'title.raw', - ]) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -613,7 +760,14 @@ describe('searchDsl/queryParams', () => { describe('namespace, type (plural, namespaced and global), search, searchFields', () => { it('includes all types for field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title'], + }) ).toEqual({ query: { bool: { @@ -639,7 +793,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field boosting', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title^3']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title^3'], + }) ).toEqual({ query: { bool: { @@ -665,10 +826,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', [ - 'title', - 'title.raw', - ]) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -697,15 +862,15 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), search, defaultSearchOperator', () => { it('supports defaultSearchOperator', () => { expect( - getQueryParams( - MAPPINGS, - SCHEMA, - 'foo-namespace', - ['saved', 'global'], - 'foo', - undefined, - 'AND' - ) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'foo', + searchFields: undefined, + defaultSearchOperator: 'AND', + }) ).toEqual({ query: { bool: { @@ -771,19 +936,19 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), hasReference', () => { it('supports hasReference', () => { expect( - getQueryParams( - MAPPINGS, - SCHEMA, - 'foo-namespace', - ['saved', 'global'], - undefined, - undefined, - 'OR', - { + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: undefined, + searchFields: undefined, + defaultSearchOperator: 'OR', + hasReference: { type: 'bar', id: '1', - } - ) + }, + }) ).toEqual({ query: { bool: { @@ -823,4 +988,345 @@ describe('searchDsl/queryParams', () => { }); }); }); + + describe('type filter', () => { + it(' with namespace', () => { + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + kueryNode: { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + }, + indexPattern: INDEX_PATTERN, + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'global.name': 'GLOBAL', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + term: { + type: 'pending', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }); + }); + it(' with namespace and more complex filter', () => { + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + kueryNode: { + type: 'function', + function: 'and', + arguments: [ + { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + }, + { + type: 'function', + function: 'not', + arguments: [ + { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'saved.obj.key1' }, + { type: 'literal', value: 'key' }, + { type: 'literal', value: true }, + ], + }, + ], + }, + ], + }, + indexPattern: INDEX_PATTERN, + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'global.name': 'GLOBAL', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'saved.obj.key1': 'key', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + term: { + type: 'pending', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }); + }); + it(' with search and searchFields', () => { + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + search: 'y*', + searchFields: ['title'], + kueryNode: { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + }, + indexPattern: INDEX_PATTERN, + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'global.name': 'GLOBAL', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + term: { + type: 'pending', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: ['pending.title', 'saved.title', 'global.title'], + }, + }, + ], + }, + }, + }); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 9c145258a755ff..125b0c40af9e41 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,9 +16,11 @@ * specific language governing permissions and limitations * under the License. */ +import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; +import { SavedObjectsIndexPattern } from '../cache_index_patterns'; /** * Gets the types based on the type. Uses mappings to support @@ -76,25 +78,43 @@ function getClauseForType(schema: SavedObjectsSchema, namespace: string | undefi }; } +interface HasReferenceQueryParams { + type: string; + id: string; +} + +interface QueryParams { + mappings: IndexMapping; + schema: SavedObjectsSchema; + namespace?: string; + type?: string | string[]; + search?: string; + searchFields?: string[]; + defaultSearchOperator?: string; + hasReference?: HasReferenceQueryParams; + kueryNode?: KueryNode; + indexPattern?: SavedObjectsIndexPattern; +} + /** * Get the "query" related keys for the search body */ -export function getQueryParams( - mappings: IndexMapping, - schema: SavedObjectsSchema, - namespace?: string, - type?: string | string[], - search?: string, - searchFields?: string[], - defaultSearchOperator?: string, - hasReference?: { - type: string; - id: string; - } -) { +export function getQueryParams({ + mappings, + schema, + namespace, + type, + search, + searchFields, + defaultSearchOperator, + hasReference, + kueryNode, + indexPattern, +}: QueryParams) { const types = getTypes(mappings, type); const bool: any = { filter: [ + ...(kueryNode != null ? [toElasticsearchQuery(kueryNode, indexPattern)] : []), { bool: { must: hasReference diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index 7bd04ca8f34947..97cab3e566d5ea 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -72,16 +72,16 @@ describe('getSearchDsl', () => { getSearchDsl(MAPPINGS, SCHEMA, opts); expect(getQueryParams).toHaveBeenCalledTimes(1); - expect(getQueryParams).toHaveBeenCalledWith( - MAPPINGS, - SCHEMA, - opts.namespace, - opts.type, - opts.search, - opts.searchFields, - opts.defaultSearchOperator, - opts.hasReference - ); + expect(getQueryParams).toHaveBeenCalledWith({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: opts.namespace, + type: opts.type, + search: opts.search, + searchFields: opts.searchFields, + defaultSearchOperator: opts.defaultSearchOperator, + hasReference: opts.hasReference, + }); }); it('passes (mappings, type, sortField, sortOrder) to getSortingParams', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 1c2c87bca6ea72..68f60607025053 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -17,12 +17,14 @@ * under the License. */ +import { KueryNode } from '@kbn/es-query'; import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +import { SavedObjectsIndexPattern } from '../cache_index_patterns'; interface GetSearchDslOptions { type: string | string[]; @@ -36,6 +38,8 @@ interface GetSearchDslOptions { type: string; id: string; }; + kueryNode?: KueryNode; + indexPattern?: SavedObjectsIndexPattern; } export function getSearchDsl( @@ -52,6 +56,8 @@ export function getSearchDsl( sortOrder, namespace, hasReference, + kueryNode, + indexPattern, } = options; if (!type) { @@ -63,7 +69,7 @@ export function getSearchDsl( } return { - ...getQueryParams( + ...getQueryParams({ mappings, schema, namespace, @@ -71,8 +77,10 @@ export function getSearchDsl( search, searchFields, defaultSearchOperator, - hasReference - ), + hasReference, + kueryNode, + indexPattern, + }), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 1cc424199b8872..e7e7a4c64392a6 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -123,6 +123,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { searchFields?: string[]; hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; + filter?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4ae1c0c267ea94..ae839644fc2e29 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -10,6 +10,7 @@ import { ConfigOptions } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { IncomingHttpHeaders } from 'http'; +import { IndexPatternsService } from 'src/legacy/server/index_patterns'; import { KibanaConfigType } from 'src/core/server/kibana_config'; import { Logger as Logger_2 } from 'src/core/server/logging'; import { ObjectType } from '@kbn/config-schema'; @@ -841,6 +842,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // (undocumented) + filter?: string; + // (undocumented) hasReference?: { type: string; id: string; diff --git a/src/dev/run_check_core_api_changes.ts b/src/dev/run_check_core_api_changes.ts index 4d0be7f3884665..d2c75c86ce7442 100644 --- a/src/dev/run_check_core_api_changes.ts +++ b/src/dev/run_check_core_api_changes.ts @@ -139,14 +139,51 @@ const runApiExtractor = ( return Extractor.invoke(config, options); }; -async function run(folder: string): Promise { +interface Options { + accept: boolean; + docs: boolean; + help: boolean; +} + +async function run( + folder: string, + { log, opts }: { log: ToolingLog; opts: Options } +): Promise { + log.info(`Core ${folder} API: checking for changes in API signature...`); + + const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); + + // If we're not accepting changes and there's a failure, exit. + if (!opts.accept && !succeeded) { + return false; + } + + // Attempt to generate docs even if api-extractor didn't succeed + if ((opts.accept && apiReportChanged) || opts.docs) { + try { + await renameExtractedApiPackageName(folder); + await runApiDocumenter(folder); + } catch (e) { + log.error(e); + return false; + } + log.info(`Core ${folder} API: updated documentation ✔`); + } + + // If the api signature changed or any errors or warnings occured, exit with an error + // NOTE: Because of https://github.com/Microsoft/web-build-tools/issues/1258 + // api-extractor will not return `succeeded: false` when the API changes. + return !apiReportChanged && succeeded; +} + +(async () => { const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); const extraFlags: string[] = []; - const opts = getopts(process.argv.slice(2), { + const opts = (getopts(process.argv.slice(2), { boolean: ['accept', 'docs', 'help'], default: { project: undefined, @@ -155,7 +192,7 @@ async function run(folder: string): Promise { extraFlags.push(name); return false; }, - }); + }) as any) as Options; if (extraFlags.length > 0) { for (const flag of extraFlags) { @@ -193,45 +230,18 @@ async function run(folder: string): Promise { return !(extraFlags.length > 0); } - log.info(`Core ${folder} API: checking for changes in API signature...`); - try { + log.info(`Core: Building types...`); await runBuildTypes(); } catch (e) { log.error(e); return false; } - const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); - - // If we're not accepting changes and there's a failure, exit. - if (!opts.accept && !succeeded) { - return false; - } - - // Attempt to generate docs even if api-extractor didn't succeed - if ((opts.accept && apiReportChanged) || opts.docs) { - try { - await renameExtractedApiPackageName(folder); - await runApiDocumenter(folder); - } catch (e) { - log.error(e); - return false; - } - log.info(`Core ${folder} API: updated documentation ✔`); - } - - // If the api signature changed or any errors or warnings occured, exit with an error - // NOTE: Because of https://github.com/Microsoft/web-build-tools/issues/1258 - // api-extractor will not return `succeeded: false` when the API changes. - return !apiReportChanged && succeeded; -} - -(async () => { - const publicSucceeded = await run('public'); - const serverSucceeded = await run('server'); + const folders = ['public', 'server']; + const results = await Promise.all(folders.map(folder => run(folder, { log, opts }))); - if (!publicSucceeded || !serverSucceeded) { + if (results.find(r => r === false) !== undefined) { process.exitCode = 1; } })(); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index d7b369cc264816..82256cf7398820 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { debounce } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; @@ -52,8 +52,6 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); - const containerRef = useRef(null); - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ INITIAL_PANEL_WIDTH, INITIAL_PANEL_WIDTH, @@ -71,9 +69,9 @@ export function Main() { }; return ( -
+ <> setShowSettings(false)} /> : null} {showHelp ? setShowHelp(false)} /> : null} -
+ ); } diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 8e2d640a595665..de9ffe50c3406a 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,6 +1,7 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). #consoleRoot { - height: 100%; + display: flex; + flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container // SASSTODO: Uncomment when tooltips are EUI-ified (inside portals) overflow: hidden; diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index c3618d412f4258..80104fc1991b09 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -45,4 +45,7 @@ export const setup = dataPlugin.setup(npSetup.core, { __LEGACY: legacyPlugin.setup(), }); -export const start = dataPlugin.start(npStart.core); +export const start = dataPlugin.start(npStart.core, { + data: npStart.plugins.data, + __LEGACY: legacyPlugin.start(), +}); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index aec97b02bc2b9f..a5aa55673cac66 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,12 +18,16 @@ */ import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { SearchService, SearchSetup } from './search'; +import { SearchService, SearchSetup, createSearchBar, StatetfulSearchBarProps } from './search'; import { QueryService, QuerySetup } from './query'; import { FilterService, FilterSetup } from './filter'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; -import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin'; +import { + LegacyDependenciesPluginSetup, + LegacyDependenciesPluginStart, +} from './shim/legacy_dependencies_plugin'; +import { DataPublicPluginStart } from '../../../../plugins/data/public'; /** * Interface for any dependencies on other plugins' `setup` contracts. @@ -34,6 +38,11 @@ export interface DataPluginSetupDependencies { __LEGACY: LegacyDependenciesPluginSetup; } +export interface DataPluginStartDependencies { + data: DataPublicPluginStart; + __LEGACY: LegacyDependenciesPluginStart; +} + /** * Interface for this plugin's returned `setup` contract. * @@ -47,6 +56,22 @@ export interface DataSetup { timefilter: TimefilterSetup; } +/** + * Interface for this plugin's returned `start` contract. + * + * @public + */ +export interface DataStart { + indexPatterns: IndexPatternsSetup; + filter: FilterSetup; + query: QuerySetup; + search: SearchSetup; + timefilter: TimefilterSetup; + ui: { + SearchBar: React.ComponentType; + }; +} + /** * Data Plugin - public * @@ -58,7 +83,9 @@ export interface DataSetup { * in the setup/start interfaces. The remaining items exported here are either types, * or static code. */ -export class DataPlugin implements Plugin { +export class DataPlugin + implements + Plugin { // Exposed services, sorted alphabetically private readonly filter: FilterService = new FilterService(); private readonly indexPatterns: IndexPatternsService = new IndexPatternsService(); @@ -96,9 +123,20 @@ export class DataPlugin implements Plugin + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-expanded={false} + aria-haspopup="true" + aria-label="Search and filter the test page" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-autocomplete="list" + aria-label="Start typing to search and filter the test page" + autoComplete="off" + autoFocus={false} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - KQL - - - - - -
-
- -
-
-
-
-
-
-
- -
-
- +
+ + + +
+ + + + + + + + + + + + + + + + + +
+ `; exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-expanded={false} + aria-haspopup="true" + aria-label="Search and filter the test page" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-autocomplete="list" + aria-label="Start typing to search and filter the test page" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ - + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - Lucene - - - - - -
-
- -
-
-
-
-
-
-
- -
-
-
+
+ + + +
+ + + + + + + + + + + + + + + + + + + `; exports[`QueryBarInput Should render the given query 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-expanded={false} + aria-haspopup="true" + aria-label="Search and filter the test page" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-autocomplete="list" + aria-label="Start typing to search and filter the test page" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - KQL - - - - - -
-
- -
-
-
-
-
-
-
- -
-
-
+
+ + + +
+ + + + + + + + + + + + + + + + + + + `; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx index e66d71b9b08b4b..a66fb682063ece 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx @@ -25,12 +25,14 @@ import { import { EuiFieldText } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryBarInput, QueryBarInputUI } from './query_bar_input'; import { coreMock } from '../../../../../../../core/public/mocks'; const startMock = coreMock.createStart(); import { IndexPattern } from '../../../index'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { mount } from 'enzyme'; const noop = () => { return; @@ -78,64 +80,67 @@ const mockIndexPattern = { ], } as IndexPattern; +function wrapQueryBarInputInContext(testProps: any, store?: any) { + const defaultOptions = { + screenTitle: 'Another Screen', + intl: null as any, + }; + + const services = { + appName: testProps.appName || 'test', + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + store: store || createMockStorage(), + }; + + return ( + + + + + + ); +} + describe('QueryBarInput', () => { beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + }) ); expect(component).toMatchSnapshot(); }); it('Should pass the query language to the language switcher', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: luceneQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + }) ); expect(component).toMatchSnapshot(); }); it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + }) ); expect(component).toMatchSnapshot(); @@ -144,43 +149,32 @@ describe('QueryBarInput', () => { it('Should create a unique PersistedLog based on the appName and query language', () => { mockPersistedLogFactory.mockClear(); - mountWithIntl( - + mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + appName: 'discover', + }) ); - expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); }); it("On language selection, should store the user's preference in localstorage and reset the query", () => { const mockStorage = createMockStorage(); const mockCallback = jest.fn(); - - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext( + { + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + appName: 'discover', + }, + mockStorage + ) ); component @@ -194,23 +188,16 @@ describe('QueryBarInput', () => { it('Should call onSubmit when the user hits enter inside the query bar', () => { const mockCallback = jest.fn(); - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + }) ); - const instance = component.instance() as QueryBarInputUI; + const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -220,23 +207,17 @@ describe('QueryBarInput', () => { }); it('Should use PersistedLog for recent search suggestions', async () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + persistedLog: mockPersistedLog, + }) ); - const instance = component.instance() as QueryBarInputUI; + const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -250,22 +231,15 @@ describe('QueryBarInput', () => { it('Should accept index pattern strings and fetch the full object', () => { mockFetchIndexPatterns.mockClear(); - - mountWithIntl( - + mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: ['logstash-*'], + disableAutoFocus: true, + }) ); + expect(mockFetchIndexPatterns).toHaveBeenCalledWith( startMock.savedObjects.client, ['logstash-*'], diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx index 7a972a6068f6f6..3cdd8d4b9c40c9 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -19,23 +19,22 @@ import { Component } from 'react'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiFieldText, EuiOutsideClickDetector, PopoverAnchorPosition } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { debounce, compact, isEqual, omit } from 'lodash'; +import { debounce, compact, isEqual } from 'lodash'; import { PersistedLog } from 'ui/persisted_log'; -import { Storage } from 'ui/storage'; -import { npStart } from 'ui/new_platform'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; + import { AutocompleteSuggestion, AutocompleteSuggestionType, } from '../../../../../../../plugins/data/public'; +import { + withKibana, + KibanaReactContextValue, +} from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern, StaticIndexPattern } from '../../../index_patterns'; import { Query } from '../index'; import { fromUser, matchPairs, toUser } from '../lib'; @@ -43,21 +42,13 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { SuggestionsComponent } from './typeahead/suggestions_component'; import { getQueryLog } from '../lib/get_query_log'; import { fetchIndexPatterns } from '../lib/fetch_index_patterns'; - -// todo: related to https://github.com/elastic/kibana/pull/45762/files -// Will be refactored after merge of related PR -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); +import { IDataPluginServices } from '../../../types'; interface Props { - uiSettings: UiSettingsClientContract; - indexPatterns: Array; - savedObjectsClient: SavedObjectsClientContract; - http: HttpServiceBase; - store: Storage; + kibana: KibanaReactContextValue; intl: InjectedIntl; + indexPatterns: Array; query: Query; - appName: string; disableAutoFocus?: boolean; screenTitle?: string; prepend?: React.ReactNode; @@ -67,6 +58,7 @@ interface Props { languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onChange?: (query: Query) => void; onSubmit?: (query: Query) => void; + dataTestSubj?: string; } interface State { @@ -107,6 +99,7 @@ export class QueryBarInputUI extends Component { public inputRef: HTMLInputElement | null = null; private persistedLog: PersistedLog | undefined; + private services = this.props.kibana.services; private componentIsUnmounting = false; private getQueryString = () => { @@ -122,9 +115,9 @@ export class QueryBarInputUI extends Component { ) as IndexPattern[]; const objectPatternsFromStrings = (await fetchIndexPatterns( - this.props.savedObjectsClient, + this.services.savedObjects!.client, stringPatterns, - this.props.uiSettings + this.services.uiSettings! )) as IndexPattern[]; this.setState({ @@ -137,13 +130,13 @@ export class QueryBarInputUI extends Component { return; } - const uiSettings = this.props.uiSettings; + const uiSettings = this.services.uiSettings; const language = this.props.query.language; const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const autocompleteProvider = getAutocompleteProvider(language); + const autocompleteProvider = this.services.autocomplete.getProvider(language); if ( !autocompleteProvider || !Array.isArray(this.state.indexPatterns) || @@ -369,11 +362,11 @@ export class QueryBarInputUI extends Component { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the // UI component's change handler. - this.props.http.post('/api/kibana/kql_opt_in_telemetry', { + this.services.http.post('/api/kibana/kql_opt_in_telemetry', { body: JSON.stringify({ opt_in: language === 'kuery' }), }); - this.props.store.set('kibana.userQueryLanguage', language); + this.services.store.set('kibana.userQueryLanguage', language); const newQuery = { query: '', language }; this.onChange(newQuery); @@ -406,7 +399,7 @@ export class QueryBarInputUI extends Component { this.persistedLog = this.props.persistedLog ? this.props.persistedLog - : getQueryLog(this.props.uiSettings, this.props.appName, this.props.query.language); + : getQueryLog(this.services.uiSettings, this.services.appName, this.props.query.language); this.fetchIndexPatterns().then(this.updateSuggestions); } @@ -419,7 +412,7 @@ export class QueryBarInputUI extends Component { this.persistedLog = this.props.persistedLog ? this.props.persistedLog - : getQueryLog(this.props.uiSettings, this.props.appName, this.props.query.language); + : getQueryLog(this.services.uiSettings, this.services.appName, this.props.query.language); if (!isEqual(prevProps.indexPatterns, this.props.indexPatterns)) { this.fetchIndexPatterns().then(this.updateSuggestions); @@ -446,41 +439,30 @@ export class QueryBarInputUI extends Component { } public render() { - const rest = omit(this.props, [ - 'indexPatterns', - 'intl', - 'query', - 'appName', - 'disableAutoFocus', - 'screenTitle', - 'prepend', - 'store', - 'persistedLog', - 'bubbleSubmitEvent', - 'languageSwitcherPopoverAnchorPosition', - 'onChange', - 'onSubmit', - 'uiSettings', - 'savedObjectsClient', - ]); + const isSuggestionsVisible = this.state.isSuggestionsVisible && { + 'aria-controls': 'kbnTypeahead__items', + 'aria-owns': 'kbnTypeahead__items', + }; + const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; return (
{ }} autoComplete="off" spellCheck={false} - aria-label={ - this.props.screenTitle - ? this.props.intl.formatMessage( - { - id: 'data.query.queryBar.searchInputAriaLabel', - defaultMessage: - 'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}', - }, - { - previouslyTranslatedPageTitle: this.props.screenTitle, - pageType: this.props.appName, - } - ) - : undefined - } + aria-label={i18n.translate('data.query.queryBar.searchInputAriaLabel', { + defaultMessage: 'Start typing to search and filter the {pageType} page', + values: { pageType: this.services.appName }, + })} type="text" - data-test-subj="queryInput" aria-autocomplete="list" - aria-controls="kbnTypeahead__items" + aria-controls={this.state.isSuggestionsVisible ? 'kbnTypeahead__items' : undefined} aria-activedescendant={ - this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' + this.state.isSuggestionsVisible && typeof this.state.index === 'number' + ? `suggestion-${this.state.index}` + : undefined } role="textbox" prepend={this.props.prepend} @@ -529,7 +501,7 @@ export class QueryBarInputUI extends Component { onSelectLanguage={this.onSelectLanguage} /> } - {...rest} + data-test-subj={this.props.dataTestSubj || 'queryInput'} />
@@ -548,4 +520,4 @@ export class QueryBarInputUI extends Component { } } -export const QueryBarInput = injectI18n(QueryBarInputUI); +export const QueryBarInput = injectI18n(withKibana(QueryBarInputUI)); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx index 0926af7b30ef78..337bb9f4861c3e 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx @@ -21,7 +21,6 @@ import { mockPersistedLogFactory } from './query_bar_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; -import './query_bar_top_row.test.mocks'; import { QueryBarTopRow } from './query_bar_top_row'; import { IndexPattern } from '../../../index'; @@ -97,42 +96,49 @@ const mockIndexPattern = { ], } as IndexPattern; -describe('QueryBarTopRowTopRow', () => { - const QUERY_INPUT_SELECTOR = 'InjectIntl(QueryBarInputUI)'; - const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; +function wrapQueryBarTopRowInContext(testProps: any) { + const defaultOptions = { + screenTitle: 'Another Screen', + onSubmit: noop, + onChange: noop, + intl: null as any, + }; + const services = { + appName: 'discover', uiSettings: startMock.uiSettings, savedObjects: startMock.savedObjects, notifications: startMock.notifications, http: startMock.http, - }; - const defaultOptions = { - appName: 'discover', - screenTitle: 'Another Screen', - onSubmit: noop, - onChange: noop, store: createMockStorage(), - intl: null as any, }; + return ( + + + + + + ); +} + +describe('QueryBarTopRowTopRow', () => { + const QUERY_INPUT_SELECTOR = 'QueryBarInputUI'; + const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; + beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + screenTitle: 'Another Screen', + isDirty: false, + indexPatterns: [mockIndexPattern], + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); @@ -141,19 +147,14 @@ describe('QueryBarTopRowTopRow', () => { it('Should create a unique PersistedLog based on the appName and query language', () => { mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + screenTitle: 'Another Screen', + indexPatterns: [mockIndexPattern], + timeHistory: timefilterSetupMock.history, + disableAutoFocus: true, + isDirty: false, + }) ); expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); @@ -161,15 +162,10 @@ describe('QueryBarTopRowTopRow', () => { it('Should render only timepicker when no options provided', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -178,16 +174,11 @@ describe('QueryBarTopRowTopRow', () => { it('Should not show timepicker when asked', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + isDirty: false, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -196,19 +187,14 @@ describe('QueryBarTopRowTopRow', () => { it('Should render timepicker with options', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: true, + dateRangeFrom: 'now-7d', + dateRangeTo: 'now', + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -217,19 +203,16 @@ describe('QueryBarTopRowTopRow', () => { it('Should render only query input bar', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + indexPatterns: [mockIndexPattern], + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: false, + dateRangeFrom: 'now-7d', + dateRangeTo: 'now', + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); @@ -238,20 +221,15 @@ describe('QueryBarTopRowTopRow', () => { it('Should NOT render query input bar if disabled', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + isDirty: false, + screenTitle: 'Another Screen', + indexPatterns: [mockIndexPattern], + showQueryInput: false, + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -260,17 +238,12 @@ describe('QueryBarTopRowTopRow', () => { it('Should NOT render query input bar if missing options', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index c25b596973174d..6895c9ecd018cb 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -21,7 +21,6 @@ import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query'; import classNames from 'classnames'; import React, { useState, useEffect } from 'react'; -import { Storage } from 'ui/storage'; import { documentationLinks } from 'ui/documentation_links'; import { PersistedLog } from 'ui/persisted_log'; @@ -30,6 +29,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSuperDatePicker } fro import { EuiSuperUpdateButton } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; +import { TimeRange } from 'src/plugins/data/public'; import { useKibana } from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern } from '../../../index_patterns'; @@ -37,21 +37,15 @@ import { QueryBarInput } from './query_bar_input'; import { getQueryLog } from '../lib/get_query_log'; import { Query } from '../index'; import { TimeHistoryContract } from '../../../timefilter'; - -interface DateRange { - from: string; - to: string; -} +import { IDataPluginServices } from '../../../types'; interface Props { query?: Query; - onSubmit: (payload: { dateRange: DateRange; query?: Query }) => void; - onChange: (payload: { dateRange: DateRange; query?: Query }) => void; + onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; disableAutoFocus?: boolean; - appName: string; screenTitle?: string; indexPatterns?: Array; - store?: Storage; intl: InjectedIntl; prepend?: React.ReactNode; showQueryInput?: boolean; @@ -70,15 +64,15 @@ interface Props { function QueryBarTopRowUI(props: Props) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); - const kibana = useKibana(); - const { uiSettings, http, notifications, savedObjects } = kibana.services; + const kibana = useKibana(); + const { uiSettings, notifications, store, appName } = kibana.services; const queryLanguage = props.query && props.query.language; let persistedLog: PersistedLog | undefined; useEffect(() => { if (!props.query) return; - persistedLog = getQueryLog(uiSettings!, props.appName, props.query.language); + persistedLog = getQueryLog(uiSettings!, appName, props.query.language); }, [queryLanguage]); function onClickSubmitButton(event: React.MouseEvent) { @@ -131,7 +125,7 @@ function QueryBarTopRowUI(props: Props) { } } - function onSubmit({ query, dateRange }: { query?: Query; dateRange: DateRange }) { + function onSubmit({ query, dateRange }: { query?: Query; dateRange: TimeRange }) { handleLuceneSyntaxWarning(); if (props.timeHistory) { @@ -153,19 +147,14 @@ function QueryBarTopRowUI(props: Props) { return ( ); @@ -176,7 +165,7 @@ function QueryBarTopRowUI(props: Props) { } function shouldRenderQueryInput(): boolean { - return Boolean(props.showQueryInput && props.indexPatterns && props.query && props.store); + return Boolean(props.showQueryInput && props.indexPatterns && props.query && store); } function renderUpdateButton() { @@ -251,7 +240,7 @@ function QueryBarTopRowUI(props: Props) { function handleLuceneSyntaxWarning() { if (!props.query) return; - const { intl, store } = props; + const { intl } = props; const { query, language } = props.query; if ( language === 'kuery' && @@ -300,8 +289,8 @@ function QueryBarTopRowUI(props: Props) { } function onLuceneSyntaxWarningOptOut(toast: Toast) { - if (!props.store) return; - props.store.set('kibana.luceneSyntaxWarningOptOut', true); + if (!store) return; + store.set('kibana.luceneSyntaxWarningOptOut', true); notifications!.toasts.remove(toast); } diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx new file mode 100644 index 00000000000000..add49e47971d34 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Filter } from '@kbn/es-query'; +import { CoreStart } from 'src/core/public'; +import { Storage } from 'ui/storage'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TimefilterSetup } from '../../../timefilter'; +import { FilterManager, SearchBar } from '../../../'; +import { SearchBarOwnProps } from '.'; + +interface StatefulSearchBarDeps { + core: CoreStart; + store: Storage; + timefilter: TimefilterSetup; + filterManager: FilterManager; + autocomplete: AutocompletePublicPluginStart; +} + +export type StatetfulSearchBarProps = SearchBarOwnProps & { + appName: string; +}; + +const defaultFiltersUpdated = (filterManager: FilterManager) => { + return (filters: Filter[]) => { + filterManager.setFilters(filters); + }; +}; + +const defaultOnRefreshChange = (timefilter: TimefilterSetup) => { + return (options: { isPaused: boolean; refreshInterval: number }) => { + timefilter.timefilter.setRefreshInterval({ + value: options.refreshInterval, + pause: options.isPaused, + }); + }; +}; + +export function createSearchBar({ + core, + store, + timefilter, + filterManager, + autocomplete, +}: StatefulSearchBarDeps) { + // App name should come from the core application service. + // Until it's available, we'll ask the user to provide it for the pre-wired component. + return (props: StatetfulSearchBarProps) => { + const timeRange = timefilter.timefilter.getTime(); + const refreshInterval = timefilter.timefilter.getRefreshInterval(); + + return ( + + + + ); + }; +} diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx index 24ffa939a746e9..accaac163acfcf 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx @@ -18,3 +18,4 @@ */ export * from './search_bar'; +export * from './create_search_bar'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 7d48977fab8a53..73e81a38572c39 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -18,14 +18,17 @@ */ import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { SearchBar } from './search_bar'; import { IndexPattern } from '../../../index_patterns'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; + import { coreMock } from '../../../../../../../../src/core/public/mocks'; const startMock = coreMock.createStart(); import { timefilterServiceMock } from '../../../timefilter/timefilter_service.mock'; +import { mount } from 'enzyme'; const timefilterSetupMock = timefilterServiceMock.createSetupContract(); jest.mock('../../../../../data/public', () => { @@ -41,13 +44,6 @@ jest.mock('../../../query/query_bar', () => { }; }); -jest.mock('ui/notify', () => ({ - toastNotifications: { - addSuccess: () => {}, - addDanger: () => {}, - }, -})); - const noop = jest.fn(); const createMockWebStorage = () => ({ @@ -87,26 +83,44 @@ const kqlQuery = { language: 'kuery', }; -describe('SearchBar', () => { - const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.filterBar'; - const QUERY_BAR = '.queryBar'; - - const options = { +function wrapSearchBarInContext(testProps: any) { + const defaultOptions = { appName: 'test', - savedObjects: startMock.savedObjects, - notifications: startMock.notifications, timeHistory: timefilterSetupMock.history, intl: null as any, }; + const services = { + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + store: createMockStorage(), + }; + + return ( + + + + + + ); +} + +describe('SearchBar', () => { + const SEARCH_BAR_ROOT = '.globalQueryBar'; + const FILTER_BAR = '.filterBar'; + const QUERY_BAR = '.queryBar'; + beforeEach(() => { jest.clearAllMocks(); }); it('Should render query bar when no options provided (in reality - timepicker)', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -115,12 +129,11 @@ describe('SearchBar', () => { }); it('Should render empty when timepicker is off and no options provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showDatePicker: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -129,14 +142,13 @@ describe('SearchBar', () => { }); it('Should render filter bar, when required fields are provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showDatePicker: false, + onFiltersUpdated: noop, + filters: [], + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -145,15 +157,14 @@ describe('SearchBar', () => { }); it('Should NOT render filter bar, if disabled', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showFilterBar: false, + filters: [], + onFiltersUpdated: noop, + showDatePicker: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -162,15 +173,13 @@ describe('SearchBar', () => { }); it('Should render query bar, when required fields are provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -179,16 +188,14 @@ describe('SearchBar', () => { }); it('Should NOT render query bar, if disabled', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + showQueryBar: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -197,17 +204,15 @@ describe('SearchBar', () => { }); it('Should render query bar and filter bar', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + filters: [], + onFiltersUpdated: noop, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index 6c73fa3614cc3c..ed2a6638aba114 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -22,10 +22,9 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -import { Storage } from 'ui/storage'; import { get, isEqual } from 'lodash'; -import { CoreStart } from 'src/core/public'; +import { TimeRange } from 'src/plugins/data/common/types'; import { IndexPattern, Query, FilterBar } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; @@ -34,54 +33,52 @@ import { SavedQueryManagementComponent } from './saved_query_management/saved_qu import { SavedQueryService } from '../lib/saved_query_service'; import { createSavedQueryService } from '../lib/saved_query_service'; import { TimeHistoryContract } from '../../../timefilter'; - -interface DateRange { - from: string; - to: string; -} - -/** - * NgReact lib requires that changes to the props need to be made in the directive config as well - * See [search_bar\directive\index.js] file - */ -export interface SearchBarProps { - appName: string; +import { + withKibana, + KibanaReactContextValue, +} from '../../../../../../../plugins/kibana_react/public'; +import { IDataPluginServices } from '../../../types'; + +interface SearchBarInjectedDeps { + kibana: KibanaReactContextValue; intl: InjectedIntl; - indexPatterns?: IndexPattern[]; - - // Query bar - showQueryBar?: boolean; - showQueryInput?: boolean; - screenTitle?: string; - store?: Storage; - query?: Query; - savedQuery?: SavedQuery; - onQuerySubmit?: (payload: { dateRange: DateRange; query?: Query }) => void; timeHistory: TimeHistoryContract; // Filter bar - showFilterBar?: boolean; - filters?: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; + filters?: Filter[]; // Date picker - showDatePicker?: boolean; dateRangeFrom?: string; dateRangeTo?: string; // Autorefresh + onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; isRefreshPaused?: boolean; refreshInterval?: number; +} + +export interface SearchBarOwnProps { + indexPatterns?: IndexPattern[]; + customSubmitButton?: React.ReactNode; + screenTitle?: string; + + // Togglers + showQueryBar?: boolean; + showQueryInput?: boolean; + showFilterBar?: boolean; + showDatePicker?: boolean; showAutoRefreshOnly?: boolean; showSaveQuery?: boolean; - onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; + + // Query bar - should be in SearchBarInjectedDeps + query?: Query; + savedQuery?: SavedQuery; + onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; onSaved?: (savedQuery: SavedQuery) => void; onSavedQueryUpdated?: (savedQuery: SavedQuery) => void; onClearSavedQuery?: () => void; - customSubmitButton?: React.ReactNode; - - // TODO: deprecate - savedObjects: CoreStart['savedObjects']; - notifications: CoreStart['notifications']; } +export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; + interface State { isFiltersVisible: boolean; showSaveQueryModal: boolean; @@ -102,7 +99,7 @@ class SearchBarUI extends Component { }; private savedQueryService!: SavedQueryService; - + private services = this.props.kibana.services; public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -253,7 +250,7 @@ class SearchBarUI extends Component { response = await this.savedQueryService.saveQuery(savedQueryAttributes); } - this.props.notifications.toasts.addSuccess( + this.services.notifications.toasts.addSuccess( `Your query "${response.attributes.title}" was saved` ); @@ -266,7 +263,7 @@ class SearchBarUI extends Component { this.props.onSaved(response); } } catch (error) { - this.props.notifications.toasts.addDanger( + this.services.notifications.toasts.addDanger( `An error occured while saving your query: ${error.message}` ); throw error; @@ -285,7 +282,7 @@ class SearchBarUI extends Component { }); }; - public onQueryBarChange = (queryAndDateRange: { dateRange: DateRange; query?: Query }) => { + public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: Query }) => { this.setState({ query: queryAndDateRange.query, dateRangeFrom: queryAndDateRange.dateRange.from, @@ -293,7 +290,7 @@ class SearchBarUI extends Component { }); }; - public onQueryBarSubmit = (queryAndDateRange: { dateRange?: DateRange; query?: Query }) => { + public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { this.setState( { query: queryAndDateRange.query, @@ -337,8 +334,8 @@ class SearchBarUI extends Component { this.setFilterBarHeight(); this.ro.observe(this.filterBarRef); } - if (this.props.savedObjects) { - this.savedQueryService = createSavedQueryService(this.props.savedObjects!.client); + if (this.services.savedObjects) { + this.savedQueryService = createSavedQueryService(this.services.savedObjects.client); } } @@ -370,9 +367,7 @@ class SearchBarUI extends Component { query={this.state.query} screenTitle={this.props.screenTitle} onSubmit={this.onQueryBarSubmit} - appName={this.props.appName} indexPatterns={this.props.indexPatterns} - store={this.props.store} prepend={this.props.showFilterBar ? savedQueryManagement : undefined} showDatePicker={this.props.showDatePicker} dateRangeFrom={this.state.dateRangeFrom} @@ -449,4 +444,4 @@ class SearchBarUI extends Component { } } -export const SearchBar = injectI18n(SearchBarUI); +export const SearchBar = injectI18n(withKibana(SearchBarUI)); diff --git a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts index 4289d56b33c605..126754388f13f2 100644 --- a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts +++ b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts @@ -18,7 +18,8 @@ */ import chrome from 'ui/chrome'; -import { CoreStart, Plugin } from '../../../../../../src/core/public'; +import { Storage } from 'ui/storage'; +import { Plugin } from '../../../../../../src/core/public'; import { initLegacyModule } from './legacy_module'; /** @internal */ @@ -26,6 +27,10 @@ export interface LegacyDependenciesPluginSetup { savedObjectsClient: any; } +export interface LegacyDependenciesPluginStart { + storage: Storage; +} + export class LegacyDependenciesPlugin implements Plugin { public setup() { initLegacyModule(); @@ -35,7 +40,9 @@ export class LegacyDependenciesPlugin implements Plugin { } as LegacyDependenciesPluginSetup; } - public start(core: CoreStart) { - // nothing to do here yet + public start() { + return { + storage: new Storage(window.localStorage), + } as LegacyDependenciesPluginStart; } } diff --git a/src/legacy/core_plugins/data/public/timefilter/get_time.ts b/src/legacy/core_plugins/data/public/timefilter/get_time.ts index e54725dd9ba486..18a43d789714d7 100644 --- a/src/legacy/core_plugins/data/public/timefilter/get_time.ts +++ b/src/legacy/core_plugins/data/public/timefilter/get_time.ts @@ -18,8 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { Field, IndexPattern } from 'ui/index_patterns'; import { TimeRange } from 'src/plugins/data/public'; +import { IndexPattern, Field } from '../index_patterns'; interface CalculateBoundsOptions { forceNow?: Date; diff --git a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts index c08ea9043da927..64129ea2af5ffb 100644 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts @@ -25,7 +25,7 @@ import { IndexPattern, TimeHistoryContract } from '../index'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; import { parseQueryString } from './lib/parse_querystring'; import { calculateBounds, getTime } from './get_time'; -import { TimefilterConfig, InputTimeRange } from './types'; +import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; export class Timefilter { // Fired when isTimeRangeSelectorEnabled \ isAutoRefreshSelectorEnabled are toggled @@ -148,19 +148,19 @@ export class Timefilter { return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); }; - public getBounds = () => { + public getBounds(): TimeRangeBounds { return this.calculateBounds(this._time); - }; + } - public calculateBounds = (timeRange: TimeRange) => { + public calculateBounds(timeRange: TimeRange): TimeRangeBounds { return calculateBounds(timeRange, { forceNow: this.getForceNow() }); - }; + } - public getActiveBounds = () => { + public getActiveBounds(): TimeRangeBounds | undefined { if (this.isTimeRangeSelectorEnabled) { return this.getBounds(); } - }; + } /** * Show the time bounds selector part of the time filter diff --git a/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts b/src/legacy/core_plugins/data/public/types.ts similarity index 63% rename from src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts rename to src/legacy/core_plugins/data/public/types.ts index 7354916c3fc359..4b7a5c1402ea72 100644 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/types.ts @@ -17,12 +17,15 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../core/public/mocks'; +import { UiSettingsClientContract, CoreStart } from 'src/core/public'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); +export interface IDataPluginServices extends Partial { + appName: string; + uiSettings: UiSettingsClientContract; + savedObjects: CoreStart['savedObjects']; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + store: Storage; + autocomplete: AutocompletePublicPluginStart; +} diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index eeee5f3f4c6c71..4cbb1c82cc1e40 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -482,7 +482,7 @@ export interface CallCluster { (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - // ingest namepsace + // ingest namespace (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; diff --git a/src/legacy/core_plugins/input_control_vis/public/register_vis.js b/src/legacy/core_plugins/input_control_vis/public/register_vis.js index 76320b331bb066..731cf2dac9dd2e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/register_vis.js +++ b/src/legacy/core_plugins/input_control_vis/public/register_vis.js @@ -24,7 +24,7 @@ import { OptionsTab } from './components/editor/options_tab'; import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; import { Status } from 'ui/vis/update_status'; import { i18n } from '@kbn/i18n'; -import { setup as visualizations } from '../../visualizations/public/legacy'; +import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; function InputControlVisProvider() { diff --git a/src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.js.snap b/src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.js.snap rename to src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.ts.snap diff --git a/src/legacy/core_plugins/interpreter/public/functions/__tests__/font.js b/src/legacy/core_plugins/interpreter/public/functions/__tests__/font.js deleted file mode 100644 index 4a7ebc1522f2ab..00000000000000 --- a/src/legacy/core_plugins/interpreter/public/functions/__tests__/font.js +++ /dev/null @@ -1,192 +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 expect from '@kbn/expect'; -import { openSans } from '../../../common/lib/fonts'; -import { font } from '../font'; -import { functionWrapper } from '../../../test_helpers'; - -describe('font', () => { - const fn = functionWrapper(font); - - describe('default output', () => { - const result = fn(null); - - it('returns a style', () => { - expect(result) - .to.have.property('type', 'style') - .and.to.have.property('spec') - .and.to.have.property('css'); - }); - }); - - describe('args', () => { - describe('size', () => { - it('sets font size', () => { - const result = fn(null, { size: 20 }); - expect(result.spec).to.have.property('fontSize', '20px'); - expect(result.css).to.contain('font-size:20px'); - }); - - it('defaults to 14px', () => { - const result = fn(null); - expect(result.spec).to.have.property('fontSize', '14px'); - expect(result.css).to.contain('font-size:14px'); - }); - }); - - describe('lHeight', () => { - it('sets line height', () => { - const result = fn(null, { lHeight: 30 }); - expect(result.spec).to.have.property('lineHeight', '30px'); - expect(result.css).to.contain('line-height:30px'); - }); - - it('defaults to 1', () => { - const result = fn(null); - expect(result.spec).to.have.property('lineHeight', '1'); - expect(result.css).to.contain('line-height:1'); - }); - }); - - describe('family', () => { - it('sets font family', () => { - const result = fn(null, { family: 'Optima, serif' }); - expect(result.spec).to.have.property('fontFamily', 'Optima, serif'); - expect(result.css).to.contain('font-family:Optima, serif'); - }); - - it(`defaults to "${openSans.value}"`, () => { - const result = fn(null); - expect(result.spec).to.have.property('fontFamily', `"${openSans.value}"`); - expect(result.css).to.contain(`font-family:"${openSans.value}"`); - }); - }); - - describe('color', () => { - it('sets font color', () => { - const result = fn(null, { color: 'blue' }); - expect(result.spec).to.have.property('color', 'blue'); - expect(result.css).to.contain('color:blue'); - }); - }); - - describe('weight', () => { - it('sets font weight', () => { - let result = fn(null, { weight: 'normal' }); - expect(result.spec).to.have.property('fontWeight', 'normal'); - expect(result.css).to.contain('font-weight:normal'); - - result = fn(null, { weight: 'bold' }); - expect(result.spec).to.have.property('fontWeight', 'bold'); - expect(result.css).to.contain('font-weight:bold'); - - result = fn(null, { weight: 'bolder' }); - expect(result.spec).to.have.property('fontWeight', 'bolder'); - expect(result.css).to.contain('font-weight:bolder'); - - result = fn(null, { weight: 'lighter' }); - expect(result.spec).to.have.property('fontWeight', 'lighter'); - expect(result.css).to.contain('font-weight:lighter'); - - result = fn(null, { weight: '400' }); - expect(result.spec).to.have.property('fontWeight', '400'); - expect(result.css).to.contain('font-weight:400'); - }); - - it('defaults to \'normal\'', () => { - const result = fn(null); - expect(result.spec).to.have.property('fontWeight', 'normal'); - expect(result.css).to.contain('font-weight:normal'); - }); - - it('throws when provided an invalid weight', () => { - expect(() => fn(null, { weight: 'foo' })).to.throwException(); - }); - }); - - describe('underline', () => { - it('sets text underline', () => { - let result = fn(null, { underline: true }); - expect(result.spec).to.have.property('textDecoration', 'underline'); - expect(result.css).to.contain('text-decoration:underline'); - - result = fn(null, { underline: false }); - expect(result.spec).to.have.property('textDecoration', 'none'); - expect(result.css).to.contain('text-decoration:none'); - }); - - it('defaults to false', () => { - const result = fn(null); - expect(result.spec).to.have.property('textDecoration', 'none'); - expect(result.css).to.contain('text-decoration:none'); - }); - }); - - describe('italic', () => { - it('sets italic', () => { - let result = fn(null, { italic: true }); - expect(result.spec).to.have.property('fontStyle', 'italic'); - expect(result.css).to.contain('font-style:italic'); - - result = fn(null, { italic: false }); - expect(result.spec).to.have.property('fontStyle', 'normal'); - expect(result.css).to.contain('font-style:normal'); - }); - - it('defaults to false', () => { - const result = fn(null); - expect(result.spec).to.have.property('fontStyle', 'normal'); - expect(result.css).to.contain('font-style:normal'); - }); - }); - - describe('align', () => { - it('sets text alignment', () => { - let result = fn(null, { align: 'left' }); - expect(result.spec).to.have.property('textAlign', 'left'); - expect(result.css).to.contain('text-align:left'); - - result = fn(null, { align: 'center' }); - expect(result.spec).to.have.property('textAlign', 'center'); - expect(result.css).to.contain('text-align:center'); - - result = fn(null, { align: 'right' }); - expect(result.spec).to.have.property('textAlign', 'right'); - expect(result.css).to.contain('text-align:right'); - - result = fn(null, { align: 'justify' }); - expect(result.spec).to.have.property('textAlign', 'justify'); - expect(result.css).to.contain('text-align:justify'); - }); - - it(`defaults to 'left'`, () => { - const result = fn(null); - expect(result.spec).to.have.property('textAlign', 'left'); - expect(result.css).to.contain('text-align:left'); - }); - - it('throws when provided an invalid alignment', () => { - expect(fn) - .withArgs(null, { align: 'foo' }) - .to.throwException(); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/clog.js b/src/legacy/core_plugins/interpreter/public/functions/clog.ts similarity index 91% rename from src/legacy/core_plugins/interpreter/public/functions/clog.js rename to src/legacy/core_plugins/interpreter/public/functions/clog.ts index 634d166f5f0bb5..4867726a42d72c 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/clog.js +++ b/src/legacy/core_plugins/interpreter/public/functions/clog.ts @@ -20,8 +20,8 @@ export const clog = () => ({ name: 'clog', help: 'Outputs the context to the console', - fn: context => { - console.log(context); //eslint-disable-line no-console + fn: (context: any) => { + console.log(context); // eslint-disable-line no-console return context; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/font.test.ts b/src/legacy/core_plugins/interpreter/public/functions/font.test.ts new file mode 100644 index 00000000000000..dba540178777bc --- /dev/null +++ b/src/legacy/core_plugins/interpreter/public/functions/font.test.ts @@ -0,0 +1,202 @@ +/* + * 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 { openSans } from '../../common/lib/fonts'; +import { font } from './font'; +import { functionWrapper } from '../../test_helpers'; + +describe('font', () => { + const fn: any = functionWrapper(font); + + describe('default output', () => { + const result = fn(null); + + it('returns a style', () => { + expect(result).toMatchObject({ + type: 'style', + spec: expect.any(Object), + css: expect.any(String), + }); + }); + }); + + describe('args', () => { + describe('size', () => { + it('sets font size', () => { + const result = fn(null, { size: 20 }); + expect(result).toMatchObject({ + spec: { + fontSize: '20px', + }, + }); + expect(result.css).toContain('font-size:20px'); + }); + + it('defaults to 14px', () => { + const result = fn(null); + expect(result).toMatchObject({ + spec: { + fontSize: '14px', + }, + }); + expect(result.css).toContain('font-size:14px'); + }); + }); + + describe('lHeight', () => { + it('sets line height', () => { + const result = fn(null, { lHeight: 30 }); + expect(result).toMatchObject({ + spec: { + lineHeight: '30px', + }, + }); + expect(result.css).toContain('line-height:30px'); + }); + + it('defaults to 1', () => { + const result = fn(null); + expect(result.spec.lineHeight).toBe('1'); + expect(result.css).toContain('line-height:1'); + }); + }); + + describe('family', () => { + it('sets font family', () => { + const result = fn(null, { family: 'Optima, serif' }); + expect(result.spec.fontFamily).toBe('Optima, serif'); + expect(result.css).toContain('font-family:Optima, serif'); + }); + + it(`defaults to "${openSans.value}"`, () => { + const result = fn(null); + expect(result.spec.fontFamily).toBe(`"${openSans.value}"`); + expect(result.css).toContain(`font-family:"${openSans.value}"`); + }); + }); + + describe('color', () => { + it('sets font color', () => { + const result = fn(null, { color: 'blue' }); + expect(result.spec.color).toBe('blue'); + expect(result.css).toContain('color:blue'); + }); + }); + + describe('weight', () => { + it('sets font weight', () => { + let result = fn(null, { weight: 'normal' }); + expect(result.spec.fontWeight).toBe('normal'); + expect(result.css).toContain('font-weight:normal'); + + result = fn(null, { weight: 'bold' }); + expect(result.spec.fontWeight).toBe('bold'); + expect(result.css).toContain('font-weight:bold'); + + result = fn(null, { weight: 'bolder' }); + expect(result.spec.fontWeight).toBe('bolder'); + expect(result.css).toContain('font-weight:bolder'); + + result = fn(null, { weight: 'lighter' }); + expect(result.spec.fontWeight).toBe('lighter'); + expect(result.css).toContain('font-weight:lighter'); + + result = fn(null, { weight: '400' }); + expect(result.spec.fontWeight).toBe('400'); + expect(result.css).toContain('font-weight:400'); + }); + + it("defaults to 'normal'", () => { + const result = fn(null); + expect(result.spec.fontWeight).toBe('normal'); + expect(result.css).toContain('font-weight:normal'); + }); + + it('throws when provided an invalid weight', () => { + expect(() => fn(null, { weight: 'foo' })).toThrow(); + }); + }); + + describe('underline', () => { + it('sets text underline', () => { + let result = fn(null, { underline: true }); + expect(result.spec.textDecoration).toBe('underline'); + expect(result.css).toContain('text-decoration:underline'); + + result = fn(null, { underline: false }); + expect(result.spec.textDecoration).toBe('none'); + expect(result.css).toContain('text-decoration:none'); + }); + + it('defaults to false', () => { + const result = fn(null); + expect(result.spec.textDecoration).toBe('none'); + expect(result.css).toContain('text-decoration:none'); + }); + }); + + describe('italic', () => { + it('sets italic', () => { + let result = fn(null, { italic: true }); + expect(result.spec.fontStyle).toBe('italic'); + expect(result.css).toContain('font-style:italic'); + + result = fn(null, { italic: false }); + expect(result.spec.fontStyle).toBe('normal'); + expect(result.css).toContain('font-style:normal'); + }); + + it('defaults to false', () => { + const result = fn(null); + expect(result.spec.fontStyle).toBe('normal'); + expect(result.css).toContain('font-style:normal'); + }); + }); + + describe('align', () => { + it('sets text alignment', () => { + let result = fn(null, { align: 'left' }); + expect(result.spec.textAlign).toBe('left'); + expect(result.css).toContain('text-align:left'); + + result = fn(null, { align: 'center' }); + expect(result.spec.textAlign).toBe('center'); + expect(result.css).toContain('text-align:center'); + + result = fn(null, { align: 'right' }); + expect(result.spec.textAlign).toBe('right'); + expect(result.css).toContain('text-align:right'); + + result = fn(null, { align: 'justify' }); + expect(result.spec.textAlign).toBe('justify'); + expect(result.css).toContain('text-align:justify'); + }); + + it(`defaults to 'left'`, () => { + const result = fn(null); + expect(result.spec.textAlign).toBe('left'); + expect(result.css).toContain('text-align:left'); + }); + + it('throws when provided an invalid alignment', () => { + expect(() => fn(null, { align: 'foo' })).toThrow(); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/index.js b/src/legacy/core_plugins/interpreter/public/functions/index.ts similarity index 92% rename from src/legacy/core_plugins/interpreter/public/functions/index.js rename to src/legacy/core_plugins/interpreter/public/functions/index.ts index 38c3920f91bd29..d86f033acb3d13 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/index.js +++ b/src/legacy/core_plugins/interpreter/public/functions/index.ts @@ -27,5 +27,12 @@ import { visualization } from './visualization'; import { visDimension } from './vis_dimension'; export const functions = [ - clog, esaggs, font, kibana, kibanaContext, range, visualization, visDimension, + clog, + esaggs, + font, + kibana, + kibanaContext, + range, + visualization, + visDimension, ]; diff --git a/src/legacy/core_plugins/interpreter/public/functions/kibana.test.js b/src/legacy/core_plugins/interpreter/public/functions/kibana.test.ts similarity index 97% rename from src/legacy/core_plugins/interpreter/public/functions/kibana.test.js rename to src/legacy/core_plugins/interpreter/public/functions/kibana.test.ts index 4757b9b12b50da..9f80449ac36be4 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/kibana.test.js +++ b/src/legacy/core_plugins/interpreter/public/functions/kibana.test.ts @@ -22,9 +22,9 @@ import { kibana } from './kibana'; describe('interpreter/functions#kibana', () => { const fn = functionWrapper(kibana); - let context; - let initialContext; - let handlers; + let context: any; + let initialContext: any; + let handlers: any; beforeEach(() => { context = { timeRange: { from: '0', to: '1' } }; diff --git a/src/legacy/core_plugins/interpreter/public/functions/kibana.js b/src/legacy/core_plugins/interpreter/public/functions/kibana.ts similarity index 93% rename from src/legacy/core_plugins/interpreter/public/functions/kibana.js rename to src/legacy/core_plugins/interpreter/public/functions/kibana.ts index e0817d8e04b026..37ff337f58b8d4 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/kibana.js +++ b/src/legacy/core_plugins/interpreter/public/functions/kibana.ts @@ -24,10 +24,10 @@ export const kibana = () => ({ type: 'kibana_context', context: {}, help: i18n.translate('interpreter.functions.kibana.help', { - defaultMessage: 'Gets kibana global context' + defaultMessage: 'Gets kibana global context', }), args: {}, - fn(context, args, handlers) { + fn(context: any, args: any, handlers: any) { const initialContext = handlers.getInitialContext ? handlers.getInitialContext() : {}; if (context.query) { @@ -45,7 +45,7 @@ export const kibana = () => ({ type: 'kibana_context', query: initialContext.query, filters: initialContext.filters, - timeRange: timeRange, + timeRange, }; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/kibana_context.js b/src/legacy/core_plugins/interpreter/public/functions/kibana_context.ts similarity index 87% rename from src/legacy/core_plugins/interpreter/public/functions/kibana_context.js rename to src/legacy/core_plugins/interpreter/public/functions/kibana_context.ts index 7b7294a87831d6..2f2241a3670945 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/kibana_context.js +++ b/src/legacy/core_plugins/interpreter/public/functions/kibana_context.ts @@ -24,13 +24,10 @@ export const kibanaContext = () => ({ name: 'kibana_context', type: 'kibana_context', context: { - types: [ - 'kibana_context', - 'null', - ], + types: ['kibana_context', 'null'], }, help: i18n.translate('interpreter.functions.kibana_context.help', { - defaultMessage: 'Updates kibana global context' + defaultMessage: 'Updates kibana global context', }), args: { q: { @@ -49,11 +46,11 @@ export const kibanaContext = () => ({ savedSearchId: { types: ['string', 'null'], default: null, - } + }, }, - async fn(context, args) { + async fn(context: any, args: any) { const $injector = await chrome.dangerouslyGetActiveInjector(); - const savedSearches = $injector.get('savedSearches'); + const savedSearches = $injector.get('savedSearches') as any; const queryArg = args.q ? JSON.parse(args.q) : []; let queries = Array.isArray(queryArg) ? queryArg : [queryArg]; let filters = args.filters ? JSON.parse(args.filters) : []; @@ -71,7 +68,7 @@ export const kibanaContext = () => ({ } if (context.filters) { - filters = filters.concat(context.filters).filter(f => !f.meta.disabled); + filters = filters.concat(context.filters).filter((f: any) => !f.meta.disabled); } const timeRange = args.timeRange ? JSON.parse(args.timeRange) : context.timeRange; @@ -79,8 +76,8 @@ export const kibanaContext = () => ({ return { type: 'kibana_context', query: queries, - filters: filters, - timeRange: timeRange, + filters, + timeRange, }; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.js b/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.ts similarity index 74% rename from src/legacy/core_plugins/interpreter/public/functions/vis_dimension.js rename to src/legacy/core_plugins/interpreter/public/functions/vis_dimension.ts index e1a6c41198bad4..19503dbe03ae98 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.js +++ b/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.ts @@ -22,48 +22,48 @@ import { i18n } from '@kbn/i18n'; export const visDimension = () => ({ name: 'visdimension', help: i18n.translate('interpreter.function.visDimension.help', { - defaultMessage: 'Generates visConfig dimension object' + defaultMessage: 'Generates visConfig dimension object', }), type: 'vis_dimension', context: { - types: [ - 'kibana_datatable' - ], + types: ['kibana_datatable'], }, args: { accessor: { types: ['string', 'number'], aliases: ['_'], help: i18n.translate('interpreter.function.visDimension.accessor.help', { - defaultMessage: 'Column in your dataset to use (either column index or column name)' + defaultMessage: 'Column in your dataset to use (either column index or column name)', }), }, format: { types: ['string'], - default: 'string' + default: 'string', }, formatParams: { types: ['string'], default: '"{}"', - } + }, }, - fn: (context, args) => { - const accessor = Number.isInteger(args.accessor) ? - args.accessor : - context.columns.find(c => c.id === args.accessor); + fn: (context: any, args: any) => { + const accessor = Number.isInteger(args.accessor) + ? args.accessor + : context.columns.find((c: any) => c.id === args.accessor); if (accessor === undefined) { - throw new Error(i18n.translate('interpreter.function.visDimension.error.accessor', { - defaultMessage: 'Column name provided is invalid' - })); + throw new Error( + i18n.translate('interpreter.function.visDimension.error.accessor', { + defaultMessage: 'Column name provided is invalid', + }) + ); } return { type: 'vis_dimension', - accessor: accessor, + accessor, format: { id: args.format, params: JSON.parse(args.formatParams), - } + }, }; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/visualization.js b/src/legacy/core_plugins/interpreter/public/functions/visualization.ts similarity index 90% rename from src/legacy/core_plugins/interpreter/public/functions/visualization.js rename to src/legacy/core_plugins/interpreter/public/functions/visualization.ts index 7dceeaf6843540..d46044b544c419 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/visualization.js +++ b/src/legacy/core_plugins/interpreter/public/functions/visualization.ts @@ -20,17 +20,16 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; -import { setup as data } from '../../../data/public/legacy'; -import { start as visualizations } from '../../../visualizations/public/legacy'; - import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { PersistedState } from 'ui/persisted_state'; +import { setup as data } from '../../../data/public/legacy'; +import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; export const visualization = () => ({ name: 'visualization', type: 'render', help: i18n.translate('interpreter.functions.visualization.help', { - defaultMessage: 'A simple visualization' + defaultMessage: 'A simple visualization', }), args: { index: { @@ -60,17 +59,17 @@ export const visualization = () => ({ uiState: { types: ['string'], default: '"{}"', - } + }, }, - async fn(context, args, handlers) { + async fn(context: any, args: any, handlers: any) { const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); + const Private = $injector.get('Private') as any; const { indexPatterns } = data.indexPatterns; const queryFilter = Private(FilterBarQueryFilterProvider); const visConfigParams = JSON.parse(args.visConfig); const schemas = JSON.parse(args.schemas); - const visType = visualizations.types.get(args.type || 'histogram'); + const visType = visualizations.types.get(args.type || 'histogram') as any; const indexPattern = args.index ? await indexPatterns.get(args.index) : null; const uiStateParams = JSON.parse(args.uiState); @@ -85,7 +84,7 @@ export const visualization = () => ({ timeRange: get(context, 'timeRange', null), query: get(context, 'query', null), filters: get(context, 'filters', null), - uiState: uiState, + uiState, inspectorAdapters: handlers.inspectorAdapters, queryFilter, forceFetch: true, @@ -95,14 +94,14 @@ export const visualization = () => ({ if (typeof visType.responseHandler === 'function') { if (context.columns) { // assign schemas to aggConfigs - context.columns.forEach(column => { + context.columns.forEach((column: any) => { if (column.aggConfig) { column.aggConfig.aggConfigs.schemas = visType.schemas.all; } }); Object.keys(schemas).forEach(key => { - schemas[key].forEach(i => { + schemas[key].forEach((i: any) => { if (context.columns[i] && context.columns[i].aggConfig) { context.columns[i].aggConfig.schema = key; } @@ -119,8 +118,8 @@ export const visualization = () => ({ value: { visData: context, visType: args.type, - visConfig: visConfigParams - } + visConfig: visConfigParams, + }, }; - } + }, }); diff --git a/src/legacy/core_plugins/interpreter/public/interpreter.test.js b/src/legacy/core_plugins/interpreter/public/interpreter.test.ts similarity index 96% rename from src/legacy/core_plugins/interpreter/public/interpreter.test.js rename to src/legacy/core_plugins/interpreter/public/interpreter.test.ts index bd7dc0a47c1240..1de1e8c0cc0598 100644 --- a/src/legacy/core_plugins/interpreter/public/interpreter.test.js +++ b/src/legacy/core_plugins/interpreter/public/interpreter.test.ts @@ -24,9 +24,9 @@ jest.mock('ui/new_platform', () => ({ injectedMetadata: { getKibanaVersion: () => '8.0.0', getBasePath: () => '/lol', - } - } - } + }, + }, + }, })); jest.mock('uiExports/interpreter'); @@ -38,7 +38,7 @@ jest.mock('@kbn/interpreter/common', () => ({ const mockInterpreter = { interpreter: { interpretAst: jest.fn(), - } + }, }; jest.mock('./lib/interpreter', () => ({ initializeInterpreter: jest.fn().mockReturnValue(Promise.resolve(mockInterpreter)), @@ -57,9 +57,9 @@ jest.mock('./functions', () => ({ functions: [{}, {}, {}] })); jest.mock('./renderers/visualization', () => ({ visualization: {} })); describe('interpreter/interpreter', () => { - let getInterpreter; - let interpretAst; - let initializeInterpreter; + let getInterpreter: any; + let interpretAst: any; + let initializeInterpreter: any; beforeEach(() => { jest.clearAllMocks(); @@ -117,5 +117,4 @@ describe('interpreter/interpreter', () => { expect(mockInterpreter.interpreter.interpretAst).toHaveBeenCalledTimes(2); }); }); - }); diff --git a/src/legacy/core_plugins/interpreter/public/interpreter.js b/src/legacy/core_plugins/interpreter/public/interpreter.ts similarity index 87% rename from src/legacy/core_plugins/interpreter/public/interpreter.js rename to src/legacy/core_plugins/interpreter/public/interpreter.ts index 84e05bb10d9fa6..8ba82d5daf759a 100644 --- a/src/legacy/core_plugins/interpreter/public/interpreter.js +++ b/src/legacy/core_plugins/interpreter/public/interpreter.ts @@ -18,6 +18,7 @@ */ import 'uiExports/interpreter'; +// @ts-ignore import { register, registryFactory } from '@kbn/interpreter/common'; import { initializeInterpreter } from './lib/interpreter'; import { registries } from './registries'; @@ -27,7 +28,10 @@ import { typeSpecs } from '../../../../plugins/expressions/common'; // Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins // can register without a transpile step. -global.kbnInterpreter = Object.assign(global.kbnInterpreter || {}, registryFactory(registries)); +(global as any).kbnInterpreter = Object.assign( + (global as any).kbnInterpreter || {}, + registryFactory(registries) +); register(registries, { types: typeSpecs, @@ -35,7 +39,7 @@ register(registries, { renderers: [visualization], }); -let interpreterPromise; +let interpreterPromise: Promise | undefined; export const getInterpreter = async () => { if (!interpreterPromise) { @@ -44,7 +48,7 @@ export const getInterpreter = async () => { return await interpreterPromise; }; -export const interpretAst = async (...params) => { +export const interpretAst = async (...params: any) => { const { interpreter } = await getInterpreter(); return await interpreter.interpretAst(...params); }; diff --git a/src/legacy/core_plugins/interpreter/public/lib/render_function.js b/src/legacy/core_plugins/interpreter/public/lib/render_function.ts similarity index 92% rename from src/legacy/core_plugins/interpreter/public/lib/render_function.js rename to src/legacy/core_plugins/interpreter/public/lib/render_function.ts index 04aa05951be70f..76d1f58b661950 100644 --- a/src/legacy/core_plugins/interpreter/public/lib/render_function.js +++ b/src/legacy/core_plugins/interpreter/public/lib/render_function.ts @@ -17,7 +17,7 @@ * under the License. */ -export function RenderFunction(config) { +export function RenderFunction(this: any, config: any) { // This must match the name of the function that is used to create the `type: render` object this.name = config.name; @@ -36,7 +36,7 @@ export function RenderFunction(config) { // the function called to render the data this.render = config.render || - function render(domNode, data, done) { + function render(domNode: any, data: any, done: any) { done(); }; } diff --git a/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.js b/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.ts similarity index 88% rename from src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.js rename to src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.ts index 60e823baf0fa78..427e7f7454c24e 100644 --- a/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.js +++ b/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.ts @@ -20,9 +20,9 @@ import { Registry } from '@kbn/interpreter/common'; import { RenderFunction } from './render_function'; -class RenderFunctionsRegistry extends Registry { - wrapper(obj) { - return new RenderFunction(obj); +class RenderFunctionsRegistry extends Registry { + wrapper(obj: any) { + return new (RenderFunction as any)(obj); } } diff --git a/src/legacy/core_plugins/interpreter/public/renderers/visualization.js b/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts similarity index 84% rename from src/legacy/core_plugins/interpreter/public/renderers/visualization.js rename to src/legacy/core_plugins/interpreter/public/renderers/visualization.ts index 38fe02436380c7..960e925b132213 100644 --- a/src/legacy/core_plugins/interpreter/public/renderers/visualization.js +++ b/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts @@ -19,17 +19,18 @@ import chrome from 'ui/chrome'; import { visualizationLoader } from 'ui/visualize/loader/visualization_loader'; +// @ts-ignore import { VisProvider } from 'ui/visualize/loader/vis'; export const visualization = () => ({ name: 'visualization', displayName: 'visualization', reuseDomNode: true, - render: async (domNode, config, handlers) => { + render: async (domNode: HTMLElement, config: any, handlers: any) => { const { visData, visConfig, params } = config; const visType = config.visType || visConfig.type; const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); + const Private = $injector.get('Private') as any; const Vis = Private(VisProvider); if (handlers.vis) { @@ -49,8 +50,10 @@ export const visualization = () => ({ handlers.onDestroy(() => visualizationLoader.destroy()); - await visualizationLoader.render(domNode, handlers.vis, visData, handlers.vis.params, uiState, params).then(() => { - if (handlers.done) handlers.done(); - }); + await visualizationLoader + .render(domNode, handlers.vis, visData, handlers.vis.params, uiState, params) + .then(() => { + if (handlers.done) handlers.done(); + }); }, }); diff --git a/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.js b/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts similarity index 95% rename from src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.js rename to src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts index a6e0e13049e1c9..00886630807748 100644 --- a/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.js +++ b/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts @@ -28,13 +28,13 @@ const mockServer = { plugins: { elasticsearch: { getCluster: () => ({ - callWithRequest: (...args) => Promise.resolve(args), + callWithRequest: (...args: any) => Promise.resolve(args), }), }, }, config: () => ({ has: () => false, - get: val => val, + get: (val: any) => val, }), info: { uri: 'serveruri', diff --git a/src/legacy/core_plugins/interpreter/server/lib/create_handlers.js b/src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts similarity index 88% rename from src/legacy/core_plugins/interpreter/server/lib/create_handlers.js rename to src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts index d4ea9b3dc6180f..6e295d0aecaa59 100644 --- a/src/legacy/core_plugins/interpreter/server/lib/create_handlers.js +++ b/src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts @@ -17,7 +17,7 @@ * under the License. */ -export const createHandlers = (request, server) => { +export const createHandlers = (request: any, server: any) => { const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); const config = server.config(); @@ -27,6 +27,6 @@ export const createHandlers = (request, server) => { config.has('server.rewriteBasePath') && config.get('server.rewriteBasePath') ? `${server.info.uri}${config.get('server.basePath')}` : server.info.uri, - elasticsearchClient: async (...args) => callWithRequest(request, ...args), + elasticsearchClient: async (...args: any) => callWithRequest(request, ...args), }; }; diff --git a/src/legacy/core_plugins/interpreter/server/routes/index.js b/src/legacy/core_plugins/interpreter/server/routes/index.ts similarity index 95% rename from src/legacy/core_plugins/interpreter/server/routes/index.js rename to src/legacy/core_plugins/interpreter/server/routes/index.ts index 9140f93a9bde64..50385147dd38e6 100644 --- a/src/legacy/core_plugins/interpreter/server/routes/index.js +++ b/src/legacy/core_plugins/interpreter/server/routes/index.ts @@ -19,6 +19,6 @@ import { registerServerFunctions } from './server_functions'; -export function routes(server) { +export function routes(server: any) { registerServerFunctions(server); } diff --git a/src/legacy/core_plugins/interpreter/server/routes/server_functions.js b/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts similarity index 84% rename from src/legacy/core_plugins/interpreter/server/routes/server_functions.js rename to src/legacy/core_plugins/interpreter/server/routes/server_functions.ts index b64a9af006e412..740b046610d9e9 100644 --- a/src/legacy/core_plugins/interpreter/server/routes/server_functions.js +++ b/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts @@ -18,16 +18,16 @@ */ import Boom from 'boom'; +import Joi from 'joi'; import { serializeProvider, API_ROUTE } from '../../common'; import { createHandlers } from '../lib/create_handlers'; -import Joi from 'joi'; /** * Register the Canvas function endopints. * * @param {*} server - The Kibana server */ -export function registerServerFunctions(server) { +export function registerServerFunctions(server: any) { getServerFunctions(server); runServerFunctions(server); } @@ -37,7 +37,7 @@ export function registerServerFunctions(server) { * * @param {*} server - The Kibana server */ -function runServerFunctions(server) { +function runServerFunctions(server: any) { server.route({ method: 'POST', path: `${API_ROUTE}/fns`, @@ -48,19 +48,20 @@ function runServerFunctions(server) { }, validate: { payload: Joi.object({ - functions: Joi.array().items( - Joi.object() - .keys({ + functions: Joi.array() + .items( + Joi.object().keys({ id: Joi.number().required(), functionName: Joi.string().required(), args: Joi.object().default({}), context: Joi.any().default(null), - }), - ).required(), + }) + ) + .required(), }).required(), }, }, - async handler(req) { + async handler(req: any) { const handlers = await createHandlers(req, server); const { functions } = req.payload; @@ -73,19 +74,19 @@ function runServerFunctions(server) { // Send the initial headers. res.writeHead(200, { 'Content-Type': 'text/plain', - 'Connection': 'keep-alive', + Connection: 'keep-alive', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-cache', }); // Write a length-delimited response - const streamResult = (result) => { + const streamResult = (result: any) => { const payload = JSON.stringify(result) + '\n'; res.write(`${payload.length}:${payload}`); }; // Tries to run an interpreter function, and ensures a consistent error payload on failure. - const tryFunction = async (id, fnCall) => { + const tryFunction = async (id: any, fnCall: any) => { try { const result = await runFunction(server, handlers, fnCall); @@ -96,7 +97,7 @@ function runServerFunctions(server) { return { id, statusCode: 200, result }; } catch (err) { if (Boom.isBoom(err)) { - return batchError(id, err.output.payload, err.statusCode); + return batchError(id, err.output.payload, (err as any).statusCode); } else if (err instanceof Error) { return batchError(id, err.message); } @@ -107,7 +108,9 @@ function runServerFunctions(server) { }; // Process each function individually, and stream the responses back to the client - await Promise.all(functions.map(({ id, ...fnCall }) => tryFunction(id, fnCall).then(streamResult))); + await Promise.all( + functions.map(({ id, ...fnCall }: any) => tryFunction(id, fnCall).then(streamResult)) + ); // All of the responses have been written, so we can close the response. res.end(); @@ -118,7 +121,7 @@ function runServerFunctions(server) { /** * A helper function for bundling up errors. */ -function batchError(id, message, statusCode = 500) { +function batchError(id: any, message: any, statusCode = 500) { return { id, statusCode, @@ -130,7 +133,7 @@ function batchError(id, message, statusCode = 500) { * Register the endpoint that returns the list of server-only functions. * @param {*} server - The Kibana server */ -function getServerFunctions(server) { +function getServerFunctions(server: any) { server.route({ method: 'GET', path: `${API_ROUTE}/fns`, @@ -147,7 +150,7 @@ function getServerFunctions(server) { * @param {*} handlers - The Canvas handlers * @param {*} fnCall - Describes the function being run `{ functionName, args, context }` */ -async function runFunction(server, handlers, fnCall) { +async function runFunction(server: any, handlers: any, fnCall: any) { const registries = server.plugins.interpreter.registries(); const { functionName, args, context } = fnCall; const types = registries.types.toJS(); diff --git a/src/legacy/core_plugins/interpreter/test_helpers.js b/src/legacy/core_plugins/interpreter/test_helpers.ts similarity index 86% rename from src/legacy/core_plugins/interpreter/test_helpers.js rename to src/legacy/core_plugins/interpreter/test_helpers.ts index e743b8a09280e2..741cd83bb47fed 100644 --- a/src/legacy/core_plugins/interpreter/test_helpers.js +++ b/src/legacy/core_plugins/interpreter/test_helpers.ts @@ -21,8 +21,9 @@ import { mapValues } from 'lodash'; // Takes a function spec and passes in default args, // overriding with any provided args. -export const functionWrapper = fnSpec => { +export const functionWrapper = (fnSpec: any) => { const spec = fnSpec(); const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); - return (context, args, handlers) => spec.fn(context, { ...defaultArgs, ...args }, handlers); + return (context: any, args: any, handlers: any) => + spec.fn(context, { ...defaultArgs, ...args }, handlers); }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx index a24a37ca971d58..b96132fa29380f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx @@ -29,12 +29,12 @@ function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInter return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx index 4abfb2e604b1dc..4e3b511782c9e7 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx @@ -38,12 +38,12 @@ function RangesPanel({ return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx index f606080afbdb2f..a76171673d9a82 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx @@ -34,12 +34,12 @@ function StylePanel({ aggs, setGaugeValue, stateParams, vis }: GaugeOptionsInter return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap index 6eef5047634f4f..d88654cfdc0c43 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -7,13 +7,13 @@ exports[`CategoryAxisPanel component should init with the default set of props 1 -

+

-

+
-

+

-

+ -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx index 434202d64d6c30..5a455f4adde31f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx @@ -38,12 +38,12 @@ function SeriesPanel(props: SeriesPanelProps) { return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx index 34a0d2cd981c5f..eb0ab4333af599 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx @@ -109,12 +109,12 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx index 982c7265d5494c..53dde185ec09fe 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx @@ -36,12 +36,12 @@ function PieOptions(props: VisOptionsProps) { <> -

+

-

+
) { -
+

-

+
-

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx index 11034f7f7335e4..8e3f66d12b9bdf 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx @@ -34,12 +34,12 @@ function PointSeriesOptions(props: VisOptionsProps) { <> -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx index 9877b84345a1fe..49e56e377a8d56 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx @@ -42,12 +42,12 @@ function ThresholdPanel({ stateParams, setValue, vis }: VisOptionsProps -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js index b11f21a96f38da..fe2cca3b800641 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js @@ -17,7 +17,7 @@ * under the License. */ -import { setup as visualizations } from '../../visualizations/public/legacy'; +import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; import histogramVisTypeProvider from './histogram'; import lineVisTypeProvider from './line'; diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js b/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts similarity index 82% rename from src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js rename to src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts index 0e1e1ecf058b9e..c232f65143d859 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts @@ -17,11 +17,19 @@ * under the License. */ -import { asPrettyString } from '../../../../../../plugins/data/common/field_formats'; +import { + FieldFormat, + asPrettyString, + KBN_FIELD_TYPES, +} from '../../../../../../plugins/data/common'; -export function createBoolFormat(FieldFormat) { +export function createBoolFormat() { return class BoolFormat extends FieldFormat { - _convert(value) { + static id = 'boolean'; + static title = 'Boolean'; + static fieldType = [KBN_FIELD_TYPES.BOOLEAN, KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]; + + _convert(value: any): string { if (typeof value === 'string') { value = value.trim().toLowerCase(); } @@ -41,9 +49,5 @@ export function createBoolFormat(FieldFormat) { return asPrettyString(value); } } - - static id = 'boolean'; - static title = 'Boolean'; - static fieldType = ['boolean', 'number', 'string']; }; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html index 5ceb28e6b225b8..39db357a69321f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -120,6 +120,7 @@
+

{{screenTitle}}

diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 9b819443808c93..89b8e2ac83ec10 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -13,7 +13,6 @@ exports[`after fetch hideWriteControls 1`] = ` noItemsFragment={
@@ -106,7 +105,6 @@ exports[`after fetch initialFilter 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -199,7 +197,6 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -292,7 +289,6 @@ exports[`after fetch renders table rows 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -385,7 +381,6 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -478,7 +473,6 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

diff --git a/src/legacy/core_plugins/kibana/public/discover/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/_discover.scss index abf24241071c21..12cac1c89275b3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_discover.scss @@ -19,9 +19,15 @@ discover-app { margin-top: 5px; } -// SASSTODO: replace the padding-top value with a variable .dscFieldList--popular { - padding-top: 10px; + padding-top: $euiSizeS; +} + +.dscFieldList--selected, +.dscFieldList--unpopular, +.dscFieldList--popular { + padding-left: $euiSizeS; + padding-right: $euiSizeS; } // SASSTODO: replace the z-index value with a variable @@ -151,9 +157,16 @@ discover-app { } } -// SASSTODO: replace the padding value with a variable +.dscFieldSearch { + padding: $euiSizeS; +} + +.dscFieldFilter { + margin-top: $euiSizeS; +} + .dscFieldDetails { - padding: 10px; + padding: $euiSizeS; background-color: $euiColorLightestShade; color: $euiTextColor; } diff --git a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss index cdc8e04dff5789..baf27bb9f82da1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss @@ -3,18 +3,4 @@ overflow: hidden; } -// SASSTODO: these are Angular Bootstrap classes. Will be replaced by EUI -.dscFieldDetails { - .progress { - background-color: shade($euiColorLightestShade, 5%); - margin-bottom: 0; - border-radius: 0; - } - .progress-bar { - padding-left: 10px; - text-align: right; - line-height: 20px; - max-width: 100%; - } -} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss index 1946cccd319a4d..22f53512be46b1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss @@ -1,3 +1,8 @@ +.dscFieldChooser { + padding-left: $euiSizeS !important; + padding-right: $euiSizeS !important; +} + .dscFieldChooser__toggle { color: $euiColorMediumShade; margin-left: $euiSizeS !important; @@ -15,3 +20,7 @@ .dscProgressBarTooltip__anchor { display: block; } + +.dscToggleFieldFilterButton { + min-height: $euiSizeXL; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx new file mode 100644 index 00000000000000..cf853d798a8abb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx @@ -0,0 +1,58 @@ +/* + * 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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { DiscoverFieldSearch } from './discover_field_search'; + +describe('DiscoverFieldSearch', () => { + function mountComponent() { + const props = { + onChange: jest.fn(), + onShowFilter: jest.fn(), + showFilter: false, + value: 'test', + }; + const comp = mountWithIntl(); + const input = findTestSubject(comp, 'fieldFilterSearchInput'); + const btn = findTestSubject(comp, 'toggleFieldFilterButton'); + return { comp, input, btn, props }; + } + + test('enter value', () => { + const { input, props } = mountComponent(); + input.simulate('change', { target: { value: 'new filter' } }); + expect(props.onChange).toBeCalledTimes(1); + }); + + // this should work, but doesn't, have to do some research + test('click toggle filter button', () => { + const { btn, props } = mountComponent(); + btn.simulate('click'); + expect(props.onShowFilter).toBeCalledTimes(1); + }); + + test('change showFilter value should change button label', () => { + const { btn, comp } = mountComponent(); + const prevFilterBtnHTML = btn.html(); + comp.setProps({ showFilter: true }); + expect(btn.html()).not.toBe(prevFilterBtnHTML); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx new file mode 100644 index 00000000000000..666ccf0acfc7a3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx @@ -0,0 +1,91 @@ +/* + * 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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +export interface Props { + /** + * triggered on input of user into search field + */ + onChange: (field: string, value: string) => void; + /** + * triggered when the "additional filter btn" is clicked + */ + onShowFilter: () => void; + /** + * determines whether additional filter fields are displayed + */ + showFilter: boolean; + /** + * the input value of the user + */ + value?: string; +} + +/** + * Component is Discover's side bar to search of available fields + * Additionally there's a button displayed that allows the user to show/hide more filter fields + */ +export function DiscoverFieldSearch({ showFilter, onChange, onShowFilter, value }: Props) { + if (typeof value !== 'string') { + // at initial rendering value is undefined (angular related), this catches the warning + // should be removed once all is react + return null; + } + const filterBtnAriaLabel = showFilter + ? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { + defaultMessage: 'Hide field filter settings', + }) + : i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { + defaultMessage: 'Show field filter settings', + }); + const searchPlaceholder = i18n.translate('kbn.discover.fieldChooser.searchPlaceHolder', { + defaultMessage: 'Search fields', + }); + + return ( + + + onChange('name', event.currentTarget.value)} + placeholder={searchPlaceholder} + value={value} + /> + + + + onShowFilter()} + size="m" + /> + + + + ); +} diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts similarity index 60% rename from src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx rename to src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts index 585fad0e058b73..baf8f3040d6b02 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts @@ -16,17 +16,18 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { wrapInI18nContext } from 'ui/i18n'; +import { DiscoverFieldSearch } from './discover_field_search'; -import { - fatalErrorsServiceMock, - notificationServiceMock, -} from '../../../../../../../core/public/mocks'; +const app = uiModules.get('apps/discover'); -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - notifications: notificationServiceMock.createSetupContract(), - }, - }, -})); +app.directive('discoverFieldSearch', function(reactDirective: any) { + return reactDirective(wrapInI18nContext(DiscoverFieldSearch), [ + ['onChange', { watchDepth: 'reference' }], + ['onShowFilter', { watchDepth: 'reference' }], + ['showFilter', { watchDepth: 'value' }], + ['value', { watchDepth: 'value' }], + ]); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html index 96aa1582b5243e..d1a75adac5b82f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html @@ -5,6 +5,75 @@ index-pattern-list="indexPatternList" > + -
    +

-
- -
- - -