diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index d44de3c2efe2f..814a7d374506f 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -185,7 +185,7 @@ There are two things you can do to if you'd like to ensure a field is searchable 1. Index your additional data as {apm-guide-ref}/metadata.html[labels] instead. These are dynamic by default, which means they will be indexed and become searchable and aggregatable. -2. Use the {apm-guide-ref}/configuration-template.html[`append_fields`] feature. As an example, +2. Use the `append_fields` feature. As an example, adding the following to `apm-server.yml` will enable dynamic indexing for `http.request.cookies`: [source,yml] diff --git a/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts b/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts index 69f10efd97d66..e3143a318a16e 100644 --- a/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts +++ b/packages/kbn-es-query/src/filters/build_filters/query_string_filter.ts @@ -39,11 +39,9 @@ export const isQueryStringFilter = (filter: Filter): filter is QueryStringFilter * * @public */ -export const buildQueryFilter = (query: QueryStringFilter['query'], index: string, alias: string) => - ({ - query, - meta: { - index, - alias, - }, - } as QueryStringFilter); +export const buildQueryFilter = ( + query: QueryStringFilter['query'], + index: string, + alias?: string, + meta: QueryStringFilterMeta = {} +) => ({ query, meta: { index, alias, ...meta } }); diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts index 3d8dcad08149c..b1c421ec9168a 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes.test.ts @@ -67,7 +67,7 @@ describe('migration v2', () => { es: { license: 'basic', dataArchive: Path.join(__dirname, 'archives', '7.14.0_xpack_sample_saved_objects.zip'), - esArgs: ['http.max_content_length=1715275b'], + esArgs: ['http.max_content_length=1715329b'], }, }, })); @@ -85,7 +85,7 @@ describe('migration v2', () => { }); it('completes the migration even when a full batch would exceed ES http.max_content_length', async () => { - root = createRoot({ maxBatchSizeBytes: 1715275 }); + root = createRoot({ maxBatchSizeBytes: 1715329 }); esServer = await startES(); await root.preboot(); await root.setup(); @@ -109,7 +109,7 @@ describe('migration v2', () => { await root.preboot(); await root.setup(); await expect(root.start()).rejects.toMatchInlineSnapshot( - `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` + `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715329 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` ); await retryAsync( @@ -122,7 +122,7 @@ describe('migration v2', () => { expect( records.find((rec) => rec.message.startsWith( - `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715274 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` + `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715329 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` ) ) ).toBeDefined(); diff --git a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts index 33f00248a110a..0352e655937da 100644 --- a/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts +++ b/src/core/server/saved_objects/migrations/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts @@ -54,7 +54,7 @@ describe('migration v2', () => { }); it('fails with a descriptive message when maxBatchSizeBytes exceeds ES http.max_content_length', async () => { - root = createRoot({ maxBatchSizeBytes: 1715275 }); + root = createRoot({ maxBatchSizeBytes: 1715329 }); esServer = await startES(); await root.preboot(); await root.setup(); diff --git a/src/plugins/data/common/search/expressions/kibana_context_type.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts index 9cc69c769f695..673372749c881 100644 --- a/src/plugins/data/common/search/expressions/kibana_context_type.ts +++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts @@ -5,10 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; -import { Filter } from '../../es_query'; +import { Filter } from '@kbn/es-query'; +import { ExpressionValueBoxed, ExpressionValueFilter } from 'src/plugins/expressions/common'; import { Query, TimeRange } from '../../query'; -import { IndexPatternField } from '../..'; +import { adaptToExpressionValueFilter, IndexPatternField } from '../..'; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type ExecutionContextSearch = { @@ -45,5 +45,13 @@ export const kibanaContext = { type: 'null', }; }, + filter: (input: KibanaContext): ExpressionValueFilter => { + const { filters = [] } = input; + return { + type: 'filter', + filterType: 'filter', + and: filters.map(adaptToExpressionValueFilter), + }; + }, }, }; diff --git a/src/plugins/data/common/search/expressions/select_filter.test.ts b/src/plugins/data/common/search/expressions/select_filter.test.ts index a2515dbcb171d..8ef2b77b1fcc6 100644 --- a/src/plugins/data/common/search/expressions/select_filter.test.ts +++ b/src/plugins/data/common/search/expressions/select_filter.test.ts @@ -28,6 +28,12 @@ describe('interpreter/functions#selectFilter', () => { }, query: {}, }, + { + meta: { + group: 'g3', + }, + query: {}, + }, { meta: { group: 'g1', @@ -68,6 +74,12 @@ describe('interpreter/functions#selectFilter', () => { }, "query": Object {}, }, + Object { + "meta": Object { + "group": "g3", + }, + "query": Object {}, + }, Object { "meta": Object { "controlledBy": "i1", @@ -94,8 +106,8 @@ describe('interpreter/functions#selectFilter', () => { `); }); - it('selects filters belonging to certain group', () => { - const actual = fn(kibanaContext, { group: 'g1' }, createMockContext()); + it('selects filters belonging to certain groups', () => { + const actual = fn(kibanaContext, { group: ['g1', 'g3'] }, createMockContext()); expect(actual).toMatchInlineSnapshot(` Object { "filters": Array [ @@ -105,6 +117,12 @@ describe('interpreter/functions#selectFilter', () => { }, "query": Object {}, }, + Object { + "meta": Object { + "group": "g3", + }, + "query": Object {}, + }, Object { "meta": Object { "controlledBy": "i1", diff --git a/src/plugins/data/common/search/expressions/select_filter.ts b/src/plugins/data/common/search/expressions/select_filter.ts index 3e76f3a6426c2..600da4b16d274 100644 --- a/src/plugins/data/common/search/expressions/select_filter.ts +++ b/src/plugins/data/common/search/expressions/select_filter.ts @@ -11,7 +11,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { KibanaContext } from './kibana_context_type'; interface Arguments { - group?: string; + group: string[]; from?: string; ungrouped?: boolean; } @@ -37,6 +37,7 @@ export const selectFilterFunction: ExpressionFunctionSelectFilter = { help: i18n.translate('data.search.functions.selectFilter.group.help', { defaultMessage: 'Select only filters belonging to the provided group', }), + multi: true, }, from: { types: ['string'], @@ -54,13 +55,15 @@ export const selectFilterFunction: ExpressionFunctionSelectFilter = { }, }, - fn(input, { group, ungrouped, from }) { + fn(input, { group = [], ungrouped, from }) { return { ...input, filters: input.filters?.filter(({ meta }) => { const isGroupMatching = - (!group && !ungrouped) || group === meta.group || (ungrouped && !meta.group); + (!group.length && !ungrouped) || + (meta.group && group.length && group.includes(meta.group)) || + (ungrouped && !meta.group); const isOriginMatching = !from || from === meta.controlledBy; return isGroupMatching && isOriginMatching; }) || [], diff --git a/src/plugins/data/common/search/expressions/utils/filters_adapter.ts b/src/plugins/data/common/search/expressions/utils/filters_adapter.ts new file mode 100644 index 0000000000000..304150ad94813 --- /dev/null +++ b/src/plugins/data/common/search/expressions/utils/filters_adapter.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter } from '@kbn/es-query'; +import { ExpressionValueFilter } from 'src/plugins/expressions/common'; + +function getGroupFromFilter(filter: Filter) { + const { meta } = filter; + const { group } = meta ?? {}; + return group; +} + +function range(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { range: rangeQuery } = query ?? {}; + const column = Object.keys(rangeQuery)[0]; + const { gte: from, lte: to } = rangeQuery[column] ?? {}; + return { + filterGroup: getGroupFromFilter(filter), + from, + to, + column, + type: 'filter', + filterType: 'time', + and: [], + }; +} + +function luceneQueryString(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { query_string: queryString } = query ?? {}; + const { query: queryValue } = queryString; + + return { + filterGroup: getGroupFromFilter(filter), + query: queryValue, + type: 'filter', + filterType: 'luceneQueryString', + and: [], + }; +} + +function term(filter: Filter): ExpressionValueFilter { + const { query } = filter; + const { term: termQuery } = query ?? {}; + const column = Object.keys(termQuery)[0]; + const { value } = termQuery[column] ?? {}; + + return { + filterGroup: getGroupFromFilter(filter), + column, + value, + type: 'filter', + filterType: 'exactly', + and: [], + }; +} + +const adapters = { range, term, luceneQueryString }; + +export function adaptToExpressionValueFilter(filter: Filter): ExpressionValueFilter { + const { query = {} } = filter; + const filterType = Object.keys(query)[0] as keyof typeof adapters; + const adapt = adapters[filterType]; + if (!adapt || typeof adapt !== 'function') { + throw new Error(`Unknown filter type: ${filterType}`); + } + return adapt(filter); +} diff --git a/src/plugins/data/common/search/expressions/utils/index.ts b/src/plugins/data/common/search/expressions/utils/index.ts index a6ea8da6ac6e9..b678bd8781d93 100644 --- a/src/plugins/data/common/search/expressions/utils/index.ts +++ b/src/plugins/data/common/search/expressions/utils/index.ts @@ -7,3 +7,4 @@ */ export * from './function_wrapper'; +export { adaptToExpressionValueFilter } from './filters_adapter'; diff --git a/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap b/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap index 1d4a8614b2921..2714dbd2265a4 100644 --- a/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap +++ b/src/plugins/expressions/common/executor/__snapshots__/executor.test.ts.snap @@ -4,5 +4,6 @@ exports[`Executor .inject .getAllMigrations returns list of all registered migra Object { "7.10.0": [Function], "7.10.1": [Function], + "8.1.0": [Function], } `; diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts index 7e314788b03fd..be985c2720f8b 100644 --- a/src/plugins/expressions/common/executor/executor.test.ts +++ b/src/plugins/expressions/common/executor/executor.test.ts @@ -246,6 +246,40 @@ describe('Executor', () => { }); describe('.migrateToLatest', () => { + const fnMigrateTo = { + name: 'fnMigrateTo', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + fn: jest.fn(), + }; + + const fnMigrateFrom = { + name: 'fnMigrateFrom', + help: 'test', + args: { + bar: { + types: ['string'], + help: 'test', + }, + }, + migrations: { + '8.1.0': ((state: ExpressionAstFunction, version: string) => { + const migrateToAst = parseExpression('fnMigrateTo'); + const { arguments: args } = state; + const ast = { ...migrateToAst.chain[0], arguments: args }; + return { type: 'expression', chain: [ast, ast] }; + }) as unknown as MigrateFunction, + }, + fn: jest.fn(), + }; + executor.registerFunction(fnMigrateFrom); + executor.registerFunction(fnMigrateTo); + test('calls migrate function for every expression function in expression', () => { executor.migrateToLatest({ state: parseExpression( @@ -255,6 +289,25 @@ describe('Executor', () => { }); expect(migrateFn).toBeCalledTimes(5); }); + + test('migrates expression function to expression function or chain of expression functions', () => { + const plainExpression = 'foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'; + const plainExpressionAst = parseExpression(plainExpression); + const migratedExpressionAst = executor.migrateToLatest({ + state: parseExpression(`${plainExpression} | fnMigrateFrom bar="baz" | fnMigrateTo`), + version: '8.0.0', + }); + + expect(migratedExpressionAst).toEqual({ + type: 'expression', + chain: [ + ...plainExpressionAst.chain, + { type: 'function', function: 'fnMigrateTo', arguments: { bar: ['baz'] } }, + { type: 'function', function: 'fnMigrateTo', arguments: { bar: ['baz'] } }, + { type: 'function', function: 'fnMigrateTo', arguments: {} }, + ], + }); + }); }); }); }); diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 01b54d13f8a76..86516344031a0 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -241,6 +241,61 @@ export class Executor = Record ExpressionAstFunction | ExpressionAstExpression + ): ExpressionAstExpression { + let additionalFunctions = 0; + return ( + ast.chain.reduce( + (newAst: ExpressionAstExpression, funcAst: ExpressionAstFunction, index: number) => { + const realIndex = index + additionalFunctions; + const { function: fnName, arguments: fnArgs } = funcAst; + const fn = getByAlias(this.getFunctions(), fnName); + if (!fn) { + return newAst; + } + + // if any of arguments are expressions we should migrate those first + funcAst.arguments = mapValues(fnArgs, (asts) => + asts.map((arg) => + arg != null && typeof arg === 'object' + ? this.walkAstAndTransform(arg, transform) + : arg + ) + ); + + const transformedFn = transform(fn, funcAst); + if (transformedFn.type === 'function') { + const prevChain = realIndex > 0 ? newAst.chain.slice(0, realIndex) : []; + const nextChain = newAst.chain.slice(realIndex + 1); + return { + ...newAst, + chain: [...prevChain, transformedFn, ...nextChain], + }; + } + + if (transformedFn.type === 'expression') { + const { chain } = transformedFn; + const prevChain = realIndex > 0 ? newAst.chain.slice(0, realIndex) : []; + const nextChain = newAst.chain.slice(realIndex + 1); + additionalFunctions += chain.length - 1; + return { + ...newAst, + chain: [...prevChain, ...chain, ...nextChain], + }; + } + + return newAst; + }, + ast + ) ?? ast + ); + } + public inject(ast: ExpressionAstExpression, references: SavedObjectReference[]) { let linkId = 0; return this.walkAst(cloneDeep(ast), (fn, link) => { @@ -296,14 +351,12 @@ export class Executor = Record { + return this.walkAstAndTransform(cloneDeep(ast) as ExpressionAstExpression, (fn, link) => { if (!fn.migrations[version]) { - return; + return link; } - ({ arguments: link.arguments, type: link.type } = fn.migrations[version]( - link - ) as ExpressionAstFunction); + return fn.migrations[version](link) as ExpressionAstExpression; }); } diff --git a/src/plugins/expressions/common/expression_types/specs/filter.ts b/src/plugins/expressions/common/expression_types/specs/filter.ts index dad69f9433a23..915beceb988fd 100644 --- a/src/plugins/expressions/common/expression_types/specs/filter.ts +++ b/src/plugins/expressions/common/expression_types/specs/filter.ts @@ -15,6 +15,7 @@ export type ExpressionValueFilter = ExpressionValueBoxed< 'filter', { filterType?: string; + filterGroup?: string; value?: string; column?: string; and: ExpressionValueFilter[]; diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts new file mode 100644 index 0000000000000..29702c3356865 --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DependencyManager } from './dependency_manager'; + +describe('DependencyManager', () => { + it('orderDependencies. Should sort topology by dependencies', () => { + const graph = { + N: [], + R: [], + A: ['B', 'C'], + B: ['D'], + C: ['F', 'B'], + F: ['E'], + E: ['D'], + D: ['L'], + }; + const sortedTopology = ['N', 'R', 'L', 'D', 'B', 'E', 'F', 'C', 'A']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + + it('orderDependencies. Should return base topology if no depended vertices', () => { + const graph = { + N: [], + R: [], + D: undefined, + }; + const sortedTopology = ['N', 'R', 'D']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + + it('orderDependencies. Should detect circular dependencies and throw error with path', () => { + const graph = { + N: ['R'], + R: ['A'], + A: ['B'], + B: ['C'], + C: ['D'], + D: ['E'], + E: ['F'], + F: ['L'], + L: ['G'], + G: ['N'], + }; + const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> '); + const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + + expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + }); +}); diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts new file mode 100644 index 0000000000000..de30b180607fe --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +type GraphVertex = string | number | symbol; +type Graph = Record; +type BreadCrumbs = Record; + +interface CycleDetectionResult { + hasCycle: boolean; + path: T[]; +} + +export class DependencyManager { + static orderDependencies(graph: Graph) { + const cycleInfo = DependencyManager.getSortedDependencies(graph); + if (cycleInfo.hasCycle) { + const error = DependencyManager.getCyclePathError(cycleInfo.path); + DependencyManager.throwCyclicPathError(error); + } + + return cycleInfo.path; + } + + /** + * DFS algorithm for checking if graph is a DAG (Directed Acyclic Graph) + * and sorting topogy (dependencies) if graph is DAG. + * @param {Graph} graph - graph of dependencies. + */ + private static getSortedDependencies( + graph: Graph = {} as Graph + ): CycleDetectionResult { + const sortedVertices: Set = new Set(); + const vertices = Object.keys(graph) as T[]; + return vertices.reduce>((cycleInfo, srcVertex) => { + if (cycleInfo.hasCycle) { + return cycleInfo; + } + + return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {}); + }, DependencyManager.createCycleInfo()); + } + + /** + * Modified DFS algorithm for topological sort. + * @param {T extends GraphVertex} srcVertex - a source vertex - the start point of dependencies ordering. + * @param {Graph} graph - graph of dependencies, represented in the adjacency list form. + * @param {Set} sortedVertices - ordered dependencies path from the free to the dependent vertex. + * @param {BreadCrumbs} visited - record of visited vertices. + * @param {BreadCrumbs} inpath - record of vertices, which was met in the path. Is used for detecting cycles. + */ + private static sortVerticesFrom( + srcVertex: T, + graph: Graph, + sortedVertices: Set, + visited: BreadCrumbs = {}, + inpath: BreadCrumbs = {} + ): CycleDetectionResult { + visited[srcVertex] = true; + inpath[srcVertex] = true; + const cycleInfo = graph[srcVertex]?.reduce | undefined>( + (info, vertex) => { + if (inpath[vertex]) { + const path = (Object.keys(inpath) as T[]).filter( + (visitedVertex) => inpath[visitedVertex] + ); + return DependencyManager.createCycleInfo([...path, vertex], true); + } else if (!visited[vertex]) { + return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath); + } + return info; + }, + undefined + ); + + inpath[srcVertex] = false; + + if (!sortedVertices.has(srcVertex)) { + sortedVertices.add(srcVertex); + } + + return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]); + } + + private static createCycleInfo( + path: T[] = [], + hasCycle: boolean = false + ): CycleDetectionResult { + return { hasCycle, path }; + } + + private static getCyclePathError( + cyclePath: CycleDetectionResult['path'] + ) { + const cycleString = cyclePath.join(' -> '); + return `Circular dependency detected while setting up services: ${cycleString}`; + } + + private static throwCyclicPathError(error: string) { + throw new Error(error); + } +} diff --git a/src/plugins/presentation_util/public/services/create/factory.ts b/src/plugins/presentation_util/public/services/create/factory.ts index ddc2e5845b037..49ed5ef8aaf8d 100644 --- a/src/plugins/presentation_util/public/services/create/factory.ts +++ b/src/plugins/presentation_util/public/services/create/factory.ts @@ -16,7 +16,10 @@ import { CoreStart, AppUpdater, PluginInitializerContext } from 'src/core/public * The `StartParameters` generic determines what parameters are expected to * create the service. */ -export type PluginServiceFactory = (params: Parameters) => Service; +export type PluginServiceFactory = ( + params: Parameters, + requiredServices: RequiredServices +) => Service; /** * Parameters necessary to create a Kibana-based service, (e.g. during Plugin @@ -38,6 +41,7 @@ export interface KibanaPluginServiceParams { * The `Setup` generic refers to the specific Plugin `TPluginsSetup`. * The `Start` generic refers to the specific Plugin `TPluginsStart`. */ -export type KibanaPluginServiceFactory = ( - params: KibanaPluginServiceParams +export type KibanaPluginServiceFactory = ( + params: KibanaPluginServiceParams, + requiredServices: RequiredServices ) => Service; diff --git a/src/plugins/presentation_util/public/services/create/provider.tsx b/src/plugins/presentation_util/public/services/create/provider.tsx index 06590bcfbb3d0..3271dc52fd9d0 100644 --- a/src/plugins/presentation_util/public/services/create/provider.tsx +++ b/src/plugins/presentation_util/public/services/create/provider.tsx @@ -17,7 +17,25 @@ import { PluginServiceFactory } from './factory'; * start the service. */ export type PluginServiceProviders = { - [K in keyof Services]: PluginServiceProvider; + [K in keyof Services]: PluginServiceProvider< + Services[K], + StartParameters, + Services, + Array + >; +}; + +type ElementOfArray = ArrayType extends Array< + infer ElementType +> + ? ElementType + : never; + +export type PluginServiceRequiredServices< + RequiredServices extends Array, + AvailableServices +> = { + [K in ElementOfArray]: AvailableServices[K]; }; /** @@ -27,16 +45,34 @@ export type PluginServiceProviders = { * The `StartParameters` generic determines what parameters are expected to * start the service. */ -export class PluginServiceProvider { - private factory: PluginServiceFactory; +export class PluginServiceProvider< + Service extends {}, + StartParameters = {}, + Services = {}, + RequiredServices extends Array = [] +> { + private factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >; + private _requiredServices?: RequiredServices; private context = createContext(null); private pluginService: Service | null = null; public readonly Provider: React.FC = ({ children }) => { return {children}; }; - constructor(factory: PluginServiceFactory) { + constructor( + factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >, + requiredServices?: RequiredServices + ) { this.factory = factory; + this._requiredServices = requiredServices; this.context.displayName = 'PluginServiceContext'; } @@ -55,8 +91,11 @@ export class PluginServiceProvider { * * @param params Parameters used to start the service. */ - start(params: StartParameters) { - this.pluginService = this.factory(params); + start( + params: StartParameters, + requiredServices: PluginServiceRequiredServices + ) { + this.pluginService = this.factory(params, requiredServices); } /** @@ -80,4 +119,8 @@ export class PluginServiceProvider { stop() { this.pluginService = null; } + + public get requiredServices() { + return this._requiredServices ?? []; + } } diff --git a/src/plugins/presentation_util/public/services/create/providers_mediator.ts b/src/plugins/presentation_util/public/services/create/providers_mediator.ts new file mode 100644 index 0000000000000..dd5937149850c --- /dev/null +++ b/src/plugins/presentation_util/public/services/create/providers_mediator.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DependencyManager } from './dependency_manager'; +import { PluginServiceProviders, PluginServiceRequiredServices } from './provider'; + +export class PluginServiceProvidersMediator { + constructor(private readonly providers: PluginServiceProviders) {} + + start(params: StartParameters) { + this.getOrderedDependencies().forEach((service) => { + this.providers[service].start(params, this.getServiceDependencies(service)); + }); + } + + stop() { + this.getOrderedDependencies().forEach((service) => this.providers[service].stop()); + } + + private getOrderedDependencies() { + const dependenciesGraph = this.getGraphOfDependencies(); + return DependencyManager.orderDependencies(dependenciesGraph); + } + + private getGraphOfDependencies() { + return this.getProvidersNames().reduce>>( + (graph, vertex) => ({ ...graph, [vertex]: this.providers[vertex].requiredServices ?? [] }), + {} as Record> + ); + } + + private getProvidersNames() { + return Object.keys(this.providers) as Array; + } + + private getServiceDependencies(service: keyof Services) { + const requiredServices = this.providers[service].requiredServices ?? []; + return this.getServicesByDeps(requiredServices); + } + + private getServicesByDeps(deps: Array) { + return deps.reduce, Services>>( + (services, dependency) => ({ + ...services, + [dependency]: this.providers[dependency].getService(), + }), + {} as PluginServiceRequiredServices, Services> + ); + } +} diff --git a/src/plugins/presentation_util/public/services/create/registry.tsx b/src/plugins/presentation_util/public/services/create/registry.tsx index e8f85666bcac4..8369815a042af 100644 --- a/src/plugins/presentation_util/public/services/create/registry.tsx +++ b/src/plugins/presentation_util/public/services/create/registry.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { PluginServiceProvider, PluginServiceProviders } from './provider'; +import { PluginServiceProvidersMediator } from './providers_mediator'; /** * A `PluginServiceRegistry` maintains a set of service providers which can be collectively @@ -19,10 +20,12 @@ import { PluginServiceProvider, PluginServiceProviders } from './provider'; */ export class PluginServiceRegistry { private providers: PluginServiceProviders; + private providersMediator: PluginServiceProvidersMediator; private _isStarted = false; constructor(providers: PluginServiceProviders) { this.providers = providers; + this.providersMediator = new PluginServiceProvidersMediator(providers); } /** @@ -69,8 +72,7 @@ export class PluginServiceRegistry { * @param params Parameters used to start the registry. */ start(params: StartParameters) { - const providerNames = Object.keys(this.providers) as Array; - providerNames.forEach((providerName) => this.providers[providerName].start(params)); + this.providersMediator.start(params); this._isStarted = true; return this; } @@ -79,8 +81,7 @@ export class PluginServiceRegistry { * Stop the registry. */ stop() { - const providerNames = Object.keys(this.providers) as Array; - providerNames.forEach((providerName) => this.providers[providerName].stop()); + this.providersMediator.stop(); this._isStarted = false; return this; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index 38dfd2261da00..3a91039d81c7a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -13,9 +13,10 @@ export const areaChart: ElementFactory = () => ({ help: 'A line chart with a filled body', type: 'chart', icon: 'visArea', - expression: `filters - | demodata - | pointseries x="time" y="mean(price)" - | plot defaultStyle={seriesStyle lines=1 fill=1} - | render`, + expression: `kibana +| selectFilter +| demodata +| pointseries x="time" y="mean(price)" +| plot defaultStyle={seriesStyle lines=1 fill=1} +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index c3f07ae601db9..b1b657bb37ff5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -15,7 +15,8 @@ export const bubbleChart: ElementFactory = () => ({ width: 700, height: 300, icon: 'heatmap', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="sum(price)" color="state" size="size(username)" | plot defaultStyle={seriesStyle points=5 fill=1} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts index d4bf6ef6f569b..c6db5ff4e3309 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts @@ -12,6 +12,7 @@ export const filterDebug: ElementFactory = () => ({ displayName: 'Debug filter', help: 'Shows the underlying global filters in a workpad', icon: 'bug', - expression: `filters + expression: `kibana +| selectFilter | render as=debug`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index c15ca14572606..9c01259c6d9e8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -13,7 +13,8 @@ export const horizontalBarChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable horizontal bar chart', icon: 'visBarHorizontal', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="size(cost)" y="project" color="project" | plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} legend=false diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index f4aabba4ca216..ef278fbea3411 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -15,7 +15,8 @@ export const horizontalProgressBar: ElementFactory = () => ({ help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="horizontalBar" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index d1d723a176b45..1675c2c78cdcb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -15,7 +15,8 @@ export const horizontalProgressPill: ElementFactory = () => ({ help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="horizontalPill" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index 84a3aee434141..cdcb9bb584b5d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -13,7 +13,8 @@ export const lineChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable line chart', icon: 'visLine', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="time" y="mean(price)" | plot defaultStyle={seriesStyle lines=3} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 6d8edd21c7e73..7bffff4fe95cd 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -12,7 +12,8 @@ export const markdown: ElementFactory = () => ({ type: 'text', help: 'Add text using Markdown', icon: 'visText', - expression: `filters + expression: `kibana +| selectFilter | demodata | markdown "### Welcome to the Markdown element diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 76176f6ba2133..aa18e235f5fd9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -19,13 +19,14 @@ export const metricElementInitializer: SetupInitializer = (core, width: 200, height: 100, icon: 'visMetric', - expression: `filters - | demodata - | math "unique(country)" - | metric "Countries" - metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} - labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${core.uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" - | render`, + expression: `kibana +| selectFilter +| demodata +| math "unique(country)" +| metric "Countries" + metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} + labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${core.uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN)}" +| render`, }); }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts index 3f01a8ccb3e73..3c5a4c16565c6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts @@ -12,9 +12,10 @@ export const metricVis: ElementFactory = () => ({ type: 'chart', help: 'Metric visualization', icon: 'visMetric', - expression: `filters - | demodata - | head 1 - | metricVis metric={visdimension "percent_uptime"} colorMode="Labels" - | render`, + expression: `kibana +| selectFilter +| demodata +| head 1 +| metricVis metric={visdimension "percent_uptime"} colorMode="Labels" +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index 2094af748ab16..4739e6ca16474 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -14,7 +14,8 @@ export const pie: ElementFactory = () => ({ height: 300, help: 'A simple pie chart', icon: 'visPie', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries color="state" size="max(price)" | pie diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index 3e879b7fb58db..c0ebfa60708d4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -12,7 +12,8 @@ export const plot: ElementFactory = () => ({ displayName: 'Coordinate plot', type: 'chart', help: 'Mixed line, bar or dot charts', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="time" y="sum(price)" color="state" | plot defaultStyle={seriesStyle points=5} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index e07a848263f50..85f853cea759b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -16,7 +16,8 @@ export const progressGauge: ElementFactory = () => ({ width: 200, height: 200, icon: 'visGoal', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 6c61ab24d13b2..100f5c65eb94a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -15,7 +15,8 @@ export const progressSemicircle: ElementFactory = () => ({ help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="semicircle" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index 15fec0d3b6390..1d9ffde49ff8b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -15,7 +15,8 @@ export const progressWheel: ElementFactory = () => ({ help: 'Displays progress as a portion of a wheel', width: 200, height: 200, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="wheel" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index 783b17e7d9362..6a064ffd297ec 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -12,7 +12,8 @@ export const repeatImage: ElementFactory = () => ({ displayName: 'Image repeat', type: 'image', help: 'Repeats an image N times', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(cost)" | repeatImage image=null diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index b2b4ea4a942a3..b78e0d1d5cf24 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -12,7 +12,8 @@ export const revealImage: ElementFactory = () => ({ displayName: 'Image reveal', type: 'image', help: 'Reveals a percentage of an image', - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | revealImage origin=bottom image=null diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts index 710f595ba7179..417fe09fbc586 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -13,7 +13,8 @@ export const table: ElementFactory = () => ({ type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', icon: 'visTable', - expression: `filters + expression: `kibana +| selectFilter | demodata | table | render`, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts index b3543d532b9be..698468ab2e150 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/tag_cloud/index.ts @@ -12,10 +12,11 @@ export const tagCloud: ElementFactory = () => ({ type: 'chart', help: 'Tagcloud visualization', icon: 'visTagCloud', - expression: `filters - | demodata - | ply by="country" fn={math "count(country)" | as "Count"} - | filterrows fn={getCell "Count" | gte 10} - | tagcloud metric={visdimension "Count"} bucket={visdimension "country"} - | render`, + expression: `kibana +| selectFilter +| demodata +| ply by="country" fn={math "count(country)" | as "Count"} +| filterrows fn={getCell "Count" | gte 10} +| tagcloud metric={visdimension "Count"} bucket={visdimension "country"} +| render`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index de573166c8e9a..a90f79aa995c5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -13,7 +13,8 @@ export const verticalBarChart: ElementFactory = () => ({ type: 'chart', help: 'A customizable vertical bar chart', icon: 'visBarVertical', - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="size(cost)" color="project" | plot defaultStyle={seriesStyle bars=0.75} legend=false diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index 04ee9c8cb7db2..89ffc18766bcd 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -15,7 +15,8 @@ export const verticalProgressBar: ElementFactory = () => ({ help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="verticalBar" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 7bbf3874f175f..b3a977c1d795a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -15,7 +15,8 @@ export const verticalProgressPill: ElementFactory = () => ({ help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="verticalPill" label={formatnumber 0%} font={font size=24 family="${openSans.value}" color="#000000" align=center} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts index c0ed25849ac97..66553b6fda6c0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts @@ -47,13 +47,14 @@ export function exactly(): ExpressionFunctionDefinition< }, }, fn: (input, args) => { - const { value, column } = args; + const { value, column, filterGroup } = args; const filter: ExpressionValueFilter = { type: 'filter', filterType: 'exactly', value, column, + filterGroup, and: [], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts index e4a6a102844a9..b61e03319b916 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts @@ -58,11 +58,12 @@ export function timefilter(): ExpressionFunctionDefinition< return input; } - const { from, to, column } = args; + const { from, to, column, filterGroup } = args; const filter: ExpressionValueFilter = { type: 'filter', filterType: 'time', column, + filterGroup, and: [], }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts new file mode 100644 index 0000000000000..75bd97421e58e --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromExpression } from '@kbn/interpreter'; +import { filters } from './filters'; + +const { migrations } = filters(); + +describe('filters migrations', () => { + const expression = 'filters group="1" group="3" ungrouped=true'; + const ast = fromExpression(expression); + it('8.1.0. Should migrate `filters` expression to `kibana | selectFilter`', () => { + const migratedAst = migrations?.['8.1.0'](ast.chain[0]); + expect(migratedAst !== null && typeof migratedAst === 'object').toBeTruthy(); + expect(migratedAst.type).toBe('expression'); + expect(Array.isArray(migratedAst.chain)).toBeTruthy(); + expect(migratedAst.chain[0].function === 'kibana').toBeTruthy(); + expect(migratedAst.chain[0].arguments).toEqual({}); + expect(migratedAst.chain[1].function === 'selectFilter').toBeTruthy(); + expect(migratedAst.chain[1].arguments).toEqual(ast.chain[0].arguments); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts new file mode 100644 index 0000000000000..8b46e818209f3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/filters.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ExpressionValueFilter, + ExpressionAstExpression, + ExpressionAstFunction, +} from 'src/plugins/expressions'; +import { fromExpression } from '@kbn/interpreter'; +import { buildFiltersFunction } from '../../../common/functions'; +import type { FiltersFunction } from '../../../common/functions'; + +/* + Expression function `filters` can't be used on the server, because it is tightly coupled with the redux store. + It is replaced with `kibana | selectFilter`. + + Current filters function definition is used only for the purpose of enabling migrations. + The function has to be registered on the server while the plugin's setup, to be able to run its migration. +*/ +const filtersFn = (): ExpressionValueFilter => ({ + type: 'filter', + and: [], +}); + +const migrations: FiltersFunction['migrations'] = { + '8.1.0': (ast: ExpressionAstFunction): ExpressionAstFunction | ExpressionAstExpression => { + const SELECT_FILTERS = 'selectFilter'; + const newExpression = `kibana | ${SELECT_FILTERS}`; + const newAst: ExpressionAstExpression = fromExpression(newExpression); + const selectFiltersAstIndex = newAst.chain.findIndex( + ({ function: fnName }) => fnName === SELECT_FILTERS + ); + const selectFilterAst = newAst.chain[selectFiltersAstIndex]; + newAst.chain.splice(selectFiltersAstIndex, 1, { ...selectFilterAst, arguments: ast.arguments }); + return newAst; + }, +}; + +export const filters = buildFiltersFunction(filtersFn, migrations); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts index ae3778366651c..388db9e6e5960 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/index.ts @@ -7,5 +7,6 @@ import { demodata } from './demodata'; import { pointseries } from './pointseries'; +import { filters } from './filters'; -export const functions = [demodata, pointseries]; +export const functions = [filters, demodata, pointseries]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx index 643d7cdedc50d..38d1d502704e2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/__stories__/render.tsx @@ -21,7 +21,6 @@ export const defaultHandlers: RendererHandlers = { onEmbeddableInputChange: action('onEmbeddableInputChange'), onResize: action('onResize'), resize: action('resize'), - setFilter: action('setFilter'), done: action('done'), onDestroy: action('onDestroy'), reload: action('reload'), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx index b831c9aa70e49..a31021cba4c10 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/advanced_filter/index.tsx @@ -25,7 +25,10 @@ export const advancedFilterFactory: StartInitializer> = render(domNode, _, handlers) { ReactDOM.render( - + handlers.event({ name: 'applyFilterAction', data: filter })} + value={handlers.getFilter()} + /> , domNode, () => handlers.done() diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index 372bcbb5642cb..5e4ea42990e47 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -55,20 +55,19 @@ export const dropdownFilterFactory: StartInitializer> = (filterExpression === undefined || !filterExpression.includes('exactly')) ) { filterExpression = ''; - handlers.setFilter(filterExpression); + handlers.event({ name: 'applyFilterAction', data: filterExpression }); } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed const { changed, newAst } = syncFilterExpression(config, filterExpression, ['filterGroup']); if (changed) { - handlers.setFilter(toExpression(newAst)); + handlers.event({ name: 'applyFilterAction', data: toExpression(newAst) }); } } - const commit = (commitValue: string) => { if (commitValue === '%%CANVAS_MATCH_ALL%%') { - handlers.setFilter(''); + handlers.event({ name: 'applyFilterAction', data: '' }); } else { const newFilterAST: Ast = { type: 'expression', @@ -86,18 +85,19 @@ export const dropdownFilterFactory: StartInitializer> = }; const newFilter = toExpression(newFilterAST); - handlers.setFilter(newFilter); + handlers.event({ name: 'applyFilterAction', data: newFilter }); } }; + const filter = ( + + ); ReactDOM.render( - - - , + {filter}, domNode, () => handlers.done() ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx index e81ca2cc1f057..f7e9d333f8683 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/time_filter/index.tsx @@ -45,7 +45,7 @@ export const timeFilterFactory: StartInitializer> = ( if (filterExpression === undefined || filterExpression.indexOf('timefilter') !== 0) { filterExpression = defaultTimeFilterExpression; - handlers.setFilter(filterExpression); + handlers.event({ name: 'applyFilterAction', data: filterExpression }); } else if (filterExpression !== '') { // NOTE: setFilter() will cause a data refresh, avoid calling unless required // compare expression and filter, update filter if needed @@ -55,14 +55,14 @@ export const timeFilterFactory: StartInitializer> = ( ]); if (changed) { - handlers.setFilter(toExpression(newAst)); + handlers.event({ name: 'applyFilterAction', data: toExpression(newAst) }); } } ReactDOM.render( handlers.event({ name: 'applyFilterAction', data: filter })} filter={filterExpression} commonlyUsedRanges={customQuickRanges} dateFormat={customDateFormat} diff --git a/x-pack/plugins/canvas/common/functions/filters.ts b/x-pack/plugins/canvas/common/functions/filters.ts new file mode 100644 index 0000000000000..5c48fbd10862a --- /dev/null +++ b/x-pack/plugins/canvas/common/functions/filters.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; +import { ExpressionValueFilter } from '../../types'; +import { getFunctionHelp } from '../../i18n'; + +export interface Arguments { + group: string[]; + ungrouped: boolean; +} + +export type FiltersFunction = ExpressionFunctionDefinition< + 'filters', + null, + Arguments, + ExpressionValueFilter +>; + +export function buildFiltersFunction( + fn: FiltersFunction['fn'], + migrations?: FiltersFunction['migrations'] +) { + return function filters(): FiltersFunction { + const { help, args: argHelp } = getFunctionHelp().filters; + + return { + name: 'filters', + type: 'filter', + help, + context: { + types: ['null'], + }, + args: { + group: { + aliases: ['_'], + types: ['string'], + help: argHelp.group, + multi: true, + }, + ungrouped: { + aliases: ['nogroup', 'nogroups'], + types: ['boolean'], + help: argHelp.ungrouped, + default: false, + }, + }, + fn, + migrations, + }; + }; +} diff --git a/x-pack/plugins/canvas/common/functions/index.ts b/x-pack/plugins/canvas/common/functions/index.ts new file mode 100644 index 0000000000000..08d9391f81c13 --- /dev/null +++ b/x-pack/plugins/canvas/common/functions/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { FiltersFunction } from './filters'; +export { buildFiltersFunction } from './filters'; diff --git a/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts index 57fdc7d7309ce..c98d2f080452a 100644 --- a/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts +++ b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts @@ -6,6 +6,8 @@ */ import { buildQueryFilter, Filter } from '@kbn/es-query'; +import dateMath from '@elastic/datemath'; +import { maxBy, minBy } from 'lodash'; import { ExpressionValueFilter } from '../../types'; // @ts-expect-error untyped local import { buildBoolArray } from './build_bool_array'; @@ -16,24 +18,45 @@ export interface EmbeddableFilterInput { timeRange?: TimeRange; } +type ESFilter = Record; + const TimeFilterType = 'time'; +const formatTime = (str: string | undefined, roundUp: boolean = false) => { + if (!str) { + return null; + } + const moment = dateMath.parse(str, { roundUp }); + return !moment || !moment.isValid() ? null : moment.valueOf(); +}; + function getTimeRangeFromFilters(filters: ExpressionValueFilter[]): TimeRange | undefined { - const timeFilter = filters.find( - (filter) => filter.filterType !== undefined && filter.filterType === TimeFilterType + const timeFilters = filters.filter( + (filter) => + filter.filterType !== undefined && + filter.filterType === TimeFilterType && + filter.from !== undefined && + filter.to !== undefined ); - return timeFilter !== undefined && timeFilter.from !== undefined && timeFilter.to !== undefined - ? { - from: timeFilter.from, - to: timeFilter.to, - } + const validatedTimeFilters = timeFilters.filter( + (filter) => formatTime(filter.from) !== null && formatTime(filter.to, true) !== null + ); + + const minFromFilter = minBy(validatedTimeFilters, (filter) => formatTime(filter.from)); + const maxToFilter = maxBy(validatedTimeFilters, (filter) => formatTime(filter.to, true)); + + return minFromFilter?.from && maxToFilter?.to + ? { from: minFromFilter.from, to: maxToFilter.to } : undefined; } export function getQueryFilters(filters: ExpressionValueFilter[]): Filter[] { const dataFilters = filters.map((filter) => ({ ...filter, type: filter.filterType })); - return buildBoolArray(dataFilters).map(buildQueryFilter); + return buildBoolArray(dataFilters).map((filter: ESFilter, index: number) => { + const { group, ...restFilter } = filter; + return buildQueryFilter(restFilter, index.toString(), '', { group }); + }); } export function buildEmbeddableFilters(filters: ExpressionValueFilter[]): EmbeddableFilterInput { diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts index 6a61ec595acb7..fa938f2c07c74 100644 --- a/x-pack/plugins/canvas/common/lib/constants.ts +++ b/x-pack/plugins/canvas/common/lib/constants.ts @@ -16,6 +16,8 @@ export const APP_ROUTE = '/app/canvas'; export const APP_ROUTE_WORKPAD = `${APP_ROUTE}#/workpad`; export const API_ROUTE = '/api/canvas'; export const API_ROUTE_WORKPAD = `${API_ROUTE}/workpad`; +export const API_ROUTE_WORKPAD_EXPORT = `${API_ROUTE_WORKPAD}/export`; +export const API_ROUTE_WORKPAD_IMPORT = `${API_ROUTE_WORKPAD}/import`; export const API_ROUTE_WORKPAD_ASSETS = `${API_ROUTE}/workpad-assets`; export const API_ROUTE_WORKPAD_STRUCTURES = `${API_ROUTE}/workpad-structures`; export const API_ROUTE_CUSTOM_ELEMENT = `${API_ROUTE}/custom-element`; diff --git a/x-pack/plugins/canvas/common/lib/filters.js b/x-pack/plugins/canvas/common/lib/filters.js index 08caded52aa26..f43e2dd3b4606 100644 --- a/x-pack/plugins/canvas/common/lib/filters.js +++ b/x-pack/plugins/canvas/common/lib/filters.js @@ -13,15 +13,16 @@ export function time(filter) { if (!filter.column) { throw new Error('column is required for Elasticsearch range filters'); } + const { from, to, column, filterGroup: group } = filter; return { - range: { - [filter.column]: { gte: filter.from, lte: filter.to }, - }, + group, + range: { [column]: { gte: from, lte: to } }, }; } export function luceneQueryString(filter) { return { + group: filter.filterGroup, query_string: { query: filter.query || '*', }, @@ -30,6 +31,7 @@ export function luceneQueryString(filter) { export function exactly(filter) { return { + group: filter.filterGroup, term: { [filter.column]: { value: filter.value, diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js index 89faef29a3b02..1ca674bfb6f9d 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/index.js @@ -7,16 +7,19 @@ import React, { useState, useEffect } from 'react'; import { PropTypes } from 'prop-types'; -import { interpretAst } from '../../../lib/run_interpreter'; import { Loading } from '../../loading'; +import { useExpressionsService } from '../../../services'; import { DatasourcePreview as Component } from './datasource_preview'; export const DatasourcePreview = (props) => { const [datatable, setDatatable] = useState(); + const expressionsService = useExpressionsService(); useEffect(() => { - interpretAst({ type: 'expression', chain: [props.function] }, {}).then(setDatatable); - }, [props.function, setDatatable]); + expressionsService + .interpretAst({ type: 'expression', chain: [props.function] }, {}) + .then(setDatatable); + }, [expressionsService, props.function, setDatatable]); if (!datatable) { return ; diff --git a/x-pack/plugins/canvas/public/components/function_form_list/index.js b/x-pack/plugins/canvas/public/components/function_form_list/index.js index 6048ac360386c..31db3366ce3b5 100644 --- a/x-pack/plugins/canvas/public/components/function_form_list/index.js +++ b/x-pack/plugins/canvas/public/components/function_form_list/index.js @@ -8,7 +8,7 @@ import { compose, withProps } from 'recompose'; import { get } from 'lodash'; import { toExpression } from '@kbn/interpreter'; -import { interpretAst } from '../../lib/run_interpreter'; +import { pluginServices } from '../../services'; import { getArgTypeDef } from '../../lib/args'; import { FunctionFormList as Component } from './function_form_list'; @@ -77,24 +77,27 @@ const componentFactory = ({ path, parentPath, removable, -}) => ({ - args, - nestedFunctionsArgs: argsWithExprFunctions, - argType: argType.function, - argTypeDef: Object.assign(argTypeDef, { - args: argumentsView, - name: argUiConfig?.name ?? argTypeDef.name, - displayName: argUiConfig?.displayName ?? argTypeDef.displayName, - help: argUiConfig?.help ?? argTypeDef.name, - }), - argResolver: (argAst) => interpretAst(argAst, prevContext), - contextExpression: getExpression(prevContext), - expressionIndex, // preserve the index in the AST - nextArgType: nextArg && nextArg.function, - path, - parentPath, - removable, -}); +}) => { + const { expressions } = pluginServices.getServices(); + return { + args, + nestedFunctionsArgs: argsWithExprFunctions, + argType: argType.function, + argTypeDef: Object.assign(argTypeDef, { + args: argumentsView, + name: argUiConfig?.name ?? argTypeDef.name, + displayName: argUiConfig?.displayName ?? argTypeDef.displayName, + help: argUiConfig?.help ?? argTypeDef.name, + }), + argResolver: (argAst) => expressions.interpretAst(argAst, prevContext), + contextExpression: getExpression(prevContext), + expressionIndex, // preserve the index in the AST + nextArgType: nextArg && nextArg.function, + path, + parentPath, + removable, + }; +}; /** * Converts expression functions at the arguments for the expression, to the array of UI component configurations. diff --git a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts index a8409270752ad..785f183b193f1 100644 --- a/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts +++ b/x-pack/plugins/canvas/public/components/function_reference_generator/function_examples.ts @@ -22,7 +22,8 @@ export const getFunctionExamples = (): FunctionExampleDict => ({ syntax: `all {neq "foo"} {neq "bar"} {neq "fizz"} all condition={gt 10} condition={lt 20}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | formatnumber "0.0%" @@ -42,7 +43,8 @@ all condition={gt 10} condition={lt 20}`, syntax: `alterColumn "cost" type="string" alterColumn column="@timestamp" name="foo"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | alterColumn "time" name="time_in_ms" type="number" | table @@ -54,7 +56,8 @@ alterColumn column="@timestamp" name="foo"`, syntax: `any {eq "foo"} {eq "bar"} {eq "fizz"} any condition={lte 10} condition={gt 30}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | filterrows { getCell "project" | any {eq "elasticsearch"} {eq "kibana"} {eq "x-pack"} @@ -70,7 +73,8 @@ any condition={lte 10} condition={gt 30}`, as "foo" as name="bar"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | ply by="project" fn={math "count(username)" | as "num_users"} fn={math "mean(price)" | as "price"} | pointseries x="project" y="num_users" size="price" color="project" @@ -94,7 +98,8 @@ asset id="asset-498f7429-4d56-42a2-a7e4-8bf08d98d114"`, syntax: `axisConfig show=false axisConfig position="right" min=0 max=10 tickSize=1`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="size(cost)" y="project" color="project" | plot defaultStyle={seriesStyle bars=0.75 horizontalBars=true} @@ -133,7 +138,8 @@ case if={lte 50} then="green"`, syntax: `columns include="@timestamp, projects, cost" columns exclude="username, country, age"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | columns include="price, cost, state, project" | table @@ -145,7 +151,8 @@ columns exclude="username, country, age"`, syntax: `compare "neq" to="elasticsearch" compare op="lte" to=100`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn project fn={getCell project | @@ -229,7 +236,8 @@ date "01/31/2019" format="MM/DD/YYYY"`, demodata "ci" demodata type="shirts"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | table | render`, @@ -252,7 +260,8 @@ eq null eq 10 eq "foo"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn project fn={getCell project | @@ -272,7 +281,8 @@ eq "foo"`, escount "currency:\"EUR\"" index="kibana_sample_data_ecommerce" escount query="response:404" index="kibana_sample_data_logs"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | escount "Cancelled:true" index="kibana_sample_data_flights" | math "value" | progress shape="semicircle" @@ -290,7 +300,8 @@ esdocs query="response:404" index="kibana_sample_data_logs" esdocs index="kibana_sample_data_flights" count=100 esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | esdocs index="kibana_sample_data_ecommerce" fields="customer_gender, taxful_total_price, order_date" sort="order_date, asc" @@ -309,7 +320,8 @@ esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc"`, syntax: `essql query="SELECT * FROM \"logstash*\"" essql "SELECT * FROM \"apm*\"" count=10000`, usage: { - expression: `filters + expression: `kibana +| selectFilter | essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM \"kibana_sample_data_flights\"" | table | render`, @@ -321,7 +333,8 @@ essql "SELECT * FROM \"apm*\"" count=10000`, exactly "age" value=50 filterGroup="group2" exactly column="project" value="beats"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | exactly column=project value=elasticsearch | demodata | pointseries x=project y="mean(age)" @@ -334,7 +347,8 @@ exactly column="project" value="beats"`, syntax: `filterrows {getCell "project" | eq "kibana"} filterrows fn={getCell "age" | gt 50}`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | filterrows {getCell "country" | any {eq "IN"} {eq "US"} {eq "CN"}} | mapColumn "@timestamp" @@ -379,7 +393,8 @@ font underline=true font italic=false font lHeight=32`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | pointseries x="project" y="size(cost)" color="project" | plot defaultStyle={seriesStyle bars=0.75} legend=false @@ -399,7 +414,8 @@ font lHeight=32`, syntax: `formatdate format="YYYY-MM-DD" formatdate "MM/DD/YYYY"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | mapColumn "time" fn={getCell time | formatdate "MMM 'YY"} | pointseries x="time" y="sum(price)" color="state" @@ -412,7 +428,8 @@ formatdate "MM/DD/YYYY"`, syntax: `formatnumber format="$0,0.00" formatnumber "0.0a"`, usage: { - expression: `filters + expression: `kibana +| selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts index eb87f4720deec..3290bc8227a29 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_create_workpad.ts @@ -29,7 +29,7 @@ export const useCreateWorkpad = () => { history.push(`/workpad/${workpad.id}/page/1`); } catch (err) { notifyService.error(err, { - title: errors.getUploadFailureErrorMessage(), + title: errors.getCreateFailureErrorMessage(), }); } return; @@ -39,8 +39,8 @@ export const useCreateWorkpad = () => { }; const errors = { - getUploadFailureErrorMessage: () => - i18n.translate('xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage', { - defaultMessage: `Couldn't upload workpad`, + getCreateFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useCreateWorkpad.createFailureErrorMessage', { + defaultMessage: `Couldn't create workpad`, }), }; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts new file mode 100644 index 0000000000000..8c8d2e26d8a22 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_import_workpad.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +// @ts-expect-error +import { getDefaultWorkpad } from '../../../state/defaults'; +import { useNotifyService, useWorkpadService } from '../../../services'; + +import type { CanvasWorkpad } from '../../../../types'; + +export const useImportWorkpad = () => { + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const history = useHistory(); + + return useCallback( + async (workpad: CanvasWorkpad) => { + try { + const importedWorkpad = await workpadService.import(workpad); + history.push(`/workpad/${importedWorkpad.id}/page/1`); + } catch (err) { + notifyService.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); + } + return; + }, + [notifyService, history, workpadService] + ); +}; + +const errors = { + getUploadFailureErrorMessage: () => + i18n.translate('xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage', { + defaultMessage: `Couldn't upload workpad`, + }), +}; diff --git a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts index caec30e083d40..045ff8b52e259 100644 --- a/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts +++ b/x-pack/plugins/canvas/public/components/home/hooks/use_upload_workpad.ts @@ -9,19 +9,25 @@ import { useCallback } from 'react'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { SavedObject } from 'kibana/public'; import { CANVAS, JSON as JSONString } from '../../../../i18n/constants'; import { useNotifyService } from '../../../services'; import { getId } from '../../../lib/get_id'; - -import { useCreateWorkpad } from './use_create_workpad'; +import { useImportWorkpad as useImportWorkpadHook } from './use_import_workpad'; import type { CanvasWorkpad } from '../../../../types'; +const isInvalidWorkpad = (workpad: CanvasWorkpad) => + !Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets; + export const useImportWorkpad = () => { const notifyService = useNotifyService(); - const createWorkpad = useCreateWorkpad(); + const importWorkpad = useImportWorkpadHook(); return useCallback( - (file?: File, onComplete: (workpad?: CanvasWorkpad) => void = () => {}) => { + ( + file?: File, + onComplete: (workpad?: CanvasWorkpad | SavedObject) => void = () => {} + ) => { if (!file) { onComplete(); return; @@ -42,16 +48,17 @@ export const useImportWorkpad = () => { // handle reading the uploaded file reader.onload = async () => { try { - const workpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + const workpad: CanvasWorkpad = JSON.parse(reader.result as string); // Type-casting because we catch below. + workpad.id = getId('workpad'); // sanity check for workpad object - if (!Array.isArray(workpad.pages) || workpad.pages.length === 0 || !workpad.assets) { + if (isInvalidWorkpad(workpad)) { onComplete(); throw new Error(errors.getMissingPropertiesErrorMessage()); } - await createWorkpad(workpad); + await importWorkpad(workpad); onComplete(workpad); } catch (e) { notifyService.error(e, { @@ -66,7 +73,7 @@ export const useImportWorkpad = () => { // read the uploaded file reader.readAsText(file); }, - [notifyService, createWorkpad] + [notifyService, importWorkpad] ); }; diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts index 85b195214d44b..21bcc89304b3c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts +++ b/x-pack/plugins/canvas/public/components/workpad_filters/hooks/use_canvas_filters.ts @@ -5,19 +5,22 @@ * 2.0. */ -import { fromExpression } from '@kbn/interpreter'; +import { ExpressionFunctionAST, fromExpression } from '@kbn/interpreter'; import { shallowEqual, useSelector } from 'react-redux'; import { State } from '../../../../types'; -import { getFiltersByGroups } from '../../../lib/filter'; +import { getFiltersByFilterExpressions } from '../../../lib/filter'; import { adaptCanvasFilter } from '../../../lib/filter_adapters'; -import { getGlobalFilters } from '../../../state/selectors/workpad'; +import { useFiltersService } from '../../../services'; -const extractExpressionAST = (filtersExpressions: string[]) => - fromExpression(filtersExpressions.join(' | ')); +const extractExpressionAST = (filters: string[]) => fromExpression(filters.join(' | ')); -export function useCanvasFilters(groups: string[] = [], ungrouped: boolean = false) { - const filterExpressions = useSelector((state: State) => getGlobalFilters(state), shallowEqual); - const filtersByGroups = getFiltersByGroups(filterExpressions, groups, ungrouped); +export function useCanvasFilters(filterExprsToGroupBy: ExpressionFunctionAST[] = []) { + const filtersService = useFiltersService(); + const filterExpressions = useSelector( + (state: State) => filtersService.getFilters(state), + shallowEqual + ); + const filtersByGroups = getFiltersByFilterExpressions(filterExpressions, filterExprsToGroupBy); const expression = extractExpressionAST(filtersByGroups); const filters = expression.chain.map(adaptCanvasFilter); diff --git a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx index 610e6e56af350..20ec56706480d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_filters/workpad_filters.tsx @@ -8,11 +8,7 @@ import React, { FC, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { State, FilterField, PositionedElement } from '../../../types'; -import { - extractGroupsFromElementsFilters, - groupFiltersBy, - extractUngroupedFromElementsFilters, -} from '../../lib/filter'; +import { groupFiltersBy, getFiltersExprsFromExpression } from '../../lib/filter'; import { setGroupFiltersByOption } from '../../state/actions/sidebar'; import { getGroupFiltersByOption } from '../../state/selectors/sidebar'; import { useCanvasFilters } from './hooks'; @@ -35,11 +31,8 @@ export const WorkpadFilters: FC = ({ element }) => { }, [dispatch] ); - - const groups = element ? extractGroupsFromElementsFilters(element.expression) : undefined; - const ungrouped = element ? extractUngroupedFromElementsFilters(element.expression) : false; - - const canvasFilters = useCanvasFilters(groups, ungrouped); + const filterExprs = element ? getFiltersExprsFromExpression(element.expression) : []; + const canvasFilters = useCanvasFilters(filterExprs); const filtersGroups = groupFiltersByField ? groupFiltersBy(canvasFilters, groupFiltersByField) diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx index 9d37873bcae0a..62d070dbf00f5 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/element_menu.stories.tsx @@ -17,7 +17,8 @@ const testElements: { [key: string]: ElementSpec } = { displayName: 'Area chart', help: 'A line chart with a filled body', type: 'chart', - expression: `filters + expression: `kibana + | selectFilter | demodata | pointseries x="time" y="mean(price)" | plot defaultStyle={seriesStyle lines=1 fill=1} @@ -47,7 +48,8 @@ const testElements: { [key: string]: ElementSpec } = { displayName: 'Debug filter', help: 'Shows the underlying global filters in a workpad', icon: 'bug', - expression: `filters + expression: `kibana + | selectFilter | render as=debug`, }, image: { @@ -64,7 +66,8 @@ const testElements: { [key: string]: ElementSpec } = { type: 'text', help: 'Add text using Markdown', icon: 'visText', - expression: `filters + expression: `kibana +| selectFilter | demodata | markdown "### Welcome to the Markdown element @@ -89,7 +92,8 @@ You can use standard Markdown in here, but you can also access your piped-in dat width: 200, height: 200, icon: 'visGoal', - expression: `filters + expression: `kibana + | selectFilter | demodata | math "mean(percent_uptime)" | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="Helvetica" color="#000000" align=center} @@ -111,7 +115,8 @@ You can use standard Markdown in here, but you can also access your piped-in dat displayName: 'Data table', type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', - expression: `filters + expression: `kibana + | selectFilter | demodata | table | render`, diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index 2634d76297b58..a168020b6eef8 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -7,15 +7,10 @@ import { fromExpression } from '@kbn/interpreter'; import { get } from 'lodash'; -import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; -import { interpretAst } from '../lib/run_interpreter'; -// @ts-expect-error untyped local -import { getState } from '../state/store'; -import { getGlobalFilters, getWorkpadVariablesAsObject } from '../state/selectors/workpad'; -import { ExpressionValueFilter } from '../../types'; -import { getFunctionHelp } from '../../i18n'; +import { pluginServices } from '../services'; +import type { FiltersFunction } from '../../common/functions'; +import { buildFiltersFunction } from '../../common/functions'; import { InitializeArguments } from '.'; -import { getFiltersByGroups } from '../lib/filter'; export interface Arguments { group: string[]; @@ -31,58 +26,34 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = // remove all allFilters that belong to a group return allFilters.filter((filter: string) => { const ast = fromExpression(filter); - const expGroups = get(ast, 'chain[0].arguments.filterGroup', []); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); return expGroups.length === 0; }); } - return getFiltersByGroups(allFilters, groups); + return allFilters.filter((filter: string) => { + const ast = fromExpression(filter); + const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); + return expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup)); + }); } -type FiltersFunction = ExpressionFunctionDefinition< - 'filters', - null, - Arguments, - ExpressionValueFilter ->; - export function filtersFunctionFactory(initialize: InitializeArguments): () => FiltersFunction { - return function filters(): FiltersFunction { - const { help, args: argHelp } = getFunctionHelp().filters; - - return { - name: 'filters', - type: 'filter', - help, - context: { - types: ['null'], - }, - args: { - group: { - aliases: ['_'], - types: ['string'], - help: argHelp.group, - multi: true, - }, - ungrouped: { - aliases: ['nogroup', 'nogroups'], - types: ['boolean'], - help: argHelp.ungrouped, - default: false, - }, - }, - fn: (input, { group, ungrouped }) => { - const filterList = getFiltersByGroup(getGlobalFilters(getState()), group, ungrouped); - - if (filterList && filterList.length) { - const filterExpression = filterList.join(' | '); - const filterAST = fromExpression(filterExpression); - return interpretAst(filterAST, getWorkpadVariablesAsObject(getState())); - } else { - const filterType = initialize.types.filter; - return filterType?.from(null, {}); - } - }, - }; + const fn: FiltersFunction['fn'] = (input, { group, ungrouped }) => { + const { expressions, filters: filtersService } = pluginServices.getServices(); + + const filterList = getFiltersByGroup(filtersService.getFilters(), group, ungrouped); + + if (filterList && filterList.length) { + const filterExpression = filterList.join(' | '); + const filterAST = fromExpression(filterExpression); + const { variables } = filtersService.getFiltersContext(); + return expressions.interpretAst(filterAST, variables); + } else { + const filterType = initialize.types.filter; + return filterType?.from(null, {}); + } }; + + return buildFiltersFunction(fn); } diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts index 3734b1bf53051..3536bed0f92b3 100644 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -10,10 +10,9 @@ import { ExpressionRendererEvent, IInterpreterRenderHandlers, } from 'src/plugins/expressions/public'; -// @ts-expect-error untyped local -import { setFilter } from '../state/actions/elements'; import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; import { RendererHandlers, CanvasElement } from '../../types'; +import { pluginServices } from '../services'; import { clearValue } from '../state/actions/resolved_args'; // This class creates stub handlers to ensure every element and renderer fulfills the contract. @@ -58,7 +57,6 @@ export const createHandlers = (baseHandlers = createBaseHandlers()): RendererHan }, resize(_size: { height: number; width: number }) {}, - setFilter() {}, }); export const assignHandlers = (handlers: Partial = {}): RendererHandlers => @@ -79,6 +77,8 @@ export const createDispatchedHandlerFactory = ( oldElement = element; } + const { filters } = pluginServices.getServices(); + const handlers: RendererHandlers & { event: IInterpreterRenderHandlers['event']; done: IInterpreterRenderHandlers['done']; @@ -89,8 +89,8 @@ export const createDispatchedHandlerFactory = ( case 'embeddableInputChange': this.onEmbeddableInputChange(event.data); break; - case 'setFilter': - this.setFilter(event.data); + case 'applyFilterAction': + filters.updateFilter(element.id, event.data); break; case 'onComplete': this.onComplete(event.data); @@ -106,10 +106,6 @@ export const createDispatchedHandlerFactory = ( break; } }, - setFilter(text: string) { - dispatch(setFilter(text, element.id, true)); - }, - getFilter() { return element.filter || ''; }, diff --git a/x-pack/plugins/canvas/public/lib/filter.test.ts b/x-pack/plugins/canvas/public/lib/filter.test.ts index bf19bd6ecf4b8..9aef71f33f609 100644 --- a/x-pack/plugins/canvas/public/lib/filter.test.ts +++ b/x-pack/plugins/canvas/public/lib/filter.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { fromExpression } from '@kbn/interpreter'; import { FC } from 'react'; import { Filter as FilterType, @@ -18,9 +19,8 @@ import { flattenFilterView, createFilledFilterView, groupFiltersBy, - getFiltersByGroups, - extractGroupsFromElementsFilters, - extractUngroupedFromElementsFilters, + getFiltersExprsFromExpression, + getFiltersByFilterExpressions, isExpressionWithFilters, } from './filter'; @@ -285,7 +285,7 @@ describe('groupFiltersBy', () => { }); }); -describe('getFiltersByGroups', () => { +describe('getFiltersByFilterExpressions', () => { const group1 = 'Group 1'; const group2 = 'Group 2'; @@ -296,66 +296,106 @@ describe('getFiltersByGroups', () => { `exactly value="kibana" column="project2" filterGroup="${group2}"`, ]; - it('returns all filters related to a specified groups', () => { - expect(getFiltersByGroups(filters, [group1, group2])).toEqual([ - filters[0], - filters[1], - filters[3], - ]); + const filtersExprWithGroup = `filters group="${group2}"`; + + const kibanaExpr = 'kibana'; + const selectFilterExprEmpty = 'selectFilter'; + const selectFilterExprWithGroup = `${selectFilterExprEmpty} group="${group2}"`; + const selectFilterExprWithGroups = `${selectFilterExprEmpty} group="${group2}" group="${group1}"`; + const selectFilterExprWithUngrouped = `${selectFilterExprEmpty} ungrouped=true`; + const selectFilterExprWithGroupAndUngrouped = `${selectFilterExprEmpty} group="${group2}" ungrouped=true`; + + const removeFilterExprEmpty = 'removeFilter'; + const removeFilterExprWithGroup = `${removeFilterExprEmpty} group="${group2}"`; + const removeFilterExprWithUngrouped = `${removeFilterExprEmpty} ungrouped=true`; + const removeFilterExprWithGroupAndUngrouped = `${removeFilterExprEmpty} group="${group2}" ungrouped=true`; + + const getFiltersAsts = (filtersExprs: string[]) => { + const ast = fromExpression(filtersExprs.join(' | ')); + return ast.chain; + }; - expect(getFiltersByGroups(filters, [group2])).toEqual([filters[1], filters[3]]); + it('returns all filters if no arguments specified to selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprEmpty]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual(filters); }); - it('returns filters without group if ungrouped is true', () => { - expect(getFiltersByGroups(filters, [], true)).toEqual([filters[2]]); + it('returns filters with group, specified to selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithGroups]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[1], filters[3]]); }); - it('returns filters with group if ungrouped is true and groups are not empty', () => { - expect(getFiltersByGroups(filters, [group1], true)).toEqual([filters[0]]); + it('returns filters without group if ungrouped is true at selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[2]]); }); - it('returns empty array if not found any filter with a specified group', () => { - expect(getFiltersByGroups(filters, ['absent group'])).toEqual([]); + it('returns filters with group if ungrouped is true and groups are not empty at selectFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, selectFilterExprWithGroupAndUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[1], filters[2], filters[3]]); }); - it('returns empty array if not groups specified', () => { - expect(getFiltersByGroups(filters, [])).toEqual(filters); + it('returns no filters if no arguments, specified to removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprEmpty]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([]); + }); + + it('returns filters without group, specified to removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithGroup]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[2]]); }); -}); -describe('extractGroupsFromElementsFilters', () => { - const exprFilters = 'filters'; - const exprRest = 'demodata | plot | render'; - - it('returns groups which are specified at filters expression', () => { - const groups = ['group 1', 'group 2', 'group 3', 'group 4']; - const additionalGroups = [...groups, 'group 5']; - const groupsExpr = groups.map((group) => `group="${group}"`).join(' '); - const additionalGroupsExpr = additionalGroups.map((group) => `group="${group}"`).join(' '); - - expect( - extractGroupsFromElementsFilters( - `${exprFilters} ${groupsExpr} | ${exprFilters} ${additionalGroupsExpr} | ${exprRest}` - ) - ).toEqual(additionalGroups); + it('returns filters without group if ungrouped is true at removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0], filters[1], filters[3]]); }); - it('returns empty array if no groups were specified at filters expression', () => { - expect(extractGroupsFromElementsFilters(`${exprFilters} | ${exprRest}`)).toEqual([]); + it('remove filters without group and with specified group if ungrouped is true and groups are not empty at removeFilter expression', () => { + const filtersExprs = getFiltersAsts([kibanaExpr, removeFilterExprWithGroupAndUngrouped]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[0]]); + }); + + it('should include/exclude filters iteratively', () => { + const filtersExprs = getFiltersAsts([ + kibanaExpr, + selectFilterExprWithGroup, + removeFilterExprWithGroup, + selectFilterExprEmpty, + ]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([]); + }); + + it('should include/exclude filters from global filters if `filters` expression is specified', () => { + const filtersExprs = getFiltersAsts([ + kibanaExpr, + selectFilterExprWithGroup, + removeFilterExprWithGroup, + selectFilterExprEmpty, + filtersExprWithGroup, + ]); + const matchedFilters = getFiltersByFilterExpressions(filters, filtersExprs); + expect(matchedFilters).toEqual([filters[1], filters[3]]); }); }); -describe('extractUngroupedFromElementsFilters', () => { - it('checks if ungrouped filters expression exist at the element', () => { - const expression = - 'filters group="10" group="11" | filters group="15" ungrouped=true | demodata | plot | render'; - const isUngrouped = extractUngroupedFromElementsFilters(expression); - expect(isUngrouped).toBeTruthy(); +describe('getFiltersExprsFromExpression', () => { + it('returns list of filters expressions asts', () => { + const filter1 = 'selectFilter'; + const filter2 = 'filters group="15" ungrouped=true'; + const filter3 = 'removeFilter'; + const expression = `kibana | ${filter1} | ${filter2} | ${filter3} | demodata | plot | render`; + const filtersAsts = getFiltersExprsFromExpression(expression); - const nextExpression = - 'filters group="10" group="11" | filters group="15" | demodata | plot | render'; - const nextIsUngrouped = extractUngroupedFromElementsFilters(nextExpression); - expect(nextIsUngrouped).toBeFalsy(); + expect(filtersAsts).toEqual([filter1, filter2, filter3].map((f) => fromExpression(f).chain[0])); }); }); diff --git a/x-pack/plugins/canvas/public/lib/filter.ts b/x-pack/plugins/canvas/public/lib/filter.ts index 6e9db1757ccc7..2554ae11220eb 100644 --- a/x-pack/plugins/canvas/public/lib/filter.ts +++ b/x-pack/plugins/canvas/public/lib/filter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { fromExpression } from '@kbn/interpreter'; +import { Ast, ExpressionFunctionAST, fromExpression, toExpression } from '@kbn/interpreter'; import { flowRight, get, groupBy } from 'lodash'; import { Filter as FilterType, @@ -14,6 +14,14 @@ import { FlattenFilterViewInstance, } from '../../types/filters'; +const SELECT_FILTER = 'selectFilter'; +const FILTERS = 'filters'; +const REMOVE_FILTER = 'removeFilter'; + +const includeFiltersExpressions = [FILTERS, SELECT_FILTER]; +const excludeFiltersExpressions = [REMOVE_FILTER]; +const filtersExpressions = [...includeFiltersExpressions, ...excludeFiltersExpressions]; + export const defaultFormatter = (value: unknown) => (value || null ? `${value}` : '-'); export const formatFilterView = @@ -55,41 +63,73 @@ export const groupFiltersBy = (filters: FilterType[], groupByField: FilterField) })); }; -export const getFiltersByGroups = ( - filters: string[], - groups: string[], - ungrouped: boolean = false -) => - filters.filter((filter: string) => { - const ast = fromExpression(filter); - const expGroups: string[] = get(ast, 'chain[0].arguments.filterGroup', []); - if (!groups?.length && ungrouped) { - return expGroups.length === 0; - } +const excludeFiltersByGroups = (filters: Ast[], filterExprAst: ExpressionFunctionAST) => { + const groupsToExclude = filterExprAst.arguments.group ?? []; + const removeUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; + return filters.filter((filter) => { + const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + (group: string) => group !== '' + ); + const noNeedToExcludeByGroup = !( + groups.length && + groupsToExclude.length && + groupsToExclude.includes(groups[0]) + ); + + const noNeedToExcludeByUngrouped = (removeUngrouped && groups.length) || !removeUngrouped; + const excludeAllFilters = !groupsToExclude.length && !removeUngrouped; - return ( - !groups.length || - (expGroups.length > 0 && expGroups.every((expGroup) => groups.includes(expGroup))) + return !excludeAllFilters && noNeedToExcludeByUngrouped && noNeedToExcludeByGroup; + }); +}; + +const includeFiltersByGroups = ( + filters: Ast[], + filterExprAst: ExpressionFunctionAST, + ignoreUngroupedIfGroups: boolean = false +) => { + const groupsToInclude = filterExprAst.arguments.group ?? []; + const includeOnlyUngrouped = filterExprAst.arguments.ungrouped?.[0] ?? false; + return filters.filter((filter) => { + const groups: string[] = get(filter, 'chain[0].arguments.filterGroup', []).filter( + (group: string) => group !== '' ); + const needToIncludeByGroup = + groups.length && groupsToInclude.length && groupsToInclude.includes(groups[0]); + + const needToIncludeByUngrouped = + includeOnlyUngrouped && + !groups.length && + (ignoreUngroupedIfGroups ? !groupsToInclude.length : true); + + const allowAll = !groupsToInclude.length && !includeOnlyUngrouped; + return needToIncludeByUngrouped || needToIncludeByGroup || allowAll; }); +}; -export const extractGroupsFromElementsFilters = (expr: string) => { - const ast = fromExpression(expr); - const filtersFns = ast.chain.filter((expression) => expression.function === 'filters'); - const groups = filtersFns.reduce((foundGroups, filterFn) => { - const filterGroups = filterFn?.arguments.group?.map((g) => g.toString()) ?? []; - return [...foundGroups, ...filterGroups]; - }, []); - return [...new Set(groups)]; +export const getFiltersByFilterExpressions = ( + filters: string[], + filterExprsAsts: ExpressionFunctionAST[] +) => { + const filtersAst = filters.map((filter) => fromExpression(filter)); + const matchedFiltersAst = filterExprsAsts.reduce((includedFilters, filter) => { + if (excludeFiltersExpressions.includes(filter.function)) { + return excludeFiltersByGroups(includedFilters, filter); + } + const isFiltersExpr = filter.function === FILTERS; + const filtersToInclude = isFiltersExpr ? filtersAst : includedFilters; + return includeFiltersByGroups(filtersToInclude, filter, isFiltersExpr); + }, filtersAst); + + return matchedFiltersAst.map((ast) => toExpression(ast)); }; -export const extractUngroupedFromElementsFilters = (expr: string) => { +export const getFiltersExprsFromExpression = (expr: string) => { const ast = fromExpression(expr); - const filtersFns = ast.chain.filter((expression) => expression.function === 'filters'); - return filtersFns.some((filterFn) => filterFn?.arguments.ungrouped?.[0]); + return ast.chain.filter((expression) => filtersExpressions.includes(expression.function)); }; export const isExpressionWithFilters = (expr: string) => { const ast = fromExpression(expr); - return ast.chain.some((expression) => expression.function === 'filters'); + return ast.chain.some((expression) => filtersExpressions.includes(expression.function)); }; diff --git a/x-pack/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/plugins/canvas/public/lib/run_interpreter.ts deleted file mode 100644 index 77c31b11924c0..0000000000000 --- a/x-pack/plugins/canvas/public/lib/run_interpreter.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fromExpression, getType } from '@kbn/interpreter'; -import { pluck } from 'rxjs/operators'; -import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -import { pluginServices } from '../services'; - -interface Options { - castToRender?: boolean; -} - -/** - * Meant to be a replacement for plugins/interpreter/interpretAST - */ -export async function interpretAst( - ast: ExpressionAstExpression, - variables: Record, - input: ExpressionValue = null -): Promise { - const context = { variables }; - const { execute } = pluginServices.getServices().expressions; - - return await execute(ast, input, context).getData().pipe(pluck('result')).toPromise(); -} - -/** - * Runs interpreter, usually in the browser - * - * @param {object} ast - Executable AST - * @param {any} input - Initial input for AST execution - * @param {object} variables - Variables to pass in to the intrepreter context - * @param {object} options - * @param {boolean} options.castToRender - try to cast to a type: render object? - * @returns {promise} - */ -export async function runInterpreter( - ast: ExpressionAstExpression, - input: ExpressionValue, - variables: Record, - options: Options = {} -): Promise { - const context = { variables }; - try { - const { execute } = pluginServices.getServices().expressions; - - const renderable = await execute(ast, input, context) - .getData() - .pipe(pluck('result')) - .toPromise(); - - if (getType(renderable) === 'render') { - return renderable; - } - - if (options.castToRender) { - return runInterpreter(fromExpression('render'), renderable, variables, { - castToRender: false, - }); - } - - throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); - } catch (err) { - const { error: displayError } = pluginServices.getServices().notify; - displayError(err); - throw err; - } -} diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 8cdc695ebaaba..1c2ce763f42e2 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -36,7 +36,6 @@ import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getPluginApi, CanvasApi } from './plugin_api'; import { setupExpressions } from './setup_expressions'; -import { pluginServiceRegistry } from './services/kibana'; export type { CoreStart, CoreSetup }; @@ -123,6 +122,8 @@ export class CanvasPlugin srcPlugin.start(coreStart, startPlugins); const { pluginServices } = await import('./services'); + const { pluginServiceRegistry } = await import('./services/kibana'); + pluginServices.setRegistry( pluginServiceRegistry.start({ coreStart, diff --git a/x-pack/plugins/canvas/public/services/expressions.ts b/x-pack/plugins/canvas/public/services/expressions.ts index 01bb0adb17711..456a1314bdfff 100644 --- a/x-pack/plugins/canvas/public/services/expressions.ts +++ b/x-pack/plugins/canvas/public/services/expressions.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { ExpressionsServiceStart } from '../../../../../src/plugins/expressions/public'; - -export type CanvasExpressionsService = ExpressionsServiceStart; +export type { CanvasExpressionsService } from './kibana/expressions'; diff --git a/x-pack/plugins/canvas/public/services/filters.ts b/x-pack/plugins/canvas/public/services/filters.ts new file mode 100644 index 0000000000000..1ced3d15f6e10 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/filters.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { CanvasFiltersService } from './kibana/filters'; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index ed55f919e4c76..4bf025c274859 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -12,6 +12,7 @@ import { PluginServices } from '../../../../../src/plugins/presentation_util/pub import { CanvasCustomElementService } from './custom_element'; import { CanvasEmbeddablesService } from './embeddables'; import { CanvasExpressionsService } from './expressions'; +import { CanvasFiltersService } from './filters'; import { CanvasLabsService } from './labs'; import { CanvasNavLinkService } from './nav_link'; import { CanvasNotifyService } from './notify'; @@ -24,6 +25,7 @@ export interface CanvasPluginServices { customElement: CanvasCustomElementService; embeddables: CanvasEmbeddablesService; expressions: CanvasExpressionsService; + filters: CanvasFiltersService; labs: CanvasLabsService; navLink: CanvasNavLinkService; notify: CanvasNotifyService; @@ -41,6 +43,7 @@ export const useEmbeddablesService = () => (() => pluginServices.getHooks().embeddables.useService())(); export const useExpressionsService = () => (() => pluginServices.getHooks().expressions.useService())(); +export const useFiltersService = () => (() => pluginServices.getHooks().filters.useService())(); export const useLabsService = () => (() => pluginServices.getHooks().labs.useService())(); export const useNavLinkService = () => (() => pluginServices.getHooks().navLink.useService())(); export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/expressions.ts b/x-pack/plugins/canvas/public/services/kibana/expressions.ts index 780de5309d97e..ea329b63863f8 100644 --- a/x-pack/plugins/canvas/public/services/kibana/expressions.ts +++ b/x-pack/plugins/canvas/public/services/kibana/expressions.ts @@ -4,16 +4,137 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { fromExpression, getType } from '@kbn/interpreter'; +import { + ExpressionAstExpression, + ExpressionExecutionParams, + ExpressionValue, +} from 'src/plugins/expressions'; +import { pluck } from 'rxjs/operators'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionsServiceStart } from '../../../../../../src/plugins/expressions/public'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; - import { CanvasStartDeps } from '../../plugin'; -import { CanvasExpressionsService } from '../expressions'; +import { CanvasFiltersService } from './filters'; +import { CanvasNotifyService } from '../notify'; + +interface Options { + castToRender?: boolean; +} + +export class ExpressionsService { + private filters: CanvasFiltersService; + private notify: CanvasNotifyService; + + constructor( + private readonly expressions: ExpressionsServiceStart, + { filters, notify }: CanvasExpressionsServiceRequiredServices + ) { + this.filters = filters; + this.notify = notify; + } + + async interpretAst( + ast: ExpressionAstExpression, + variables: Record, + input: ExpressionValue = null + ) { + const context = await this.getGlobalContext(); + return await this.interpretAstWithContext(ast, input, { + ...(context ?? {}), + variables, + }); + } + + async interpretAstWithContext( + ast: ExpressionAstExpression, + input: ExpressionValue = null, + context?: ExpressionExecutionParams + ): Promise { + return await this.expressions + .execute(ast, input, context) + .getData() + .pipe(pluck('result')) + .toPromise(); + } + + /** + * Runs interpreter, usually in the browser + * + * @param {object} ast - Executable AST + * @param {any} input - Initial input for AST execution + * @param {object} variables - Variables to pass in to the intrepreter context + * @param {object} options + * @param {boolean} options.castToRender - try to cast to a type: render object? + * @returns {Promise} + */ + async runInterpreter( + ast: ExpressionAstExpression, + input: ExpressionValue, + variables: Record, + options: Options = {} + ): Promise { + const context = await this.getGlobalContext(); + const fullContext = { ...(context ?? {}), variables }; + + try { + const renderable = await this.interpretAstWithContext(ast, input, fullContext); + + if (getType(renderable) === 'render') { + return renderable; + } + + if (options.castToRender) { + return this.runInterpreter(fromExpression('render'), renderable, fullContext, { + castToRender: false, + }); + } + + throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); + } catch (err) { + this.notify.error(err); + throw err; + } + } + + getRenderer(name: string) { + return this.expressions.getRenderer(name); + } + + getFunctions() { + return this.expressions.getFunctions(); + } + + private async getFilters() { + const filtersList = this.filters.getFilters(); + const context = this.filters.getFiltersContext(); + const filterExpression = filtersList.join(' | '); + const filterAST = fromExpression(filterExpression); + return await this.interpretAstWithContext(filterAST, null, context); + } + + private async getGlobalContext() { + const canvasFilters = await this.getFilters(); + const kibanaFilters = buildEmbeddableFilters(canvasFilters ? canvasFilters.and : []); + return { + searchContext: { ...kibanaFilters }, + }; + } +} + +export type CanvasExpressionsService = ExpressionsService; +export interface CanvasExpressionsServiceRequiredServices { + notify: CanvasNotifyService; + filters: CanvasFiltersService; +} export type CanvasExpressionsServiceFactory = KibanaPluginServiceFactory< CanvasExpressionsService, - CanvasStartDeps + CanvasStartDeps, + CanvasExpressionsServiceRequiredServices >; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ({ startPlugins }) => - startPlugins.expressions; +export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( + { startPlugins }, + requiredServices +) => new ExpressionsService(startPlugins.expressions, requiredServices); diff --git a/x-pack/plugins/canvas/public/services/kibana/filters.ts b/x-pack/plugins/canvas/public/services/kibana/filters.ts new file mode 100644 index 0000000000000..872b6759b389b --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/filters.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +// @ts-expect-error untyped local +import { getState, getStore } from '../../state/store'; +import { State } from '../../../types'; +import { getGlobalFilters, getWorkpadVariablesAsObject } from '../../state/selectors/workpad'; +import { CanvasStartDeps } from '../../plugin'; +// @ts-expect-error untyped local +import { setFilter } from '../../state/actions/elements'; + +export class FiltersService { + constructor() {} + + getFilters(state: State = getState()) { + return getGlobalFilters(state); + } + + updateFilter(filterId: string, filterExpression: string) { + const { dispatch } = getStore(); + dispatch(setFilter(filterExpression, filterId, true)); + } + + getFiltersContext(state: State = getState()) { + const variables = getWorkpadVariablesAsObject(state); + return { variables }; + } +} + +export type CanvasFiltersService = FiltersService; + +export type CanvasFiltersServiceFactory = KibanaPluginServiceFactory< + CanvasFiltersService, + CanvasStartDeps +>; + +export const filtersServiceFactory: CanvasFiltersServiceFactory = () => new FiltersService(); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 91767947bc0a6..c1ceb531657d0 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -24,10 +24,12 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; +import { filtersServiceFactory } from './filters'; export { customElementServiceFactory } from './custom_element'; export { embeddablesServiceFactory } from './embeddables'; export { expressionsServiceFactory } from './expressions'; +export { filtersServiceFactory } from './filters'; export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; @@ -41,7 +43,8 @@ export const pluginServiceProviders: PluginServiceProviders< > = { customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory), + expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), + filters: new PluginServiceProvider(filtersServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts index 9f69d5096237c..c0ef1097555a6 100644 --- a/x-pack/plugins/canvas/public/services/kibana/workpad.ts +++ b/x-pack/plugins/canvas/public/services/kibana/workpad.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SavedObject } from 'kibana/public'; import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasStartDeps } from '../../plugin'; @@ -67,6 +68,21 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; }, + export: async (id: string) => { + const workpad = await coreStart.http.get>( + `${getApiPath()}/export/${id}` + ); + const { attributes } = workpad; + + return { + ...workpad, + attributes: { + ...attributes, + css: attributes.css ?? DEFAULT_WORKPAD_CSS, + variables: attributes.variables ?? [], + }, + }; + }, resolve: async (id: string) => { const { workpad, outcome, aliasId } = await coreStart.http.get( `${getApiPath()}/resolve/${id}` @@ -93,6 +109,14 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, }), }); }, + import: (workpad: CanvasWorkpad) => + coreStart.http.post(`${getApiPath()}/import`, { + body: JSON.stringify({ + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + variables: workpad.variables || [], + }), + }), createFromTemplate: (templateId: string) => { return coreStart.http.post(getApiPath(), { body: JSON.stringify({ templateId }), diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts index 6c77bdb1adeac..5dd40997900c6 100644 --- a/x-pack/plugins/canvas/public/services/storybook/workpad.ts +++ b/x-pack/plugins/canvas/public/services/storybook/workpad.ts @@ -31,7 +31,7 @@ type CanvasWorkpadServiceFactory = PluginServiceFactory () => new Promise((resolve) => setTimeout(resolve, time)); -const { findNoTemplates, findNoWorkpads, findSomeTemplates } = stubs; +const { findNoTemplates, findNoWorkpads, findSomeTemplates, importWorkpad } = stubs; const getRandomName = () => { const lorem = @@ -85,6 +85,10 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ action('workpadService.findTemplates')(); return (hasTemplates ? findSomeTemplates() : findNoTemplates())(); }, + import: (workpad) => { + action('workpadService.import')(workpad); + return importWorkpad(workpad); + }, create: (workpad) => { action('workpadService.create')(workpad); return Promise.resolve(workpad); diff --git a/x-pack/plugins/canvas/public/services/stubs/expressions.ts b/x-pack/plugins/canvas/public/services/stubs/expressions.ts index 6660c1c6efb35..405f2ebe0ba91 100644 --- a/x-pack/plugins/canvas/public/services/stubs/expressions.ts +++ b/x-pack/plugins/canvas/public/services/stubs/expressions.ts @@ -10,11 +10,22 @@ import { plugin } from '../../../../../../src/plugins/expressions/public'; import { functions as functionDefinitions } from '../../../canvas_plugin_src/functions/common'; import { renderFunctions } from '../../../canvas_plugin_src/renderers/core'; import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; -import { CanvasExpressionsService } from '../expressions'; +import { + CanvasExpressionsService, + CanvasExpressionsServiceRequiredServices, + ExpressionsService, +} from '../kibana/expressions'; -type CanvasExpressionsServiceFactory = PluginServiceFactory; +type CanvasExpressionsServiceFactory = PluginServiceFactory< + CanvasExpressionsService, + {}, + CanvasExpressionsServiceRequiredServices +>; -export const expressionsServiceFactory: CanvasExpressionsServiceFactory = () => { +export const expressionsServiceFactory: CanvasExpressionsServiceFactory = ( + params, + requiredServices +) => { const placeholder = {} as any; const expressionsPlugin = plugin(placeholder); const setup = expressionsPlugin.setup(placeholder); @@ -25,5 +36,5 @@ export const expressionsServiceFactory: CanvasExpressionsServiceFactory = () => expressionsService.registerRenderer(fn as unknown as AnyExpressionRenderDefinition); }); - return expressionsService; + return new ExpressionsService(expressionsService, requiredServices); }; diff --git a/x-pack/plugins/canvas/public/services/stubs/filters.ts b/x-pack/plugins/canvas/public/services/stubs/filters.ts new file mode 100644 index 0000000000000..972dbfd6dc0e4 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/filters.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasFiltersService } from '../filters'; + +export type CanvasFiltersServiceFactory = PluginServiceFactory; + +const noop = (..._args: any[]): any => {}; + +export const filtersServiceFactory: CanvasFiltersServiceFactory = () => ({ + getFilters: () => [ + 'exactly value="machine-learning" column="project1" filterGroup="Group 1"', + 'exactly value="kibana" column="project2" filterGroup="Group 1"', + 'time column="@timestamp1" from="2021-11-02 17:13:18" to="2021-11-09 17:13:18" filterGroup="Some group"', + ], + updateFilter: noop, + getFiltersContext: () => ({ variables: {} }), +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 2216013a29c12..d90b1a3c92201 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -24,9 +24,11 @@ import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; +import { filtersServiceFactory } from './filters'; export { customElementServiceFactory } from './custom_element'; export { expressionsServiceFactory } from './expressions'; +export { filtersServiceFactory } from './filters'; export { labsServiceFactory } from './labs'; export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; @@ -38,7 +40,8 @@ export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { customElement: new PluginServiceProvider(customElementServiceFactory), embeddables: new PluginServiceProvider(embeddablesServiceFactory), - expressions: new PluginServiceProvider(expressionsServiceFactory), + expressions: new PluginServiceProvider(expressionsServiceFactory, ['filters', 'notify']), + filters: new PluginServiceProvider(filtersServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), navLink: new PluginServiceProvider(navLinkServiceFactory), notify: new PluginServiceProvider(notifyServiceFactory), diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index c10244038750d..6268fa128df0f 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -6,13 +6,12 @@ */ import moment from 'moment'; - import { PluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; // @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; +import { getDefaultWorkpad, getExportedWorkpad } from '../../state/defaults'; import { CanvasWorkpadService } from '../workpad'; -import { CanvasTemplate } from '../../../types'; +import { CanvasTemplate, CanvasWorkpad } from '../../../types'; type CanvasWorkpadServiceFactory = PluginServiceFactory; @@ -94,6 +93,7 @@ export const findNoTemplates = .then(() => getNoTemplates()); }; +export const importWorkpad = (workpad: CanvasWorkpad) => Promise.resolve(workpad); export const getNoTemplates = () => ({ templates: [] }); export const getSomeTemplates = () => ({ templates }); @@ -103,6 +103,7 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({ Promise.resolve({ outcome: 'exactMatch', workpad: { ...getDefaultWorkpad(), id } }), findTemplates: findNoTemplates(), create: (workpad) => Promise.resolve(workpad), + import: (workpad) => importWorkpad(workpad), createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()), find: findNoWorkpads(), remove: (_id: string) => Promise.resolve(), diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index 8e77ab3f321ef..233b1a70ff7f6 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -25,10 +25,12 @@ export interface ResolveWorkpadResponse { outcome: SavedObjectsResolveResponse['outcome']; aliasId?: SavedObjectsResolveResponse['alias_target_id']; } + export interface CanvasWorkpadService { get: (id: string) => Promise; resolve: (id: string) => Promise; create: (workpad: CanvasWorkpad) => Promise; + import: (workpad: CanvasWorkpad) => Promise; createFromTemplate: (templateId: string) => Promise; find: (term: string) => Promise; remove: (id: string) => Promise; diff --git a/x-pack/plugins/canvas/public/state/actions/elements.js b/x-pack/plugins/canvas/public/state/actions/elements.js index bcc02c3cbc2cd..72186abd38c94 100644 --- a/x-pack/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/plugins/canvas/public/state/actions/elements.js @@ -20,7 +20,6 @@ import { import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; -import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { subMultitree } from '../../lib/aeroelastic/functional'; import { pluginServices } from '../../services'; import { selectToplevelNodes } from './transient'; @@ -101,11 +100,16 @@ export const fetchContext = createThunk( }); const variables = getWorkpadVariablesAsObject(getState()); + + const { expressions } = pluginServices.getServices(); const elementWithNewAst = set(element, pathToTarget, astChain); + // get context data from a partial AST - return interpretAst(elementWithNewAst.ast, variables, prevContextValue).then((value) => { - dispatch(args.setValue({ path: contextPath, value })); - }); + return expressions + .interpretAst(elementWithNewAst.ast, variables, prevContextValue) + .then((value) => { + dispatch(args.setValue({ path: contextPath, value })); + }); } ); @@ -124,14 +128,14 @@ const fetchRenderableWithContextFn = ({ dispatch, getState }, element, ast, cont }); const variables = getWorkpadVariablesAsObject(getState()); - - return runInterpreter(ast, context, variables, { castToRender: true }) + const { expressions, notify } = pluginServices.getServices(); + return expressions + .runInterpreter(ast, context, variables, { castToRender: true }) .then((renderable) => { dispatch(getAction(renderable)); }) .catch((err) => { - const notifyService = pluginServices.getServices().notify; - notifyService.error(err); + notify.error(err); dispatch(getAction(err)); }); }; @@ -171,12 +175,13 @@ export const fetchAllRenderables = createThunk( const argumentPath = [element.id, 'expressionRenderable']; const variables = getWorkpadVariablesAsObject(getState()); + const { expressions, notify } = pluginServices.getServices(); - return runInterpreter(ast, null, variables, { castToRender: true }) + return expressions + .runInterpreter(ast, null, variables, { castToRender: true }) .then((renderable) => ({ path: argumentPath, value: renderable })) .catch((err) => { - const notifyService = pluginServices.getServices().notify; - notifyService.error(err); + notify.error(err); return { path: argumentPath, value: err }; }); }); diff --git a/x-pack/plugins/canvas/public/state/defaults.js b/x-pack/plugins/canvas/public/state/defaults.js index 40e8425c98ff0..a4a38d50388d5 100644 --- a/x-pack/plugins/canvas/public/state/defaults.js +++ b/x-pack/plugins/canvas/public/state/defaults.js @@ -87,6 +87,14 @@ export const getDefaultWorkpad = () => { }; }; +export const getExportedWorkpad = () => { + const workpad = getDefaultWorkpad(); + return { + id: workpad.id, + attributes: workpad, + }; +}; + export const getDefaultSidebar = () => ({ groupFiltersByOption: DEFAULT_GROUP_BY_FIELD, }); diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index ac94ccc562e88..557a6b8acc4e7 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -27,6 +27,7 @@ import { ExpressionAstFunction, ExpressionAstExpression, } from '../../../types'; +import { isExpressionWithFilters } from '../../lib/filter'; type Modify = Pick> & R; type WorkpadInfo = Modify; @@ -248,7 +249,7 @@ function extractFilterGroups( // TODO: we always get a function here, right? const { function: fn, arguments: args } = item; - if (fn === 'filters') { + if (isExpressionWithFilters(fn)) { // we have a filter function, extract groups from args return groups.concat( buildGroupValues(args, (argValue) => { diff --git a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts index 216cdc0970dc4..13e4e34b20b66 100644 --- a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts @@ -12,6 +12,7 @@ export interface MockWorkpadRouteContext extends CanvasRouteHandlerContext { workpad: { create: jest.Mock; get: jest.Mock; + import: jest.Mock; update: jest.Mock; resolve: jest.Mock; }; @@ -23,6 +24,7 @@ export const workpadRouteContextMock = { workpad: { create: jest.fn(), get: jest.fn(), + import: jest.fn(), update: jest.fn(), resolve: jest.fn(), }, diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index ebe43ba76a46a..27b6186216b69 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -23,7 +23,8 @@ import { initRoutes } from './routes'; import { registerCanvasUsageCollector } from './collectors'; import { loadSampleData } from './sample_data'; import { setupInterpreter } from './setup_interpreter'; -import { customElementType, workpadType, workpadTemplateType } from './saved_objects'; +import { customElementType, workpadTypeFactory, workpadTemplateType } from './saved_objects'; +import type { CanvasSavedObjectTypeMigrationsDeps } from './saved_objects/migrations'; import { initializeTemplates } from './templates'; import { essqlSearchStrategyProvider } from './lib/essql_strategy'; import { getUISettings } from './ui_settings'; @@ -53,10 +54,18 @@ export class CanvasPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { const expressionsFork = plugins.expressions.fork(); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); + + const deps: CanvasSavedObjectTypeMigrationsDeps = { expressions: expressionsFork }; coreSetup.uiSettings.register(getUISettings()); - coreSetup.savedObjects.registerType(customElementType); - coreSetup.savedObjects.registerType(workpadType); - coreSetup.savedObjects.registerType(workpadTemplateType); + coreSetup.savedObjects.registerType(customElementType(deps)); + coreSetup.savedObjects.registerType(workpadTypeFactory(deps)); + coreSetup.savedObjects.registerType(workpadTemplateType(deps)); plugins.features.registerKibanaFeature(getCanvasFeature(plugins)); @@ -84,13 +93,6 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork, { - embeddablePersistableStateService: { - extract: plugins.embeddable.extract, - inject: plugins.embeddable.inject, - }, - }); - coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); plugins.data.search.registerSearchStrategy(ESSQL_SEARCH_STRATEGY, strategy); diff --git a/x-pack/plugins/canvas/server/routes/workpad/import.ts b/x-pack/plugins/canvas/server/routes/workpad/import.ts new file mode 100644 index 0000000000000..35d362f43becc --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/import.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_WORKPAD_IMPORT } from '../../../common/lib/constants'; +import { ImportedCanvasWorkpad } from '../../../types'; +import { ImportedWorkpadSchema } from './workpad_schema'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +const createRequestBodySchema = ImportedWorkpadSchema; + +export function initializeImportWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_WORKPAD_IMPORT}`, + validate: { + body: createRequestBodySchema, + }, + options: { + body: { + maxBytes: 26214400, + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const workpad = request.body as ImportedCanvasWorkpad; + + const createdObject = await context.canvas.workpad.import(workpad); + + return response.ok({ + body: { ...okResponse, id: createdObject.id }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts index 8483642e59c5a..b97d58ee232f1 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/index.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -9,6 +9,7 @@ import { RouteInitializerDeps } from '../'; import { initializeFindWorkpadsRoute } from './find'; import { initializeGetWorkpadRoute } from './get'; import { initializeCreateWorkpadRoute } from './create'; +import { initializeImportWorkpadRoute } from './import'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; import { initializeDeleteWorkpadRoute } from './delete'; import { initializeResolveWorkpadRoute } from './resolve'; @@ -18,6 +19,7 @@ export function initWorkpadRoutes(deps: RouteInitializerDeps) { initializeResolveWorkpadRoute(deps); initializeGetWorkpadRoute(deps); initializeCreateWorkpadRoute(deps); + initializeImportWorkpadRoute(deps); initializeUpdateWorkpadRoute(deps); initializeUpdateWorkpadAssetsRoute(deps); initializeDeleteWorkpadRoute(deps); diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts index 9bde26298185b..473b46d470265 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_schema.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; +import { TypeOf, schema } from '@kbn/config-schema'; export const PositionSchema = schema.object({ angle: schema.number(), @@ -18,30 +18,30 @@ export const PositionSchema = schema.object({ export const WorkpadElementSchema = schema.object({ expression: schema.string(), - filter: schema.maybe(schema.nullable(schema.string())), + filter: schema.nullable(schema.string({ defaultValue: '' })), id: schema.string(), position: PositionSchema, }); export const WorkpadPageSchema = schema.object({ elements: schema.arrayOf(WorkpadElementSchema), - groups: schema.maybe( - schema.arrayOf( - schema.object({ - id: schema.string(), - position: PositionSchema, - }) - ) + groups: schema.arrayOf( + schema.object({ + id: schema.string(), + position: PositionSchema, + }), + { defaultValue: [] } ), id: schema.string(), style: schema.recordOf(schema.string(), schema.string()), - transition: schema.maybe( - schema.oneOf([ - schema.object({}), + transition: schema.oneOf( + [ + schema.object({}, { defaultValue: {} }), schema.object({ name: schema.string(), }), - ]) + ], + { defaultValue: {} } ), }); @@ -55,44 +55,71 @@ export const WorkpadAssetSchema = schema.object({ export const WorkpadVariable = schema.object({ name: schema.string(), value: schema.oneOf([schema.string(), schema.number(), schema.boolean()]), - type: schema.string(), + type: schema.string({ + validate: (type) => { + const validTypes = ['string', 'number', 'boolean']; + if (type && !validTypes.includes(type)) { + return `${type} is invalid type for a variable. Valid types: ${validTypes.join(', ')}.`; + } + }, + }), }); -export const WorkpadSchema = schema.object( - { - '@created': schema.maybe(schema.string()), - '@timestamp': schema.maybe(schema.string()), - assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), - colors: schema.arrayOf(schema.string()), - css: schema.string(), - variables: schema.arrayOf(WorkpadVariable), - height: schema.number(), - id: schema.string(), - isWriteable: schema.maybe(schema.boolean()), - name: schema.string(), - page: schema.number(), - pages: schema.arrayOf(WorkpadPageSchema), - width: schema.number(), - }, - { - validate: (workpad) => { - // Validate unique page ids - const pageIdsArray = workpad.pages.map((page) => page.id); - const pageIdsSet = new Set(pageIdsArray); +const commonWorkpadFields = { + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + colors: schema.arrayOf(schema.string()), + css: schema.string(), + variables: schema.arrayOf(WorkpadVariable), + height: schema.number(), + id: schema.maybe(schema.string()), + isWriteable: schema.maybe(schema.boolean()), + name: schema.string(), + page: schema.number(), + pages: schema.arrayOf(WorkpadPageSchema), + width: schema.number(), +}; - if (pageIdsArray.length !== pageIdsSet.size) { - return 'Page Ids are not unique'; - } +const WorkpadSchemaWithoutValidation = schema.object({ + assets: schema.maybe(schema.recordOf(schema.string(), WorkpadAssetSchema)), + ...commonWorkpadFields, +}); - // Validate unique element ids - const elementIdsArray = workpad.pages - .map((page) => page.elements.map((element) => element.id)) - .flat(); - const elementIdsSet = new Set(elementIdsArray); +const ImportedWorkpadSchemaWithoutValidation = schema.object({ + assets: schema.recordOf(schema.string(), WorkpadAssetSchema), + ...commonWorkpadFields, +}); - if (elementIdsArray.length !== elementIdsSet.size) { - return 'Element Ids are not unique'; - } - }, +const validate = (workpad: TypeOf) => { + // Validate unique page ids + const pageIdsArray = workpad.pages.map((page) => page.id); + const pageIdsSet = new Set(pageIdsArray); + + if (pageIdsArray.length !== pageIdsSet.size) { + return 'Page Ids are not unique'; + } + + // Validate unique element ids + const elementIdsArray = workpad.pages + .map((page) => page.elements.map((element) => element.id)) + .flat(); + const elementIdsSet = new Set(elementIdsArray); + + if (elementIdsArray.length !== elementIdsSet.size) { + return 'Element Ids are not unique'; + } +}; + +export const WorkpadSchema = WorkpadSchemaWithoutValidation.extends( + {}, + { + validate, + } +); + +export const ImportedWorkpadSchema = ImportedWorkpadSchemaWithoutValidation.extends( + {}, + { + validate, } ); diff --git a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts index d62642f5619ea..82305b2fdd95f 100644 --- a/x-pack/plugins/canvas/server/saved_objects/custom_element.ts +++ b/x-pack/plugins/canvas/server/saved_objects/custom_element.ts @@ -7,8 +7,9 @@ import { SavedObjectsType } from 'src/core/server'; import { CUSTOM_ELEMENT_TYPE } from '../../common/lib/constants'; +import { customElementMigrationsFactory, CanvasSavedObjectTypeMigrationsDeps } from './migrations'; -export const customElementType: SavedObjectsType = { +export const customElementType = (deps: CanvasSavedObjectTypeMigrationsDeps): SavedObjectsType => ({ name: CUSTOM_ELEMENT_TYPE, hidden: false, namespaceType: 'multiple-isolated', @@ -31,7 +32,7 @@ export const customElementType: SavedObjectsType = { '@created': { type: 'date' }, }, }, - migrations: {}, + migrations: customElementMigrationsFactory(deps), management: { icon: 'canvasApp', defaultSearchField: 'name', @@ -40,4 +41,4 @@ export const customElementType: SavedObjectsType = { return obj.attributes.displayName; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/index.ts b/x-pack/plugins/canvas/server/saved_objects/index.ts index dfc27c4b6fa66..9e7cd8644c7c7 100644 --- a/x-pack/plugins/canvas/server/saved_objects/index.ts +++ b/x-pack/plugins/canvas/server/saved_objects/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { workpadType } from './workpad'; +import { workpadTypeFactory } from './workpad'; import { customElementType } from './custom_element'; import { workpadTemplateType } from './workpad_template'; -export { customElementType, workpadType, workpadTemplateType }; +export { customElementType, workpadTypeFactory, workpadTemplateType }; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts new file mode 100644 index 0000000000000..20eba14c5cff0 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/expressions.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Ast, fromExpression, toExpression } from '@kbn/interpreter'; +import { Serializable } from '@kbn/utility-types'; +import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { flowRight, mapValues } from 'lodash'; +import { + CanvasElement, + CanvasTemplateElement, + CanvasTemplate, + CustomElement, + CustomElementContent, + CustomElementNode, +} from '../../../types'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; +import { WorkpadAttributes } from '../../routes/workpad/workpad_attributes'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; + +type ToSerializable = { + [K in keyof Type]: Type[K] extends unknown[] + ? ToSerializable + : Type[K] extends {} + ? ToSerializable + : Serializable; +}; + +type ExprAst = ToSerializable; + +interface CommonPage { + elements?: T[]; +} +interface CommonWorkpad, U> { + pages?: T[]; +} + +type MigrationFn = ( + migrate: MigrateFunction, + version: string +) => SavedObjectMigrationFn; + +const toAst = (expression: string): ExprAst => fromExpression(expression); +const fromAst = (ast: Ast): string => toExpression(ast); + +const migrateExpr = (expr: string, migrateFn: MigrateFunction) => + flowRight(fromAst, migrateFn, toAst)(expr); + +const migrateWorkpadElement = + (migrate: MigrateFunction) => + ({ filter, expression, ...element }: CanvasElement | CustomElementNode) => ({ + ...element, + filter: filter ? migrateExpr(filter, migrate) : filter, + expression: expression ? migrateExpr(expression, migrate) : expression, + }); + +const migrateTemplateElement = + (migrate: MigrateFunction) => + ({ expression, ...element }: CanvasTemplateElement) => ({ + ...element, + expression: expression ? migrateExpr(expression, migrate) : expression, + }); + +const migrateWorkpadElements = , U>( + doc: SavedObjectUnsanitizedDoc | undefined>, + migrateElementFn: any +) => { + if ( + typeof doc.attributes !== 'object' || + doc.attributes === null || + doc.attributes === undefined + ) { + return doc; + } + + const { pages } = doc.attributes; + + const newPages = pages?.map((page) => { + const { elements } = page; + const newElements = elements?.map(migrateElementFn); + return { ...page, elements: newElements }; + }); + + return { ...doc, attributes: { ...doc.attributes, pages: newPages } }; +}; + +const migrateTemplateWorkpadExpressions: MigrationFn = + (migrate) => (doc) => + migrateWorkpadElements(doc, migrateTemplateElement(migrate)); + +const migrateWorkpadExpressionsAndFilters: MigrationFn = (migrate) => (doc) => + migrateWorkpadElements(doc, migrateWorkpadElement(migrate)); + +const migrateCustomElementExpressionsAndFilters: MigrationFn = + (migrate) => (doc) => { + if ( + typeof doc.attributes !== 'object' || + doc.attributes === null || + doc.attributes === undefined + ) { + return doc; + } + + const { content } = doc.attributes; + const { selectedNodes = [] }: CustomElementContent = content + ? JSON.parse(content) + : { selectedNodes: [] }; + + const newSelectedNodes = selectedNodes.map((element) => { + const newElement = migrateWorkpadElement(migrate)(element); + return { ...element, ...newElement, ast: toAst(newElement.expression) }; + }); + + const newContent = JSON.stringify({ selectedNodes: newSelectedNodes }); + return { ...doc, attributes: { ...doc.attributes, content: newContent } }; + }; + +export const workpadExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateWorkpadExpressionsAndFilters + ) as MigrateFunctionsObject; + +export const templateWorkpadExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateTemplateWorkpadExpressions + ) as MigrateFunctionsObject; + +export const customElementExpressionsMigrationsFactory = ({ + expressions, +}: CanvasSavedObjectTypeMigrationsDeps) => + mapValues>( + expressions.getAllMigrations(), + migrateCustomElementExpressionsAndFilters + ) as MigrateFunctionsObject; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts new file mode 100644 index 0000000000000..88913b50c3c4c --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + customElementExpressionsMigrationsFactory, + templateWorkpadExpressionsMigrationsFactory, + workpadExpressionsMigrationsFactory, +} from './expressions'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; +import { workpadMigrationsFactory as workpadMigrationsFactoryFn } from './workpad'; +import { mergeMigrationFunctionMaps } from '../../../../../../src/plugins/kibana_utils/common'; + +export const workpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + mergeMigrationFunctionMaps( + workpadMigrationsFactoryFn(deps), + workpadExpressionsMigrationsFactory(deps) + ); + +export const templateWorkpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + templateWorkpadExpressionsMigrationsFactory(deps); + +export const customElementMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + customElementExpressionsMigrationsFactory(deps); + +export type { CanvasSavedObjectTypeMigrationsDeps } from './types'; diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts index c9c36fd7b26a9..f1dcbd5fe9e7c 100644 --- a/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/remove_attributes_id.ts @@ -7,7 +7,7 @@ import { SavedObjectMigrationFn } from 'src/core/server'; -export const removeAttributesId: SavedObjectMigrationFn = (doc) => { +export const removeAttributesId: SavedObjectMigrationFn = (doc) => { if (typeof doc.attributes === 'object' && doc.attributes !== null) { delete (doc.attributes as any).id; } diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts new file mode 100644 index 0000000000000..18ce0bd88cb69 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ExpressionsService } from 'src/plugins/expressions/public'; + +export interface CanvasSavedObjectTypeMigrationsDeps { + expressions: ExpressionsService; +} diff --git a/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts new file mode 100644 index 0000000000000..d4d7e2b429711 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/migrations/workpad.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common'; +import { removeAttributesId } from './remove_attributes_id'; +import { CanvasSavedObjectTypeMigrationsDeps } from './types'; + +export const workpadMigrationsFactory = (deps: CanvasSavedObjectTypeMigrationsDeps) => + ({ + '7.0.0': removeAttributesId, + } as unknown as MigrateFunctionsObject); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad.ts b/x-pack/plugins/canvas/server/saved_objects/workpad.ts index a8f0f3daf2175..db22025e625e8 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad.ts @@ -7,9 +7,12 @@ import { SavedObjectsType } from 'src/core/server'; import { CANVAS_TYPE } from '../../common/lib/constants'; -import { removeAttributesId } from './migrations/remove_attributes_id'; +import { workpadMigrationsFactory } from './migrations'; +import type { CanvasSavedObjectTypeMigrationsDeps } from './migrations'; -export const workpadType: SavedObjectsType = { +export const workpadTypeFactory = ( + deps: CanvasSavedObjectTypeMigrationsDeps +): SavedObjectsType => ({ name: CANVAS_TYPE, hidden: false, namespaceType: 'multiple-isolated', @@ -29,9 +32,7 @@ export const workpadType: SavedObjectsType = { '@created': { type: 'date' }, }, }, - migrations: { - '7.0.0': removeAttributesId, - }, + migrations: workpadMigrationsFactory(deps), management: { importableAndExportable: true, icon: 'canvasApp', @@ -46,4 +47,4 @@ export const workpadType: SavedObjectsType = { }; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts index eff7f45dcadae..a55c7348c62bb 100644 --- a/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_template.ts @@ -7,8 +7,14 @@ import { SavedObjectsType } from 'src/core/server'; import { TEMPLATE_TYPE } from '../../common/lib/constants'; +import { + CanvasSavedObjectTypeMigrationsDeps, + templateWorkpadMigrationsFactory, +} from './migrations'; -export const workpadTemplateType: SavedObjectsType = { +export const workpadTemplateType = ( + deps: CanvasSavedObjectTypeMigrationsDeps +): SavedObjectsType => ({ name: TEMPLATE_TYPE, hidden: false, namespaceType: 'agnostic', @@ -44,7 +50,7 @@ export const workpadTemplateType: SavedObjectsType = { }, }, }, - migrations: {}, + migrations: templateWorkpadMigrationsFactory(deps), management: { importableAndExportable: false, icon: 'canvasApp', @@ -53,4 +59,4 @@ export const workpadTemplateType: SavedObjectsType = { return obj.attributes.name; }, }, -}; +}); diff --git a/x-pack/plugins/canvas/server/templates/pitch_presentation.ts b/x-pack/plugins/canvas/server/templates/pitch_presentation.ts index a9f09ada989c6..82ad535852c97 100644 --- a/x-pack/plugins/canvas/server/templates/pitch_presentation.ts +++ b/x-pack/plugins/canvas/server/templates/pitch_presentation.ts @@ -63,7 +63,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "# Sample."\n| render css=".canvasRenderEl h1 {\ntext-align: center;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "# Sample."\n| render css=".canvasRenderEl h1 {\ntext-align: center;\n}"', }, { id: 'element-33286979-7ea0-41ce-9835-b3bf07f09272', @@ -76,7 +76,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### This is a subtitle"\n| render css=".canvasRenderEl h3 {\ntext-align: center;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### This is a subtitle"\n| render css=".canvasRenderEl h3 {\ntext-align: center;\n}"', }, { id: 'element-1e3b3ffe-4ed8-4376-aad3-77e06d29cafe', @@ -89,7 +89,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "Footnote can go here"\n| render \n css=".canvasRenderEl p {\ntext-align: center;\ncolor: #FFFFFF;\nfont-size: 18px;\nopacity: .7;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "Footnote can go here"\n| render \n css=".canvasRenderEl p {\ntext-align: center;\ncolor: #FFFFFF;\nfont-size: 18px;\nopacity: .7;\n}"', }, { id: 'element-5b5035a3-d5b7-4483-a240-2cf80f5e0acf', @@ -150,7 +150,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render', }, { id: 'element-96a390b6-3d0a-4372-89cb-3ff38eec9565', @@ -162,7 +163,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Half text, half _image._"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Half text, half _image._"\n| render', }, { id: 'element-118b848d-0f89-4d20-868c-21597b7fd5e0', @@ -188,7 +190,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, ], groups: [], @@ -223,7 +225,7 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### BIOS"\n| render', + expression: 'kibana\n| selectFilter\n| demodata\n| markdown "##### BIOS"\n| render', }, { id: 'element-e2c658ee-7614-4d92-a46e-2b1a81a24485', @@ -236,7 +238,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Jane Doe" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Jane Doe" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-3d16765e-5251-4954-8e2a-6c64ed465b73', @@ -249,7 +251,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Developer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Developer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, { id: 'element-624675cf-46e9-4545-b86a-5409bbe53ac1', @@ -262,7 +264,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-dc841809-d2a9-491b-b44f-be92927b8034', @@ -301,7 +303,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. " \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-62f241ec-71ce-4edb-a27b-0de990522d20', @@ -314,7 +316,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Designer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Designer" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, { id: 'element-aa6c07e0-937f-4362-9d52-f70738faa0c5', @@ -340,7 +342,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## John Smith" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## John Smith" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, ], groups: [], @@ -388,7 +390,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 10"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 10"\n| render', }, { id: 'element-96be0724-0945-4802-8929-1dc456192fb5', @@ -401,7 +404,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Another page style."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Another page style."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', }, { id: 'element-3b4ba0ff-7f95-460e-9fa6-0cbb0f8f3df8', @@ -427,7 +430,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-0b9aa82b-fb0c-4000-805b-146cc9280bc5', @@ -440,7 +443,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Introduction"\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Introduction"\n| render css=".canvasRenderEl h3 {\ncolor: #444444;\n}"', }, ], groups: [], @@ -489,7 +492,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-1ba728f0-f645-4910-9d32-fa5b5820a94c', @@ -502,7 +505,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-db9051eb-7699-4883-b67f-945979cf5650', @@ -528,7 +531,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-fc11525c-2d9c-4a7b-9d96-d54e7bc6479b', @@ -554,7 +557,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-eb9a8883-de47-4a46-9400-b7569f9e69e6', @@ -567,7 +570,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-20c1c86a-658b-4bd2-8326-f987ef84e730', @@ -580,7 +583,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. Donec mauris, ut scelerisque urna. Sed vel neque quis metus luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render', }, { id: 'element-335db0c3-f678-4cb8-8b93-a6494f1787f5', @@ -593,7 +596,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="wheel" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=30 align="center" color="#45bdb0" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render', }, { id: 'element-079d3cbf-8b15-4ce2-accb-6ba04481019d', @@ -667,7 +670,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render', }, { id: 'element-0f2b9268-f0bd-41b7-abc8-5593276f26fa', @@ -680,7 +684,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Bold title text goes _here_."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Bold title text goes _here_."\n| render', }, { id: 'element-4f4b503e-f1ef-4ab7-aa1d-5d95b3e2e605', @@ -706,7 +710,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-f3f28541-06fe-47ea-89b7-1c5831e28e71', @@ -719,7 +723,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "Caption text goes here" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="right" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "Caption text goes here" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="right" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', }, ], groups: [], @@ -768,7 +772,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-5afa7019-af44-4919-9e11-24e2348cfae9', @@ -781,7 +785,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title for live charts."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title for live charts."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-7b856b52-0d8b-492b-a71f-3508a84388a6', @@ -820,7 +824,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## _Charts with live data._"\n| render css=".canvasRenderEl h1 {\n\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## _Charts with live data._"\n| render css=".canvasRenderEl h1 {\n\n}"', }, { id: 'element-317bed0b-f067-4d2d-8cb4-1145f6e0a11c', @@ -833,7 +837,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-34385617-6eb7-4918-b4db-1a0e8dd6eabe', @@ -846,7 +850,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-b22a35eb-b177-4664-800e-57b91436a879', @@ -859,7 +863,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-651f8a4a-6069-49bf-a7b0-484854628a79', @@ -872,7 +876,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "mean(percent_uptime)"\n| progress shape="horizontalBar" label={formatnumber "0%"} \n font={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=18 align="center" color="#444444" weight="bold" underline=false italic=false} valueColor="#45bdb0" valueWeight=15 barColor="#444444" barWeight=15\n| render css=".canvasRenderEl {\nwidth: 100%;\n}"', }, { id: 'element-0ee8c529-4155-442f-8c7c-1df86be37051', @@ -885,7 +889,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, { id: 'element-3fb61301-3dc2-411f-ac69-ad22bd37c77d', @@ -898,7 +902,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. \n\nDonec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Cras dapibus urna non feugiat imperdiet. \n\nDonec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render', }, ], groups: [], @@ -960,7 +964,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #45bdb0;\n}"', }, { id: 'element-8b9d3e2b-1d7b-48f4-897c-bf48f0f363d4', @@ -973,7 +977,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title on a _dark_ background."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title on a _dark_ background."\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-080c3153-45f7-4efc-8b23-ed7735da426f', @@ -999,7 +1003,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dapibus urna non feugiat imperdiet. Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus."\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, ], groups: [], @@ -1021,7 +1025,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Bullet point layout style"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Bullet point layout style"\n| render', }, { id: 'element-37dc903a-1c6d-4452-8fc0-38d4afa4631a', @@ -1034,7 +1039,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus\n- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus"\n| render css=".canvasRenderEl li {\nfont-size: 24px;\nline-height: 30px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus\n- Dolor sit amet, consectetur adipiscing elit\n- Cras dapibus urna non feugiat imperdiet\n- Donec vel sollicitudin mauris, ut scelerisque urna\n- Sed vel neque quis metus vulputate luctus"\n| render css=".canvasRenderEl li {\nfont-size: 24px;\nline-height: 30px;\n}"', }, { id: 'element-e506de9d-bda1-4018-89bf-f8d02ee5738e', @@ -1047,7 +1052,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="left" color="#000000" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 18px;\nopacity: .8;\n}"', }, { id: 'element-ea5319f5-d204-48c5-a9a0-0724676869a6', @@ -1073,7 +1078,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], @@ -1095,7 +1100,8 @@ export const pitch: CanvasTemplate = { angle: 0, parent: null, }, - expression: 'filters\n| demodata\n| markdown "## Paragraph layout style"\n| render', + expression: + 'kibana\n| selectFilter\n| demodata\n| markdown "## Paragraph layout style"\n| render', }, { id: 'element-92b05ab1-c504-4110-a8ad-73d547136024', @@ -1108,7 +1114,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Proin ipsum orci, consectetur a lacus vel, varius rutrum neque. Mauris quis gravida tellus. Integer quis tellus non lectus vestibulum fermentum. Quisque tortor justo, vulputate quis mollis eu, molestie eu ex. Nam eu arcu ac dui mattis facilisis aliquam venenatis est. Quisque tempor risus quis arcu viverra, quis consequat dolor molestie. Sed sed arcu dictum, sollicitudin dui id, iaculis elit. Nunc odio ex, placerat sed hendrerit vitae, finibus eu felis. Sed vulputate mi diam, at dictum mi tempus eu.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus."\n| render css=".canvasRenderEl p {\nfont-size: 24px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Proin ipsum orci, consectetur a lacus vel, varius rutrum neque. Mauris quis gravida tellus. Integer quis tellus non lectus vestibulum fermentum. Quisque tortor justo, vulputate quis mollis eu, molestie eu ex. Nam eu arcu ac dui mattis facilisis aliquam venenatis est. Quisque tempor risus quis arcu viverra, quis consequat dolor molestie. Sed sed arcu dictum, sollicitudin dui id, iaculis elit. Nunc odio ex, placerat sed hendrerit vitae, finibus eu felis. Sed vulputate mi diam, at dictum mi tempus eu.\n\nClass aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus."\n| render css=".canvasRenderEl p {\nfont-size: 24px;\n}"', }, { id: 'element-e49141ec-3034-4bec-88ca-f9606d12a60a', @@ -1134,7 +1140,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], @@ -1170,7 +1176,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## Title text can also go _here_ on multiple lines." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## Title text can also go _here_ on multiple lines." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-a8e0d4b3-864d-4dae-b0dc-64caad06c106', @@ -1196,7 +1202,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, { id: 'element-b54e2908-6908-4dd6-90f1-3ca489807016', @@ -1222,7 +1228,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Vivamus malesuada tortor vel eleifend lobortis. Donec vestibulum neque vel neque vehicula auctor. Proin id felis a leo ultrices maximus. Nam est nulla, venenatis at mi et, sodales convallis eros. Aliquam a convallis justo, eu viverra augue. Donec mollis ipsum sed orci posuere, vel posuere neque tempus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\n}"', }, { id: 'element-aa54f47c-fecf-4bdb-ac1d-b815d4a8d71d', @@ -1235,7 +1241,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## This title is a _centered_ layout." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## This title is a _centered_ layout." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h2 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-6ae072e7-213c-4de9-af22-7fb3e254cf52', @@ -1284,7 +1290,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "## \\"Aliquam mollis auctor nisl vitae varius. Donec nunc turpis, condimentum non sagittis tristique, sollicitudin blandit sem.\\"" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=true}\n| render', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "## \\"Aliquam mollis auctor nisl vitae varius. Donec nunc turpis, condimentum non sagittis tristique, sollicitudin blandit sem.\\"" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=true}\n| render', }, { id: 'element-989daff8-3571-4e02-b5fc-26657b2d9aaf', @@ -1310,7 +1316,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Lorem Ipsum" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Lorem Ipsum" \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, { id: 'element-cf931bd0-e3b6-4ae3-9164-8fe9ba14873d', @@ -1372,7 +1378,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #FFFFFF;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "##### CATEGORY 01"\n| render css=".canvasRenderEl h5 {\ncolor: #FFFFFF;\n}"', }, { id: 'element-dc4336d5-9752-421f-8196-9f4a6f8150f0', @@ -1385,7 +1391,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, { id: 'element-b8325cb3-2856-4fd6-8c5a-cba2430dda3e', @@ -1411,7 +1417,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(project)"\n| metric "Projects" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(project)"\n| metric "Projects" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-07f73884-13e9-4a75-8a23-4eb137e75817', @@ -1424,7 +1430,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#FFFFFF" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 16px;\nopacity: .7;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Vel sollicitudin mauris, ut scelerisque urna." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=14 align="center" color="#FFFFFF" weight="normal" underline=false italic=true}\n| render css=".canvasRenderEl p {\nfont-size: 16px;\nopacity: .7;\n}"', }, { id: 'element-201b8f78-045e-4457-9ada-5166965e64cf', @@ -1437,7 +1443,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, { id: 'element-9b667060-18ba-4f4d-84a2-48adff57efac', @@ -1450,7 +1456,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(country)"\n| metric "Countries" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(country)"\n| metric "Countries" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-23fcecca-1f6a-44f6-b441-0f65e03d8210', @@ -1463,7 +1469,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| math "unique(username)"\n| metric "Customers" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| math "unique(username)"\n| metric "Customers" \n metricFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=72 align="center" color="#45bdb0" weight="bold" underline=false italic=false} \n labelFont={font family="Futura, Impact, Helvetica, Arial, sans-serif" size=24 align="center" color="#45bdb0" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl .canvasMetric__metric {\nmargin-bottom: 32px;\n}"', }, { id: 'element-19f1db84-7a46-4ccb-a6b9-afd6ddd68523', @@ -1476,7 +1482,7 @@ export const pitch: CanvasTemplate = { parent: 'group-1303d0b2-057a-40bf-a0ff-4907b00a285c', }, expression: - 'filters\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown \n "Donec vel sollicitudin mauris, ut scelerisque urna. Sed vel neque quis metus vulputate luctus." \n font={font family="\'Open Sans\', Helvetica, Arial, sans-serif" size=18 align="center" color="#000000" weight="normal" underline=false italic=false}\n| render css=".canvasRenderEl p {\ncolor: #FFFFFF;\nopacity: .8;\nfont-size: 18px;\n}"', }, ], groups: [], @@ -1499,7 +1505,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "## An alternative opening title slide."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "## An alternative opening title slide."\n| render css=".canvasRenderEl h2 {\nfont-size: 64px;\n}"', }, { id: 'element-433586c1-4d44-40cf-988e-cf51871248fb', @@ -1525,7 +1531,7 @@ export const pitch: CanvasTemplate = { parent: null, }, expression: - 'filters\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', + 'kibana\n| selectFilter\n| demodata\n| markdown "### Subtitle goes here"\n| render css=".canvasRenderEl h3 {\ncolor: #45bdb0;\ntext-transform: none;\n}"', }, ], groups: [], diff --git a/x-pack/plugins/canvas/server/workpad_route_context.ts b/x-pack/plugins/canvas/server/workpad_route_context.ts index 9727327fcbd79..d7c818b786e32 100644 --- a/x-pack/plugins/canvas/server/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/workpad_route_context.ts @@ -16,12 +16,13 @@ import { WorkpadAttributes } from './routes/workpad/workpad_attributes'; import { CANVAS_TYPE } from '../common/lib/constants'; import { injectReferences, extractReferences } from './saved_objects/workpad_references'; import { getId } from '../common/lib/get_id'; -import { CanvasWorkpad } from '../types'; +import { CanvasWorkpad, ImportedCanvasWorkpad } from '../types'; export interface CanvasRouteHandlerContext extends RequestHandlerContext { canvas: { workpad: { create: (attributes: CanvasWorkpad) => Promise>; + import: (workpad: ImportedCanvasWorkpad) => Promise>; get: (id: string) => Promise>; resolve: (id: string) => Promise>; update: ( @@ -62,6 +63,33 @@ export const createWorkpadRouteContext: ( { id, references } ); }, + import: async (workpad: ImportedCanvasWorkpad) => { + const now = new Date().toISOString(); + const { id: maybeId, ...workpadWithoutId } = workpad; + + // Functionality of running migrations on import of workpads was implemented in v8.1.0. + // As only attributes of the saved object workpad are exported, to run migrations it is necessary + // to specify the minimal version of possible migrations to execute them. It is v8.0.0 in the current case. + const DEFAULT_MIGRATION_VERSION = { [CANVAS_TYPE]: '8.0.0' }; + const DEFAULT_CORE_MIGRATION_VERSION = '8.0.0'; + + const id = maybeId ? maybeId : getId('workpad'); + + return await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + isWriteable: true, + ...workpadWithoutId, + '@timestamp': now, + '@created': now, + }, + { + migrationVersion: DEFAULT_MIGRATION_VERSION, + coreMigrationVersion: DEFAULT_CORE_MIGRATION_VERSION, + id, + } + ); + }, get: async (id: string) => { const workpad = await context.core.savedObjects.client.get( CANVAS_TYPE, diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts index efb121b2948af..09add343aeac4 100644 --- a/x-pack/plugins/canvas/types/canvas.ts +++ b/x-pack/plugins/canvas/types/canvas.ts @@ -66,15 +66,30 @@ export interface CanvasWorkpad { width: number; } -type CanvasTemplateElement = Omit; -type CanvasTemplatePage = Omit & { elements: CanvasTemplateElement[] }; +export type ImportedCanvasWorkpad = Omit< + CanvasWorkpad, + '@created' | '@timestamp' | 'id' | 'isWriteable' +> & { + id?: CanvasWorkpad['id']; + isWriteable?: CanvasWorkpad['isWriteable']; + '@created'?: CanvasWorkpad['@created']; + '@timestamp'?: CanvasWorkpad['@timestamp']; +}; + +export type CanvasTemplateElement = Omit; +export type CanvasTemplatePage = Omit & { + elements: CanvasTemplateElement[]; +}; + export interface CanvasTemplate { id: string; name: string; help: string; tags: string[]; template_key: string; - template?: Omit & { pages: CanvasTemplatePage[] }; + template?: Omit & { + pages: CanvasTemplatePage[] | undefined; + }; } export interface CanvasWorkpadBoundingBox { diff --git a/x-pack/plugins/canvas/types/elements.ts b/x-pack/plugins/canvas/types/elements.ts index 0baf1e086d155..0119c0a842f50 100644 --- a/x-pack/plugins/canvas/types/elements.ts +++ b/x-pack/plugins/canvas/types/elements.ts @@ -49,6 +49,12 @@ export interface CustomElement { content: string; } +export type CustomElementNode = Omit; + +export interface CustomElementContent { + selectedNodes: CustomElementNode[]; +} + export interface ElementPosition { /** * distance from the left edge of the page diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts index 2c3931485757d..7c9b785dbb2bb 100644 --- a/x-pack/plugins/canvas/types/renderers.ts +++ b/x-pack/plugins/canvas/types/renderers.ts @@ -26,8 +26,6 @@ export interface CanvasSpecificRendererHandlers { onResize: GenericRendererCallback; /** Handler to invoke when an element should be resized. */ resize: (size: { height: number; width: number }) => void; - /** Sets the value of the filter property on the element object persisted on the workpad */ - setFilter: (filter: string) => void; } export type RendererHandlers = IInterpreterRenderHandlers & CanvasSpecificRendererHandlers; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx index 084dcd139f9bf..21fcac7ea6c75 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx @@ -5,22 +5,45 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { Query, IndexPattern, TimefilterContract } from 'src/plugins/data/public'; -import { EuiButton } from '@elastic/eui'; +import { TimefilterContract } from 'src/plugins/data/public'; +import { DataView } from 'src/plugins/data/common'; + +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiRadioGroup, + EuiRadioGroupOption, + EuiToolTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { setFullTimeRange } from './full_time_range_selector_service'; import { useDataVisualizerKibana } from '../../../kibana_context'; +import { DV_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage'; + +export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; interface Props { timefilter: TimefilterContract; - indexPattern: IndexPattern; + indexPattern: DataView; disabled: boolean; - query?: Query; + query?: QueryDslQueryContainer; callback?: (a: any) => void; } +const FROZEN_TIER_PREFERENCE = { + EXCLUDE: 'exclude-frozen', + INCLUDE: 'include-frozen', +} as const; + +type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE]; + // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana data view or query. export const FullTimeRangeSelector: FC = ({ @@ -37,36 +60,144 @@ export const FullTimeRangeSelector: FC = ({ } = useDataVisualizerKibana(); // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop - async function setRange(i: IndexPattern, q?: Query) { - try { - const fullTimeRange = await setFullTimeRange(timefilter, i, q); - if (typeof callback === 'function') { - callback(fullTimeRange); + const setRange = useCallback( + async (i: DataView, q?: QueryDslQueryContainer, excludeFrozenData?: boolean) => { + try { + const fullTimeRange = await setFullTimeRange(timefilter, i, q, excludeFrozenData); + if (typeof callback === 'function') { + callback(fullTimeRange); + } + } catch (e) { + toasts.addDanger( + i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', + { + defaultMessage: 'An error occurred setting the time range.', + } + ) + ); } - } catch (e) { - toasts.addDanger( - i18n.translate( - 'xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification', + }, + [callback, timefilter, toasts] + ); + + const [isPopoverOpen, setPopover] = useState(false); + + const [frozenDataPreference, setFrozenDataPreference] = useStorage( + DV_FROZEN_TIER_PREFERENCE, + // By default we will exclude frozen data tier + FROZEN_TIER_PREFERENCE.EXCLUDE + ); + + const setPreference = useCallback( + (id: string) => { + setFrozenDataPreference(id as FrozenTierPreference); + setRange(indexPattern, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE); + closePopover(); + }, + [indexPattern, query, setFrozenDataPreference, setRange] + ); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const sortOptions: EuiRadioGroupOption[] = useMemo(() => { + return [ + { + id: FROZEN_TIER_PREFERENCE.EXCLUDE, + label: i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', { - defaultMessage: 'An error occurred setting the time range.', + defaultMessage: 'Exclude frozen data tier', } - ) - ); - } - } + ), + }, + { + id: FROZEN_TIER_PREFERENCE.INCLUDE, + label: i18n.translate( + 'xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', + { + defaultMessage: 'Include frozen data tier', + } + ), + }, + ]; + }, []); + + const popoverContent = useMemo( + () => ( + + + + ), + [sortOptions, frozenDataPreference, setPreference] + ); + + const buttonTooltip = useMemo( + () => + frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + + ) : ( + + ), + [frozenDataPreference] + ); + return ( - setRange(indexPattern, query)} - data-test-subj="dataVisualizerButtonUseFullData" - > - - + + + setRange(indexPattern, query, true)} + data-test-subj="dataVisualizerButtonUseFullData" + > + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + ); }; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts index f2d14de9812ca..303d54c9d45cc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts @@ -7,12 +7,14 @@ import moment from 'moment'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Query, TimefilterContract } from 'src/plugins/data/public'; +import { TimefilterContract } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { isPopulatedObject } from '../../../../../common/utils/object_utils'; import { getTimeFieldRange } from '../../services/time_field_range'; import { GetTimeFieldRangeResponse } from '../../../../../common/types/time_field_request'; +import { addExcludeFrozenToQuery } from '../../utils/query_utils'; export interface TimeRange { from: number; @@ -22,14 +24,15 @@ export interface TimeRange { export async function setFullTimeRange( timefilter: TimefilterContract, indexPattern: IndexPattern, - query?: Query + query?: QueryDslQueryContainer, + excludeFrozenData?: boolean ): Promise { const runtimeMappings = indexPattern.getComputedFields() .runtimeFields as estypes.MappingRuntimeFields; const resp = await getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, - query, + query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query, ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); timefilter.setTime({ diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts new file mode 100644 index 0000000000000..d6b0bb3322c03 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_storage.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import { useDataVisualizerKibana } from '../../kibana_context'; + +export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference'; + +export type DV = Partial<{ + [DV_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen'; +}> | null; + +export type DVKey = keyof Exclude; + +/** + * Hook for accessing and changing a value in the storage. + * @param key - Storage key + * @param initValue + */ +export function useStorage(key: DVKey, initValue?: T): [T, (value: T) => void] { + const { + services: { storage }, + } = useDataVisualizerKibana(); + + const [val, setVal] = useState(storage.get(key) ?? initValue); + + const setStorage = useCallback( + (value: T): void => { + try { + storage.set(key, value); + setVal(value); + } catch (e) { + throw new Error('Unable to update storage with provided value'); + } + }, + [key, storage] + ); + + return [val, setStorage]; +} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts index 58a4bd4520829..bcf32a7f62bd7 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts @@ -6,9 +6,9 @@ */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { lazyLoadModules } from '../../../lazy_load_bundle'; import { GetTimeFieldRangeResponse } from '../../../../common/types/time_field_request'; -import { Query } from '../../../../../../../src/plugins/data/common/query'; export async function getTimeFieldRange({ index, @@ -18,7 +18,7 @@ export async function getTimeFieldRange({ }: { index: string; timeFieldName?: string; - query?: Query; + query?: QueryDslQueryContainer; runtimeMappings?: estypes.MappingRuntimeFields; }) { const body = JSON.stringify({ index, timeFieldName, query, runtimeMappings }); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts new file mode 100644 index 0000000000000..947b87e9976d5 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addExcludeFrozenToQuery } from './query_utils'; + +describe('Util: addExcludeFrozenToQuery()', () => { + test('Validation checks.', () => { + expect( + addExcludeFrozenToQuery({ + match_all: {}, + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }) + ).toMatchObject({ + bool: { + must: [{ match_all: {} }], + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: { + term: { + category: { + value: 'clothing', + }, + }, + }, + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: [{ term: { category: { value: 'clothing' } } }], + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect(addExcludeFrozenToQuery(undefined)).toMatchObject({ + bool: { + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + }); +}); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts new file mode 100644 index 0000000000000..43c5d49d1986f --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/query_utils.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { cloneDeep } from 'lodash'; +import { isPopulatedObject } from '../../../../common/utils/object_utils'; + +export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { + const FROZEN_TIER_TERM = { + term: { + _tier: { + value: 'data_frozen', + }, + }, + }; + + if (!originalQuery) { + return { + bool: { + must_not: [FROZEN_TIER_TERM], + }, + }; + } + + const query = cloneDeep(originalQuery); + + delete query.match_all; + + if (isPopulatedObject(query.bool)) { + // Must_not can be both arrays or singular object + if (Array.isArray(query.bool.must_not)) { + query.bool.must_not.push(FROZEN_TIER_TERM); + } else { + // If there's already a must_not condition + if (isPopulatedObject(query.bool.must_not)) { + query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; + } + if (query.bool.must_not === undefined) { + query.bool.must_not = [FROZEN_TIER_TERM]; + } + } + } else { + query.bool = { + must_not: [FROZEN_TIER_TERM], + }; + } + + return query; +}; diff --git a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts index 58d0ac021ff22..83fcc104fbe4b 100644 --- a/x-pack/plugins/data_visualizer/public/application/kibana_context.ts +++ b/x-pack/plugins/data_visualizer/public/application/kibana_context.ts @@ -8,7 +8,11 @@ import { CoreStart } from 'kibana/public'; import { KibanaReactContextValue, useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { DataVisualizerStartDependencies } from '../plugin'; +import type { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; -export type StartServices = CoreStart & DataVisualizerStartDependencies; +export type StartServices = CoreStart & + DataVisualizerStartDependencies & { + storage: IStorageWrapper; + }; export type DataVisualizerKibanaReactContextValue = KibanaReactContextValue; export const useDataVisualizerKibana = () => useKibana(); diff --git a/x-pack/plugins/file_upload/server/get_time_field_range.ts b/x-pack/plugins/file_upload/server/get_time_field_range.ts index 126269e22dd3a..84fc6ac002008 100644 --- a/x-pack/plugins/file_upload/server/get_time_field_range.ts +++ b/x-pack/plugins/file_upload/server/get_time_field_range.ts @@ -6,13 +6,14 @@ */ import { IScopedClusterClient } from 'kibana/server'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { isPopulatedObject } from '../common/utils'; export async function getTimeFieldRange( client: IScopedClusterClient, index: string[] | string, timeFieldName: string, - query: any, + query: QueryDslQueryContainer, runtimeMappings?: estypes.MappingRuntimeFields ): Promise<{ success: boolean; diff --git a/x-pack/plugins/ml/common/types/storage.ts b/x-pack/plugins/ml/common/types/storage.ts index 6da8076e22332..22374a5533fab 100644 --- a/x-pack/plugins/ml/common/types/storage.ts +++ b/x-pack/plugins/ml/common/types/storage.ts @@ -13,6 +13,8 @@ export const ML_APPLY_TIME_RANGE_CONFIG = 'ml.jobSelectorFlyout.applyTimeRange'; export const ML_GETTING_STARTED_CALLOUT_DISMISSED = 'ml.gettingStarted.isDismissed'; +export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; + export type PartitionFieldConfig = | { /** @@ -44,6 +46,7 @@ export type MlStorage = Partial<{ [ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig; [ML_APPLY_TIME_RANGE_CONFIG]: ApplyTimeRangeConfig; [ML_GETTING_STARTED_CALLOUT_DISMISSED]: boolean | undefined; + [ML_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen'; }> | null; export type MlStorageKey = keyof Exclude; diff --git a/x-pack/plugins/ml/common/util/query_utils.test.ts b/x-pack/plugins/ml/common/util/query_utils.test.ts new file mode 100644 index 0000000000000..947b87e9976d5 --- /dev/null +++ b/x-pack/plugins/ml/common/util/query_utils.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addExcludeFrozenToQuery } from './query_utils'; + +describe('Util: addExcludeFrozenToQuery()', () => { + test('Validation checks.', () => { + expect( + addExcludeFrozenToQuery({ + match_all: {}, + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }) + ).toMatchObject({ + bool: { + must: [{ match_all: {} }], + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: { + term: { + category: { + value: 'clothing', + }, + }, + }, + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect( + addExcludeFrozenToQuery({ + bool: { + must: [], + must_not: [{ term: { category: { value: 'clothing' } } }], + }, + }) + ).toMatchObject({ + bool: { + must: [], + must_not: [ + { term: { category: { value: 'clothing' } } }, + { term: { _tier: { value: 'data_frozen' } } }, + ], + }, + }); + + expect(addExcludeFrozenToQuery(undefined)).toMatchObject({ + bool: { + must_not: [{ term: { _tier: { value: 'data_frozen' } } }], + }, + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/query_utils.ts b/x-pack/plugins/ml/common/util/query_utils.ts new file mode 100644 index 0000000000000..22c0f45f2f239 --- /dev/null +++ b/x-pack/plugins/ml/common/util/query_utils.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { cloneDeep } from 'lodash'; +import { isPopulatedObject } from './object_utils'; + +export const addExcludeFrozenToQuery = (originalQuery: QueryDslQueryContainer | undefined) => { + const FROZEN_TIER_TERM = { + term: { + _tier: { + value: 'data_frozen', + }, + }, + }; + + if (!originalQuery) { + return { + bool: { + must_not: [FROZEN_TIER_TERM], + }, + }; + } + + const query = cloneDeep(originalQuery); + + delete query.match_all; + + if (isPopulatedObject(query.bool)) { + // Must_not can be both arrays or singular object + if (Array.isArray(query.bool.must_not)) { + query.bool.must_not.push(FROZEN_TIER_TERM); + } else { + // If there's already a must_not condition + if (isPopulatedObject(query.bool.must_not)) { + query.bool.must_not = [query.bool.must_not, FROZEN_TIER_TERM]; + } + if (query.bool.must_not === undefined) { + query.bool.must_not = [FROZEN_TIER_TERM]; + } + } + } else { + query.bool = { + must_not: [FROZEN_TIER_TERM], + }; + } + + return query; +}; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap b/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap index eb9705f3438aa..9a3fb9b29d09b 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/__snapshots__/full_time_range_selector.test.tsx.snap @@ -1,19 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FullTimeRangeSelector renders the selector 1`] = ` - - } - /> - + delay="regular" + display="inlineBlock" + position="top" + > + + + + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mlFullTimeRangeSelectorOption" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + `; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx index d04f8f7b648f5..3f64ff794d9ab 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.test.tsx @@ -20,6 +20,12 @@ jest.mock('./full_time_range_selector_service', () => ({ mockSetFullTimeRange(indexPattern, query), })); +jest.mock('../../contexts/ml/use_storage', () => { + return { + useStorage: jest.fn(() => 'exclude-frozen'), + }; +}); + describe('FullTimeRangeSelector', () => { const dataView = { id: '0844fc70-5ab5-11e9-935e-836737467b0f', diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx index f0af666e07dbc..44f6fc5e604cb 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector.tsx @@ -5,44 +5,160 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { Query } from 'src/plugins/data/public'; -import { EuiButton } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiButton, + EuiFlexItem, + EuiButtonIcon, + EuiRadioGroup, + EuiPanel, + EuiToolTip, + EuiPopover, + EuiRadioGroupOption, +} from '@elastic/eui'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { i18n } from '@kbn/i18n'; import type { DataView } from '../../../../../../../src/plugins/data_views/public'; import { setFullTimeRange } from './full_time_range_selector_service'; +import { useStorage } from '../../contexts/ml/use_storage'; +import { ML_FROZEN_TIER_PREFERENCE } from '../../../../common/types/storage'; interface Props { dataView: DataView; - query: Query; + query: QueryDslQueryContainer; disabled: boolean; callback?: (a: any) => void; } +const FROZEN_TIER_PREFERENCE = { + EXCLUDE: 'exclude-frozen', + INCLUDE: 'include-frozen', +} as const; + +type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE]; + // Component for rendering a button which automatically sets the range of the time filter // to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query. export const FullTimeRangeSelector: FC = ({ dataView, query, disabled, callback }) => { // wrapper around setFullTimeRange to allow for the calling of the optional callBack prop - async function setRange(i: DataView, q: Query) { - const fullTimeRange = await setFullTimeRange(i, q); + async function setRange(i: DataView, q: QueryDslQueryContainer, excludeFrozenData = true) { + const fullTimeRange = await setFullTimeRange(i, q, excludeFrozenData); if (typeof callback === 'function') { callback(fullTimeRange); } } + + const [isPopoverOpen, setPopover] = useState(false); + const [frozenDataPreference, setFrozenDataPreference] = useStorage( + ML_FROZEN_TIER_PREFERENCE, + FROZEN_TIER_PREFERENCE.EXCLUDE + ); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const sortOptions: EuiRadioGroupOption[] = useMemo(() => { + return [ + { + id: FROZEN_TIER_PREFERENCE.EXCLUDE, + label: i18n.translate( + 'xpack.ml.fullTimeRangeSelector.useFullDataExcludingFrozenMenuLabel', + { + defaultMessage: 'Exclude frozen data tier', + } + ), + }, + { + id: FROZEN_TIER_PREFERENCE.INCLUDE, + label: i18n.translate( + 'xpack.ml.fullTimeRangeSelector.useFullDataIncludingFrozenMenuLabel', + { + defaultMessage: 'Include frozen data tier', + } + ), + }, + ]; + }, []); + + const setPreference = useCallback((id: string) => { + setFrozenDataPreference(id as FrozenTierPreference); + setRange(dataView, query, id === FROZEN_TIER_PREFERENCE.EXCLUDE); + closePopover(); + }, []); + + const popoverContent = useMemo( + () => ( + + + + ), + [frozenDataPreference, sortOptions] + ); + + const buttonTooltip = useMemo( + () => + frozenDataPreference === FROZEN_TIER_PREFERENCE.EXCLUDE ? ( + + ) : ( + + ), + [frozenDataPreference] + ); + return ( - setRange(dataView, query)} - data-test-subj="mlButtonUseFullData" - > - - + + + setRange(dataView, query, true)} + data-test-subj="mlButtonUseFullData" + > + + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downRight" + > + {popoverContent} + + + ); }; diff --git a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts index 8f0d344a36f36..7e14639f1b8b4 100644 --- a/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/ml/public/application/components/full_time_range_selector/full_time_range_selector_service.ts @@ -8,13 +8,14 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import type { Query } from 'src/plugins/data/public'; import dateMath from '@elastic/datemath'; +import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; import { getTimefilter, getToastNotifications } from '../../util/dependency_cache'; import { ml, GetTimeFieldRangeResponse } from '../../services/ml_api_service'; import type { DataView } from '../../../../../../../src/plugins/data_views/public'; import { isPopulatedObject } from '../../../../common/util/object_utils'; -import { RuntimeMappings } from '../../../../common/types/fields'; +import type { RuntimeMappings } from '../../../../common/types/fields'; +import { addExcludeFrozenToQuery } from '../../../../common/util/query_utils'; export interface TimeRange { from: number; @@ -23,7 +24,8 @@ export interface TimeRange { export async function setFullTimeRange( indexPattern: DataView, - query: Query + query: QueryDslQueryContainer, + excludeFrozenData: boolean ): Promise { try { const timefilter = getTimefilter(); @@ -31,7 +33,8 @@ export async function setFullTimeRange( const resp = await ml.getTimeFieldRange({ index: indexPattern.title, timeFieldName: indexPattern.timeFieldName, - query, + // By default we want to use full non-frozen time range + query: excludeFrozenData ? addExcludeFrozenToQuery(query) : query, ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); timefilter.setTime({ diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index a6a707634811d..c370778b178c8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -42,6 +42,7 @@ import { TIME_FORMAT } from '../../../../../common/constants/time_format'; import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; import { isPopulatedObject } from '../../../../../common/util/object_utils'; import { RuntimeMappings } from '../../../../../common/types/fields'; +import { addExcludeFrozenToQuery } from '../../../../../common/util/query_utils'; import { MlPageHeader } from '../../../components/page_header'; export interface ModuleJobUI extends ModuleJob { @@ -136,7 +137,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const { start, end } = await ml.getTimeFieldRange({ index: dataView.title, timeFieldName: dataView.timeFieldName, - query: combinedQuery, + // By default we want to use full non-frozen time range + query: addExcludeFrozenToQuery(combinedQuery), ...(isPopulatedObject(runtimeMappings) ? { runtimeMappings } : {}), }); return { diff --git a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts index e608bfeb622d8..128517777bb46 100644 --- a/x-pack/plugins/ml/server/models/fields_service/fields_service.ts +++ b/x-pack/plugins/ml/server/models/fields_service/fields_service.ts @@ -16,7 +16,6 @@ import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed, IndicesOptions } from '../../../common/types/anomaly_detection_jobs'; import { RuntimeMappings } from '../../../common/types/fields'; import { isPopulatedObject } from '../../../common/util/object_utils'; - /** * Service for carrying out queries to obtain data * specific to fields in Elasticsearch indices. diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index fcc3fdd64c36c..03860fd3cd122 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -24,3 +24,7 @@ export const observabilityFeatureId = 'observability'; // Used by Cases to install routes export const casesPath = '/cases'; + +// Name of a locator created by the uptime plugin. Intended for use +// by other plugins as well, so defined here to prevent cross-references. +export const uptimeOverviewLocatorID = 'uptime-overview-locator'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 9a45dbcbdbd64..e502cf7fb37e0 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -24,6 +24,7 @@ export type { ObservabilityPublicPluginsStart, }; export { enableInspectEsQueries } from '../common/ui_settings_keys'; +export { uptimeOverviewLocatorID } from '../common'; export interface ConfigSchema { unsafe: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 901fbbef6714b..ad1b64be36fda 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6565,7 +6565,7 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "Elasticsearch インデックスを取得できませんでした", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "「{functionName}」のレンダリングが失敗しました", "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "ワークパッドのクローンを作成できませんでした", - "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", + "xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage": "ワークパッドをアップロードできませんでした", "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "すべてのワークパッドを削除できませんでした", "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "ワークパッドが見つかりませんでした", "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "{JSON} 個のファイルしか受け付けられませんでした", @@ -8388,7 +8388,6 @@ "xpack.dataVisualizer.index.fieldNameSelect": "フィールド名", "xpack.dataVisualizer.index.fieldTypeSelect": "フィールド型", "xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "時間範囲の設定中にエラーが発生しました。", - "xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "完全な {indexPatternTitle} データを使用", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "異常検知は時間ベースのインデックスでのみ実行されます", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "インデックスパターン {indexPatternTitle} は時系列に基づくものではありません", "xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName}の平均", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 76eac37d763c9..3c75348b6ad02 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6610,7 +6610,7 @@ "xpack.canvas.error.esService.indicesFetchErrorMessage": "无法提取 Elasticsearch 索引", "xpack.canvas.error.RenderWithFn.renderErrorMessage": "呈现“{functionName}”失败。", "xpack.canvas.error.useCloneWorkpad.cloneFailureErrorMessage": "无法克隆 Workpad", - "xpack.canvas.error.useCreateWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", + "xpack.canvas.error.useUploadWorkpad.uploadFailureErrorMessage": "无法上传 Workpad", "xpack.canvas.error.useDeleteWorkpads.deleteFailureErrorMessage": "无法删除所有 Workpad", "xpack.canvas.error.useFindWorkpads.findFailureErrorMessage": "无法查找 Workpad", "xpack.canvas.error.useImportWorkpad.acceptJSONOnlyErrorMessage": "仅接受 {JSON} 文件", @@ -8461,7 +8461,6 @@ "xpack.dataVisualizer.index.fieldNameSelect": "字段名称", "xpack.dataVisualizer.index.fieldTypeSelect": "字段类型", "xpack.dataVisualizer.index.fullTimeRangeSelector.errorSettingTimeRangeNotification": "设置时间范围时出错。", - "xpack.dataVisualizer.index.fullTimeRangeSelector.useFullDataButtonLabel": "使用完整的 {indexPatternTitle} 数据", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationDescription": "仅针对基于时间的索引运行异常检测", "xpack.dataVisualizer.index.indexPatternNotBasedOnTimeSeriesNotificationTitle": "索引模式 {indexPatternTitle} 不基于时间序列", "xpack.dataVisualizer.index.lensChart.averageOfLabel": "{fieldName} 的平均值", diff --git a/x-pack/plugins/uptime/common/config.ts b/x-pack/plugins/uptime/common/config.ts index 38ba7b7b3fd48..08db653214208 100644 --- a/x-pack/plugins/uptime/common/config.ts +++ b/x-pack/plugins/uptime/common/config.ts @@ -7,46 +7,50 @@ import { PluginConfigDescriptor } from 'kibana/server'; import { schema, TypeOf } from '@kbn/config-schema'; +import { sslSchema } from '@kbn/server-http-tools'; -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - ui: true, - }, - schema: schema.maybe( +const serviceConfig = schema.object({ + enabled: schema.boolean(), + username: schema.maybe(schema.string()), + password: schema.maybe(schema.string()), + manifestUrl: schema.string(), + hosts: schema.maybe(schema.arrayOf(schema.string())), + syncInterval: schema.maybe(schema.string()), + tls: schema.maybe(sslSchema), +}); + +const uptimeConfig = schema.object({ + index: schema.maybe(schema.string()), + ui: schema.maybe( schema.object({ - index: schema.maybe(schema.string()), - ui: schema.maybe( - schema.object({ - unsafe: schema.maybe( - schema.object({ - monitorManagement: schema.maybe( - schema.object({ - enabled: schema.boolean(), - }) - ), - }) - ), - }) - ), unsafe: schema.maybe( schema.object({ - service: schema.maybe( + monitorManagement: schema.maybe( schema.object({ enabled: schema.boolean(), - username: schema.string(), - password: schema.string(), - manifestUrl: schema.string(), - hosts: schema.maybe(schema.arrayOf(schema.string())), - syncInterval: schema.maybe(schema.string()), }) ), }) ), }) ), + unsafe: schema.maybe( + schema.object({ + service: serviceConfig, + }) + ), +}); + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: uptimeConfig, }; -export type UptimeConfig = TypeOf; +export type UptimeConfig = TypeOf; +export type ServiceConfig = TypeOf; + export interface UptimeUiConfig { ui?: TypeOf['ui']; } diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts new file mode 100644 index 0000000000000..d8746d715581d --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './tls_alert_flyouts_in_alerting_app'; +export * from './status_alert_flyouts_in_alerting_app'; diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts new file mode 100644 index 0000000000000..ba973a7aa8a61 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before } from '@elastic/synthetics'; +import { assertText, byTestId, loginToKibana, waitForLoadingToFinish } from '../utils'; + +journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`; + + step('Go to Alerting app', async () => { + await page.goto(`${baseUrl}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Open monitor status flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'This alert will apply to approximately 0 monitors.' }); + }); + + step('can add filters', async () => { + await page.click('text=Add filter'); + await page.click(byTestId('"uptimeAlertAddFilter.monitor.type"')); + await page.click(byTestId('"uptimeCreateStatusAlert.filter_scheme"')); + }); + + step('can open query bar', async () => { + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus.filterBar"')); + + await page.fill(byTestId('"xpack.uptime.alerts.monitorStatus.filterBar"'), 'monitor.type : '); + + await waitForLoadingToFinish({ page }); + + await assertText({ page, text: 'browser' }); + await assertText({ page, text: 'http' }); + + const suggestionItem = await page.$(byTestId('autoCompleteSuggestionText')); + expect(await suggestionItem?.textContent()).toBe('"browser" '); + + await page.click(byTestId('euiFlyoutCloseButton')); + await page.click(byTestId('confirmModalConfirmButton')); + }); + + step('Open tls alert flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'has a certificate expiring within' }); + }); + + step('Tls alert flyout has setting values', async () => { + await assertText({ page, text: '30 days' }); + await assertText({ page, text: '730 days' }); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts new file mode 100644 index 0000000000000..024e8e53c3b2a --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, before } from '@elastic/synthetics'; +import { assertText, byTestId, loginToKibana, waitForLoadingToFinish } from '../utils'; + +journey('TlsFlyoutInAlertingApp', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const baseUrl = `${params.kibanaUrl}/app/management/insightsAndAlerting/triggersActions/rules`; + + step('Go to Alerting app', async () => { + await page.goto(`${baseUrl}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Open tls alert flyout', async () => { + await page.click(byTestId('createFirstAlertButton')); + await waitForLoadingToFinish({ page }); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); + await waitForLoadingToFinish({ page }); + await assertText({ page, text: 'has a certificate expiring within' }); + }); + + step('Tls alert flyout has setting values', async () => { + await assertText({ page, text: '30 days' }); + await assertText({ page, text: '730 days' }); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index 89abed5ce8f29..6bdea1beb016b 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -7,3 +7,4 @@ export * from './uptime.journey'; export * from './step_duration.journey'; +export * from './alerts'; diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts index 3188c86f82049..6d2f1dd554108 100644 --- a/x-pack/plugins/uptime/e2e/journeys/utils.ts +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { Page } from '@elastic/synthetics'; -import { byTestId } from './uptime.journey'; +import { expect, Page } from '@elastic/synthetics'; export async function waitForLoadingToFinish({ page }: { page: Page }) { while (true) { @@ -25,3 +24,12 @@ export async function loginToKibana({ page }: { page: Page }) { await waitForLoadingToFinish({ page }); } + +export const byTestId = (testId: string) => { + return `[data-test-subj=${testId}]`; +}; + +export const assertText = async ({ page, text }: { page: Page; text: string }) => { + await page.waitForSelector(`text=${text}`); + expect(await page.$(`text=${text}`)).toBeTruthy(); +}; diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index fe4d3ff804bf9..0581692e0e278 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -13,18 +13,22 @@ import { esArchiverLoad, esArchiverUnload } from './tasks/es_archiver'; import './journeys'; +const listOfJourneys = [ + 'uptime', + 'StepsDuration', + 'TlsFlyoutInAlertingApp', + 'StatusFlyoutInAlertingApp', +] as const; + export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) { return async ({ getService }: any) => { const result = await playwrightStart(getService, headless, match); - if ( - result?.uptime && - result.uptime.status !== 'succeeded' && - result.StepsDuration && - result.StepsDuration.status !== 'succeeded' - ) { - throw new Error('Tests failed'); - } + listOfJourneys.forEach((journey) => { + if (result?.[journey] && result[journey].status !== 'succeeded') { + throw new Error('Tests failed'); + } + }); }; } diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index f3971b6bd4bf3..35be0b19d4521 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -1,43 +1,27 @@ { - "configPath": [ - "xpack", - "uptime" - ], + "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": [ - "cloud", - "data", - "fleet", - "home", - "ml" - ], + "optionalPlugins": ["cloud", "data", "fleet", "home", "ml"], "requiredPlugins": [ "alerting", "embeddable", "encryptedSavedObjects", - "inspector", "features", + "inspector", "licensing", "observability", "ruleRegistry", "security", + "share", + "taskManager", "triggersActionsUi", - "usageCollection", - "taskManager" + "usageCollection" ], "server": true, "ui": true, "version": "8.0.0", - "requiredBundles": [ - "observability", - "kibanaReact", - "kibanaUtils", - "home", - "data", - "ml", - "fleet" - ], + "requiredBundles": ["data", "fleet", "home", "kibanaReact", "kibanaUtils", "ml", "observability"], "owner": { "name": "Uptime", "githubTeam": "uptime" diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.test.ts b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts new file mode 100644 index 0000000000000..c414778f7769c --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/locators/overview.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OVERVIEW_ROUTE } from '../../../common/constants'; +import { uptimeOverviewNavigatorParams } from './overview'; + +describe('uptimeOverviewNavigatorParams', () => { + it('supplies the correct app name', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({}); + expect(location.app).toEqual('uptime'); + }); + + it('creates the expected path when no params specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({}); + expect(location.path).toEqual(OVERVIEW_ROUTE); + }); + + it('creates a path with expected search when ip is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ ip: '127.0.0.1' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1"`); + }); + + it('creates a path with expected search when hostname is specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ hostname: 'elastic.co' }); + expect(location.path).toEqual(`${OVERVIEW_ROUTE}?search=url.domain: "elastic.co"`); + }); + + it('creates a path with expected search when multiple keys are specified', async () => { + const location = await uptimeOverviewNavigatorParams.getLocation({ + hostname: 'elastic.co', + ip: '127.0.0.1', + }); + expect(location.path).toEqual( + `${OVERVIEW_ROUTE}?search=monitor.ip: "127.0.0.1" OR url.domain: "elastic.co"` + ); + }); +}); diff --git a/x-pack/plugins/uptime/public/apps/locators/overview.ts b/x-pack/plugins/uptime/public/apps/locators/overview.ts new file mode 100644 index 0000000000000..d7faf7b78f797 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/locators/overview.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { uptimeOverviewLocatorID } from '../../../../observability/public'; +import { OVERVIEW_ROUTE } from '../../../common/constants'; + +const formatSearchKey = (key: string, value: string) => `${key}: "${value}"`; + +async function navigate({ ip, hostname }: { ip?: string; hostname?: string }) { + const searchParams: string[] = []; + + if (ip) searchParams.push(formatSearchKey('monitor.ip', ip)); + if (hostname) searchParams.push(formatSearchKey('url.domain', hostname)); + + const searchString = searchParams.join(' OR '); + + const path = + searchParams.length === 0 ? OVERVIEW_ROUTE : OVERVIEW_ROUTE + `?search=${searchString}`; + + return { + app: 'uptime', + path, + state: {}, + }; +} + +export const uptimeOverviewNavigatorParams = { + id: uptimeOverviewLocatorID, + getLocation: navigate, +}; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index ec6deef429ca9..dd2287b3b1642 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { CoreSetup, CoreStart, @@ -14,6 +15,7 @@ import { import { from } from 'rxjs'; import { map } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; +import { SharePluginSetup, SharePluginStart } from '../../../../../src/plugins/share/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../../src/core/public'; import { @@ -29,6 +31,7 @@ import { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; + import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types'; import { FleetStart } from '../../../fleet/public'; import { @@ -47,19 +50,21 @@ import { Start as InspectorPluginStart } from '../../../../../src/plugins/inspec import { UptimeUiConfig } from '../../common/config'; export interface ClientPluginsSetup { - data: DataPublicPluginSetup; home?: HomePublicPluginSetup; + data: DataPublicPluginSetup; observability: ObservabilityPublicSetup; + share: SharePluginSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; } export interface ClientPluginsStart { - embeddable: EmbeddableStart; - data: DataPublicPluginStart; - triggersActionsUi: TriggersAndActionsUIPublicPluginStart; fleet?: FleetStart; - observability: ObservabilityPublicStart; + data: DataPublicPluginStart; inspector: InspectorPluginStart; + embeddable: EmbeddableStart; + observability: ObservabilityPublicStart; + share: SharePluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export interface UptimePluginServices extends Partial { diff --git a/x-pack/plugins/uptime/public/apps/render_app.tsx b/x-pack/plugins/uptime/public/apps/render_app.tsx index cc831680dbf09..23f8fc9a8e58c 100644 --- a/x-pack/plugins/uptime/public/apps/render_app.tsx +++ b/x-pack/plugins/uptime/public/apps/render_app.tsx @@ -18,6 +18,7 @@ import { import { UptimeApp, UptimeAppProps } from './uptime_app'; import { ClientPluginsSetup, ClientPluginsStart } from './plugin'; import { UptimeUiConfig } from '../../common/config'; +import { uptimeOverviewNavigatorParams } from './locators/overview'; export function renderApp( core: CoreStart, @@ -41,6 +42,8 @@ export function renderApp( const canSave = (capabilities.uptime.save ?? false) as boolean; + plugins.share.url.locators.create(uptimeOverviewNavigatorParams); + const props: UptimeAppProps = { plugins, canSave, diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx index 82917fc4e1758..9f3da1674ca09 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_tls.tsx @@ -6,10 +6,11 @@ */ import { useDispatch, useSelector } from 'react-redux'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { AlertTlsComponent } from '../alert_tls'; import { setAlertFlyoutVisible } from '../../../../state/actions'; import { selectDynamicSettings } from '../../../../state/selectors'; +import { getDynamicSettings } from '../../../../state/actions/dynamic_settings'; export const AlertTls: React.FC<{}> = () => { const dispatch = useDispatch(); @@ -18,6 +19,13 @@ export const AlertTls: React.FC<{}> = () => { [dispatch] ); const { settings } = useSelector(selectDynamicSettings); + + useEffect(() => { + if (typeof settings === 'undefined') { + dispatch(getDynamicSettings()); + } + }, [dispatch, settings]); + return ( { disabled={false} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > @@ -90,6 +91,7 @@ describe('AddFilterButton component', () => { disabled={false} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > @@ -143,6 +145,7 @@ describe('AddFilterButton component', () => { disabled={true} flush="left" iconType="plusInCircleFilled" + isLoading={false} onClick={[Function]} size="s" > diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx index 66f0f296b1248..58b8e7bb085da 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_status_alert/add_filter_btn.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui'; import * as labels from '../translations'; +import { useIndexPattern } from '../../../../contexts/uptime_index_pattern_context'; interface Props { newFilters: string[]; @@ -20,6 +21,8 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler const getSelectedItems = (fieldName: string) => alertFilters?.[fieldName] ?? []; + const indexPattern = useIndexPattern(); + const onButtonClick = () => { setPopover(!isPopoverOpen); }; @@ -62,6 +65,7 @@ export const AddFilterButton: React.FC = ({ newFilters, onNewFilter, aler onClick={onButtonClick} size="s" flush="left" + isLoading={!indexPattern} > {labels.ADD_FILTER} diff --git a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx index 8171f7e19865f..6c658ec7f5d40 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_index_pattern_context.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import React, { createContext, useContext, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { createContext, useContext } from 'react'; import { useFetcher } from '../../../observability/public'; import { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public'; -import { indexStatusSelector, selectDynamicSettings } from '../state/selectors'; -import { getDynamicSettings } from '../state/actions/dynamic_settings'; +import { useHasData } from '../components/overview/empty_state/use_has_data'; export const UptimeIndexPatternContext = createContext({} as IndexPattern); @@ -18,16 +16,7 @@ export const UptimeIndexPatternContextProvider: React.FC<{ data: DataPublicPlugi children, data: { indexPatterns }, }) => { - const { settings } = useSelector(selectDynamicSettings); - const { data: indexStatus } = useSelector(indexStatusSelector); - - const dispatch = useDispatch(); - - useEffect(() => { - if (typeof settings === 'undefined') { - dispatch(getDynamicSettings()); - } - }, [dispatch, settings]); + const { settings, data: indexStatus } = useHasData(); const heartbeatIndices = settings?.heartbeatIndices || ''; diff --git a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts index 024e387d23547..ffe7c61c7a4e3 100644 --- a/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts +++ b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { takeLatest, put, call } from 'redux-saga/effects'; +import { takeLeading, put, call, takeLatest } from 'redux-saga/effects'; import { Action } from 'redux-actions'; import { i18n } from '@kbn/i18n'; import { fetchEffectFactory } from './fetch_effect'; @@ -25,7 +25,7 @@ import { DynamicSettings } from '../../../common/runtime_types'; import { kibanaService } from '../kibana_service'; export function* fetchDynamicSettingsEffect() { - yield takeLatest( + yield takeLeading( String(getDynamicSettings), fetchEffectFactory(getDynamicSettingsAPI, getDynamicSettingsSuccess, getDynamicSettingsFail) ); diff --git a/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts index 5aa6b7ea7c5a9..5fc99816df006 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects/saved_objects.ts @@ -38,7 +38,7 @@ export const registerUptimeSavedObjects = ( }; export interface UMSavedObjectsAdapter { - config: UptimeConfig; + config: UptimeConfig | null; getUptimeDynamicSettings: UMSavedObjectsQueryFn; setUptimeDynamicSettings: UMSavedObjectsQueryFn; } diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts index f028d5e154a56..496f39557adb1 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.test.ts @@ -25,7 +25,10 @@ describe('getEsHostsTest', () => { it('should return expected host in cloud', function () { const esHosts = getEsHosts({ cloud: cloudSetup, - config: {}, + config: { + enabled: true, + manifestUrl: 'https://testing.com', + }, }); expect(esHosts).toEqual([ @@ -36,11 +39,9 @@ describe('getEsHostsTest', () => { it('should return expected host from config', function () { const esHosts = getEsHosts({ config: { - unsafe: { - service: { - hosts: ['http://localhost:9200'], - }, - }, + enabled: true, + manifestUrl: 'https://testing.com', + hosts: ['http://localhost:9200'], }, }); @@ -50,11 +51,9 @@ describe('getEsHostsTest', () => { const esHosts = getEsHosts({ cloud: cloudSetup, config: { - unsafe: { - service: { - hosts: ['http://localhost:9200'], - }, - }, + enabled: true, + manifestUrl: 'https://testing.com', + hosts: ['http://localhost:9200'], }, }); diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts index d0de73b73e23e..847fcfa9db834 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_es_hosts.ts @@ -14,14 +14,14 @@ import { CloudSetup } from '../../../../cloud/server'; import { decodeCloudId } from '../../../../fleet/common'; -import { UptimeConfig } from '../../../common/config'; +import { ServiceConfig } from '../../../common/config'; export function getEsHosts({ cloud, config, }: { cloud?: CloudSetup; - config: UptimeConfig; + config: ServiceConfig; }): string[] { const cloudId = cloud?.isCloudEnabled && cloud.cloudId; const cloudUrl = cloudId && decodeCloudId(cloudId)?.elasticsearchUrl; @@ -30,7 +30,7 @@ export function getEsHosts({ return cloudHosts; } - const flagHosts = config?.unsafe?.service?.hosts; + const flagHosts = config.hosts; if (flagHosts && flagHosts.length > 0) { return flagHosts; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts index 736e73da71134..d515e394cab7e 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/service_api_client.ts @@ -8,10 +8,13 @@ import axios from 'axios'; import { forkJoin, from as rxjsFrom, Observable, of } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; +import * as https from 'https'; +import { SslConfig } from '@kbn/server-http-tools'; import { getServiceLocations } from './get_service_locations'; import { Logger } from '../../../../../../src/core/server'; import { MonitorFields, ServiceLocations } from '../../../common/runtime_types'; import { convertToDataStreamFormat } from './formatters/convert_to_data_stream'; +import { ServiceConfig } from '../../../common/config'; const TEST_SERVICE_USERNAME = 'localKibanaIntegrationTestsUser'; @@ -24,14 +27,23 @@ export interface ServiceData { } export class ServiceAPIClient { - private readonly username: string; + private readonly username?: string; private readonly authorization: string; private locations: ServiceLocations; private logger: Logger; + private readonly config: ServiceConfig; - constructor(manifestUrl: string, username: string, password: string, logger: Logger) { + constructor(logger: Logger, config: ServiceConfig) { + this.config = config; + const { username, password, manifestUrl } = config; this.username = username; - this.authorization = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + + if (username && password) { + this.authorization = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + } else { + this.authorization = ''; + } + this.logger = logger; this.locations = []; @@ -40,6 +52,19 @@ export class ServiceAPIClient { }); } + getHttpsAgent() { + const config = this.config; + if (config.tls && config.tls.certificate && config.tls.key) { + const tlsConfig = new SslConfig(config.tls); + + return new https.Agent({ + rejectUnauthorized: true, // (NOTE: this will disable client verification) + cert: tlsConfig.certificate, + key: tlsConfig.key, + }); + } + } + async post(data: ServiceData) { return this.callAPI('POST', data); } @@ -68,9 +93,12 @@ export class ServiceAPIClient { method, url: url + '/monitors', data: { monitors: monitorsStreams, output }, - headers: { - Authorization: this.authorization, - }, + headers: this.authorization + ? { + Authorization: this.authorization, + } + : undefined, + httpsAgent: this.getHttpsAgent(), }); }; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index 82a901192b0ee..e25535df08baf 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -20,7 +20,7 @@ import { SyntheticsServiceApiKey } from '../../../common/runtime_types/synthetic import { getAPIKeyForSyntheticsService } from './get_api_key'; import { syntheticsMonitorType } from '../saved_objects/synthetics_monitor'; import { getEsHosts } from './get_es_hosts'; -import { UptimeConfig } from '../../../common/config'; +import { ServiceConfig } from '../../../common/config'; import { ServiceAPIClient } from './service_api_client'; import { formatMonitorConfig } from './formatters/format_configs'; import { @@ -40,19 +40,17 @@ export class SyntheticsService { private readonly server: UptimeServerSetup; private apiClient: ServiceAPIClient; - private readonly config: UptimeConfig; + private readonly config: ServiceConfig; private readonly esHosts: string[]; private apiKey: SyntheticsServiceApiKey | undefined; - constructor(logger: Logger, server: UptimeServerSetup) { + constructor(logger: Logger, server: UptimeServerSetup, config: ServiceConfig) { this.logger = logger; this.server = server; - this.config = server.config; + this.config = config; - const { manifestUrl, username, password } = this.config.unsafe.service; - - this.apiClient = new ServiceAPIClient(manifestUrl, username, password, logger); + this.apiClient = new ServiceAPIClient(logger, this.config); this.esHosts = getEsHosts({ config: this.config, cloud: server.cloud }); } @@ -116,8 +114,7 @@ export class SyntheticsService { public async scheduleSyncTask( taskManager: TaskManagerStartContract ): Promise { - const interval = - this.config.unsafe.service.syncInterval ?? SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; + const interval = this.config.syncInterval ?? SYNTHETICS_SERVICE_SYNC_INTERVAL_DEFAULT; try { await taskManager.removeIfExists(SYNTHETICS_SERVICE_SYNC_MONITORS_TASK_ID); diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 692607041ea80..4c076db0255ef 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { PluginInitializerContext, CoreStart, @@ -75,7 +74,12 @@ export class Plugin implements PluginType { } as UptimeServerSetup; if (this.server?.config?.unsafe?.service.enabled) { - this.syntheticService = new SyntheticsService(this.logger, this.server); + this.syntheticService = new SyntheticsService( + this.logger, + this.server, + this.server.config.unsafe.service + ); + this.syntheticService.registerSyncTask(plugins.taskManager); } @@ -111,7 +115,7 @@ export class Plugin implements PluginType { this.server.savedObjectsClient = this.savedObjectsClient; } - if (this.server?.config?.unsafe?.service.enabled) { + if (this.server?.config?.unsafe?.service?.enabled) { this.syntheticService?.init(); this.syntheticService?.scheduleSyncTask(plugins.taskManager); if (this.server && this.syntheticService) { diff --git a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts index ecf95c7e9175a..dfd0dcd1a9107 100644 --- a/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/synthetics_service/get_service_locations.ts @@ -14,5 +14,5 @@ export const getServiceLocationsRoute: UMRestApiRouteFactory = () => ({ path: API_URLS.SERVICE_LOCATIONS, validate: {}, handler: async ({ server }): Promise => - getServiceLocations({ manifestUrl: server.config.unsafe.service.manifestUrl }), + getServiceLocations({ manifestUrl: server.config.unsafe!.service.manifestUrl }), }); diff --git a/x-pack/test/functional/apps/canvas/filters.ts b/x-pack/test/functional/apps/canvas/filters.ts index e5b97fa2350f1..ce8b319b9d53f 100644 --- a/x-pack/test/functional/apps/canvas/filters.ts +++ b/x-pack/test/functional/apps/canvas/filters.ts @@ -43,16 +43,18 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro // Double check that the filter has the correct time range and default filter value const startingMatchFilters = await PageObjects.canvas.getMatchFiltersFromDebug(); - expect(startingMatchFilters[0].value).to.equal('apm'); - expect(startingMatchFilters[0].column).to.equal('project'); + const projectQuery = startingMatchFilters[0].query.term.project; + expect(projectQuery !== null && typeof projectQuery === 'object').to.equal(true); + expect(projectQuery?.value).to.equal('apm'); // Change dropdown value await testSubjects.selectValue('canvasDropdownFilter__select', 'beats'); await retry.try(async () => { const matchFilters = await PageObjects.canvas.getMatchFiltersFromDebug(); - expect(matchFilters[0].value).to.equal('beats'); - expect(matchFilters[0].column).to.equal('project'); + const newProjectQuery = matchFilters[0].query.term.project; + expect(newProjectQuery !== null && typeof newProjectQuery === 'object').to.equal(true); + expect(newProjectQuery?.value).to.equal('beats'); }); }); @@ -66,18 +68,20 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro }); const startingTimeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(startingTimeFilters[0].column).to.equal('@timestamp'); - expect(new Date(startingTimeFilters[0].from).toDateString()).to.equal('Sun Oct 18 2020'); - expect(new Date(startingTimeFilters[0].to).toDateString()).to.equal('Sat Oct 24 2020'); + const timestampQuery = startingTimeFilters[0].query.range['@timestamp']; + expect(timestampQuery !== null && typeof timestampQuery === 'object').to.equal(true); + expect(new Date(timestampQuery.gte).toDateString()).to.equal('Sun Oct 18 2020'); + expect(new Date(timestampQuery.lte).toDateString()).to.equal('Sat Oct 24 2020'); await testSubjects.click('superDatePickerstartDatePopoverButton'); await find.clickByCssSelector('.react-datepicker [aria-label="day-19"]', 20000); await retry.try(async () => { const timeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(timeFilters[0].column).to.equal('@timestamp'); - expect(new Date(timeFilters[0].from).toDateString()).to.equal('Mon Oct 19 2020'); - expect(new Date(timeFilters[0].to).toDateString()).to.equal('Sat Oct 24 2020'); + const newTimestampQuery = timeFilters[0].query.range['@timestamp']; + expect(newTimestampQuery !== null && typeof newTimestampQuery === 'object').to.equal(true); + expect(new Date(newTimestampQuery.gte).toDateString()).to.equal('Mon Oct 19 2020'); + expect(new Date(newTimestampQuery.lte).toDateString()).to.equal('Sat Oct 24 2020'); }); await testSubjects.click('superDatePickerendDatePopoverButton'); @@ -85,9 +89,10 @@ export default function canvasFiltersTest({ getService, getPageObjects }: FtrPro await retry.try(async () => { const timeFilters = await PageObjects.canvas.getTimeFiltersFromDebug(); - expect(timeFilters[0].column).to.equal('@timestamp'); - expect(new Date(timeFilters[0].from).toDateString()).to.equal('Mon Oct 19 2020'); - expect(new Date(timeFilters[0].to).toDateString()).to.equal('Fri Oct 23 2020'); + const newTimestampQuery = timeFilters[0].query.range['@timestamp']; + expect(newTimestampQuery !== null && typeof newTimestampQuery === 'object').to.equal(true); + expect(new Date(newTimestampQuery.gte).toDateString()).to.equal('Mon Oct 19 2020'); + expect(new Date(newTimestampQuery.lte).toDateString()).to.equal('Fri Oct 23 2020'); }); }); }); diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index 2b570a4d7dae6..a51b878b6af30 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -108,7 +108,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo const filters = JSON.parse(content); - return filters.and.filter((f: any) => f.filterType === 'time'); + return filters.filters.filter((f: any) => f.query?.range); }, async getMatchFiltersFromDebug() { @@ -119,7 +119,7 @@ export function CanvasPageProvider({ getService, getPageObjects }: FtrProviderCo const filters = JSON.parse(content); - return filters.and.filter((f: any) => f.filterType === 'exactly'); + return filters.filters.filter((f: any) => f.query?.term); }, async clickAddFromLibrary() {