diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx
index 4e1754638d479..e3cc396783628 100644
--- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx
+++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx
@@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
export function LoadingSpinner() {
return (
- <>
+
@@ -30,6 +30,6 @@ export function LoadingSpinner() {
- >
+
);
}
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index fdb14b3f1f63e..27844cc2347b9 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -37,7 +37,6 @@ import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/publi
import { SharePluginStart } from 'src/plugins/share/public';
import { ChartsPluginStart } from 'src/plugins/charts/public';
import { VisualizationsStart } from 'src/plugins/visualizations/public';
-import { SavedObjectKibanaServices } from 'src/plugins/saved_objects/public';
import { DiscoverStartPlugins } from './plugin';
import { createSavedSearchesLoader, SavedSearch } from './saved_searches';
@@ -78,12 +77,9 @@ export async function buildServices(
context: PluginInitializerContext,
getEmbeddableInjector: any
): Promise
{
- const services: SavedObjectKibanaServices = {
+ const services = {
savedObjectsClient: core.savedObjects.client,
- indexPatterns: plugins.data.indexPatterns,
- search: plugins.data.search,
- chrome: core.chrome,
- overlays: core.overlays,
+ savedObjects: plugins.savedObjects,
};
const savedObjectService = createSavedSearchesLoader(services);
diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts
index b1bbc89b62d9d..11ec4f08d9514 100644
--- a/src/plugins/discover/public/plugin.ts
+++ b/src/plugins/discover/public/plugin.ts
@@ -41,7 +41,7 @@ import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwardi
import { HomePublicPluginSetup } from 'src/plugins/home/public';
import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public';
import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public';
-import { SavedObjectLoader } from '../../saved_objects/public';
+import { SavedObjectLoader, SavedObjectsStart } from '../../saved_objects/public';
import { createKbnUrlTracker } from '../../kibana_utils/public';
import { DEFAULT_APP_CATEGORIES } from '../../../core/public';
import { UrlGeneratorState } from '../../share/public';
@@ -141,6 +141,7 @@ export interface DiscoverStartPlugins {
urlForwarding: UrlForwardingStart;
inspector: InspectorPublicPluginStart;
visualizations: VisualizationsStart;
+ savedObjects: SavedObjectsStart;
}
const innerAngularName = 'app/discover';
@@ -351,10 +352,7 @@ export class DiscoverPlugin
urlGenerator: this.urlGenerator,
savedSearchLoader: createSavedSearchesLoader({
savedObjectsClient: core.savedObjects.client,
- indexPatterns: plugins.data.indexPatterns,
- search: plugins.data.search,
- chrome: core.chrome,
- overlays: core.overlays,
+ savedObjects: plugins.savedObjects,
}),
};
}
diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts
index 2b8574a8fa118..1ec4549f05d49 100644
--- a/src/plugins/discover/public/saved_searches/_saved_search.ts
+++ b/src/plugins/discover/public/saved_searches/_saved_search.ts
@@ -16,16 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {
- createSavedObjectClass,
- SavedObject,
- SavedObjectKibanaServices,
-} from '../../../saved_objects/public';
+import { SavedObject, SavedObjectsStart } from '../../../saved_objects/public';
-export function createSavedSearchClass(services: SavedObjectKibanaServices) {
- const SavedObjectClass = createSavedObjectClass(services);
-
- class SavedSearch extends SavedObjectClass {
+export function createSavedSearchClass(savedObjects: SavedObjectsStart) {
+ class SavedSearch extends savedObjects.SavedObjectClass {
public static type: string = 'search';
public static mapping = {
title: 'text',
@@ -70,5 +64,5 @@ export function createSavedSearchClass(services: SavedObjectKibanaServices) {
}
}
- return SavedSearch as new (id: string) => SavedObject;
+ return (SavedSearch as unknown) as new (id: string) => SavedObject;
}
diff --git a/src/plugins/discover/public/saved_searches/saved_searches.ts b/src/plugins/discover/public/saved_searches/saved_searches.ts
index 0bc332ed8ec74..fd7a185f7012f 100644
--- a/src/plugins/discover/public/saved_searches/saved_searches.ts
+++ b/src/plugins/discover/public/saved_searches/saved_searches.ts
@@ -17,12 +17,18 @@
* under the License.
*/
-import { SavedObjectLoader, SavedObjectKibanaServices } from '../../../saved_objects/public';
+import { SavedObjectsClientContract } from 'kibana/public';
+import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public';
import { createSavedSearchClass } from './_saved_search';
-export function createSavedSearchesLoader(services: SavedObjectKibanaServices) {
- const SavedSearchClass = createSavedSearchClass(services);
- const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient);
+interface Services {
+ savedObjectsClient: SavedObjectsClientContract;
+ savedObjects: SavedObjectsStart;
+}
+
+export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: Services) {
+ const SavedSearchClass = createSavedSearchClass(savedObjects);
+ const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, savedObjectsClient);
// Customize loader properties since adding an 's' on type doesn't work for type 'search' .
savedSearchLoader.loaderProperties = {
name: 'searches',
diff --git a/src/plugins/expressions/.eslintrc.json b/src/plugins/expressions/.eslintrc.json
new file mode 100644
index 0000000000000..2aab6c2d9093b
--- /dev/null
+++ b/src/plugins/expressions/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "@typescript-eslint/consistent-type-definitions": 0
+ }
+}
diff --git a/src/plugins/expressions/common/ast/types.ts b/src/plugins/expressions/common/ast/types.ts
index 09fb4fae3f201..e8cf497774569 100644
--- a/src/plugins/expressions/common/ast/types.ts
+++ b/src/plugins/expressions/common/ast/types.ts
@@ -24,12 +24,12 @@ export type ExpressionAstNode =
| ExpressionAstFunction
| ExpressionAstArgument;
-export interface ExpressionAstExpression {
+export type ExpressionAstExpression = {
type: 'expression';
chain: ExpressionAstFunction[];
-}
+};
-export interface ExpressionAstFunction {
+export type ExpressionAstFunction = {
type: 'function';
function: string;
arguments: Record;
@@ -38,9 +38,9 @@ export interface ExpressionAstFunction {
* Debug information added to each function when expression is executed in *debug mode*.
*/
debug?: ExpressionAstFunctionDebug;
-}
+};
-export interface ExpressionAstFunctionDebug {
+export type ExpressionAstFunctionDebug = {
/**
* True if function successfully returned output, false if function threw.
*/
@@ -83,6 +83,6 @@ export interface ExpressionAstFunctionDebug {
* timing starts after the arguments have been resolved.
*/
duration: number | undefined;
-}
+};
export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression;
diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts
index d4c9b0a25d45b..69140453f486d 100644
--- a/src/plugins/expressions/common/execution/execution.ts
+++ b/src/plugins/expressions/common/execution/execution.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import { i18n } from '@kbn/i18n';
import { keys, last, mapValues, reduce, zipObject } from 'lodash';
import { Executor, ExpressionExecOptions } from '../executor';
import { createExecutionContainer, ExecutionContainer } from './container';
@@ -217,7 +218,27 @@ export class Execution<
const fn = getByAlias(this.state.get().functions, fnName);
if (!fn) {
- return createError({ message: `Function ${fnName} could not be found.` });
+ return createError({
+ name: 'fn not found',
+ message: i18n.translate('expressions.execution.functionNotFound', {
+ defaultMessage: `Function {fnName} could not be found.`,
+ values: {
+ fnName,
+ },
+ }),
+ });
+ }
+
+ if (fn.disabled) {
+ return createError({
+ name: 'fn is disabled',
+ message: i18n.translate('expressions.execution.functionDisabled', {
+ defaultMessage: `Function {fnName} is disabled.`,
+ values: {
+ fnName,
+ },
+ }),
+ });
}
let args: Record = {};
diff --git a/src/plugins/expressions/common/execution/execution_contract.ts b/src/plugins/expressions/common/execution/execution_contract.ts
index 20c5b2dd434b5..79bb4c58ab48d 100644
--- a/src/plugins/expressions/common/execution/execution_contract.ts
+++ b/src/plugins/expressions/common/execution/execution_contract.ts
@@ -62,7 +62,7 @@ export class ExecutionContract<
return {
type: 'error',
error: {
- type: e.type,
+ name: e.name,
message: e.message,
stack: e.stack,
},
diff --git a/src/plugins/expressions/common/executor/executor.test.ts b/src/plugins/expressions/common/executor/executor.test.ts
index 81845401d32e4..a658d3457407c 100644
--- a/src/plugins/expressions/common/executor/executor.test.ts
+++ b/src/plugins/expressions/common/executor/executor.test.ts
@@ -21,7 +21,7 @@ import { Executor } from './executor';
import * as expressionTypes from '../expression_types';
import * as expressionFunctions from '../expression_functions';
import { Execution } from '../execution';
-import { parseExpression } from '../ast';
+import { ExpressionAstFunction, parseExpression } from '../ast';
describe('Executor', () => {
test('can instantiate', () => {
@@ -152,4 +152,47 @@ describe('Executor', () => {
});
});
});
+
+ describe('.inject', () => {
+ const executor = new Executor();
+
+ const injectFn = jest.fn().mockImplementation((args, references) => args);
+ const extractFn = jest.fn().mockReturnValue({ args: {}, references: [] });
+
+ const fooFn = {
+ name: 'foo',
+ help: 'test',
+ args: {
+ bar: {
+ types: ['string'],
+ help: 'test',
+ },
+ },
+ extract: (state: ExpressionAstFunction['arguments']) => {
+ return extractFn(state);
+ },
+ inject: (state: ExpressionAstFunction['arguments']) => {
+ return injectFn(state);
+ },
+ fn: jest.fn(),
+ };
+ executor.registerFunction(fooFn);
+
+ test('calls inject function for every expression function in expression', () => {
+ executor.inject(
+ parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}'),
+ []
+ );
+ expect(injectFn).toBeCalledTimes(5);
+ });
+
+ describe('.extract', () => {
+ test('calls extract function for every expression function in expression', () => {
+ executor.extract(
+ parseExpression('foo bar="baz" | foo bar={foo bar="baz" | foo bar={foo bar="baz"}}')
+ );
+ expect(extractFn).toBeCalledTimes(5);
+ });
+ });
+ });
});
diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts
index 2b5f9f2556d89..28aae8c8f4834 100644
--- a/src/plugins/expressions/common/executor/executor.ts
+++ b/src/plugins/expressions/common/executor/executor.ts
@@ -19,6 +19,7 @@
/* eslint-disable max-classes-per-file */
+import { cloneDeep, mapValues } from 'lodash';
import { ExecutorState, ExecutorContainer } from './container';
import { createExecutorContainer } from './container';
import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions';
@@ -26,9 +27,12 @@ import { Execution, ExecutionParams } from '../execution/execution';
import { IRegistry } from '../types';
import { ExpressionType } from '../expression_types/expression_type';
import { AnyExpressionTypeDefinition } from '../expression_types/types';
-import { ExpressionAstExpression } from '../ast';
+import { ExpressionAstExpression, ExpressionAstFunction } from '../ast';
import { typeSpecs } from '../expression_types/specs';
import { functionSpecs } from '../expression_functions/specs';
+import { getByAlias } from '../util';
+import { SavedObjectReference } from '../../../../core/types';
+import { PersistableState } from '../../../kibana_utils/common';
export interface ExpressionExecOptions {
/**
@@ -83,7 +87,8 @@ export class FunctionsRegistry implements IRegistry {
}
}
-export class Executor = Record> {
+export class Executor = Record>
+ implements PersistableState {
static createWithDefaults = Record>(
state?: ExecutorState
): Executor {
@@ -197,6 +202,56 @@ export class Executor = Record void
+ ) {
+ for (const link of ast.chain) {
+ const { function: fnName, arguments: fnArgs } = link;
+ const fn = getByAlias(this.state.get().functions, fnName);
+
+ if (fn) {
+ // if any of arguments are expressions we should migrate those first
+ link.arguments = mapValues(fnArgs, (asts, argName) => {
+ return asts.map((arg) => {
+ if (typeof arg === 'object') {
+ return this.walkAst(arg, action);
+ }
+ return arg;
+ });
+ });
+
+ action(fn, link);
+ }
+ }
+
+ return ast;
+ }
+
+ public inject(ast: ExpressionAstExpression, references: SavedObjectReference[]) {
+ return this.walkAst(cloneDeep(ast), (fn, link) => {
+ link.arguments = fn.inject(link.arguments, references);
+ });
+ }
+
+ public extract(ast: ExpressionAstExpression) {
+ const allReferences: SavedObjectReference[] = [];
+ const newAst = this.walkAst(cloneDeep(ast), (fn, link) => {
+ const { state, references } = fn.extract(link.arguments);
+ link.arguments = state;
+ allReferences.push(...references);
+ });
+ return { state: newAst, references: allReferences };
+ }
+
+ public telemetry(ast: ExpressionAstExpression, telemetryData: Record) {
+ this.walkAst(cloneDeep(ast), (fn, link) => {
+ telemetryData = fn.telemetry(link.arguments, telemetryData);
+ });
+
+ return telemetryData;
+ }
+
public fork(): Executor {
const initialState = this.state.get();
const fork = new Executor(initialState);
diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts
index 71f0d91510136..0b56d3c169ff4 100644
--- a/src/plugins/expressions/common/expression_functions/expression_function.ts
+++ b/src/plugins/expressions/common/expression_functions/expression_function.ts
@@ -17,12 +17,16 @@
* under the License.
*/
+import { identity } from 'lodash';
import { AnyExpressionFunctionDefinition } from './types';
import { ExpressionFunctionParameter } from './expression_function_parameter';
import { ExpressionValue } from '../expression_types/types';
import { ExecutionContext } from '../execution';
+import { ExpressionAstFunction } from '../ast';
+import { SavedObjectReference } from '../../../../core/types';
+import { PersistableState } from '../../../kibana_utils/common';
-export class ExpressionFunction {
+export class ExpressionFunction implements PersistableState {
/**
* Name of function
*/
@@ -60,8 +64,34 @@ export class ExpressionFunction {
*/
inputTypes: string[] | undefined;
+ disabled: boolean;
+ telemetry: (
+ state: ExpressionAstFunction['arguments'],
+ telemetryData: Record
+ ) => Record;
+ extract: (
+ state: ExpressionAstFunction['arguments']
+ ) => { state: ExpressionAstFunction['arguments']; references: SavedObjectReference[] };
+ inject: (
+ state: ExpressionAstFunction['arguments'],
+ references: SavedObjectReference[]
+ ) => ExpressionAstFunction['arguments'];
+
constructor(functionDefinition: AnyExpressionFunctionDefinition) {
- const { name, type, aliases, fn, help, args, inputTypes, context } = functionDefinition;
+ const {
+ name,
+ type,
+ aliases,
+ fn,
+ help,
+ args,
+ inputTypes,
+ context,
+ disabled,
+ telemetry,
+ inject,
+ extract,
+ } = functionDefinition;
this.name = name;
this.type = type;
@@ -70,6 +100,10 @@ export class ExpressionFunction {
Promise.resolve(fn(input, params, handlers as ExecutionContext));
this.help = help || '';
this.inputTypes = inputTypes || context?.types;
+ this.disabled = disabled || false;
+ this.telemetry = telemetry || ((s, c) => c);
+ this.inject = inject || identity;
+ this.extract = extract || ((s) => ({ state: s, references: [] }));
for (const [key, arg] of Object.entries(args || {})) {
this.args[key] = new ExpressionFunctionParameter(key, arg);
diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts
index d58d872aff722..caaef541aefd5 100644
--- a/src/plugins/expressions/common/expression_functions/types.ts
+++ b/src/plugins/expressions/common/expression_functions/types.ts
@@ -30,6 +30,8 @@ import {
ExpressionFunctionVar,
ExpressionFunctionTheme,
} from './specs';
+import { ExpressionAstFunction } from '../ast';
+import { PersistableStateDefinition } from '../../../kibana_utils/common';
/**
* `ExpressionFunctionDefinition` is the interface plugins have to implement to
@@ -41,12 +43,17 @@ export interface ExpressionFunctionDefinition<
Arguments extends Record,
Output,
Context extends ExecutionContext = ExecutionContext
-> {
+> extends PersistableStateDefinition {
/**
* The name of the function, as will be used in expression.
*/
name: Name;
+ /**
+ * if set to true function will be disabled (but its migrate function will still be available)
+ */
+ disabled?: boolean;
+
/**
* Name of type of value this function outputs.
*/
diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts
index ebaedcbba0d23..7607945d8a664 100644
--- a/src/plugins/expressions/common/expression_types/specs/error.ts
+++ b/src/plugins/expressions/common/expression_types/specs/error.ts
@@ -20,20 +20,16 @@
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
import { ExpressionValueRender } from './render';
import { getType } from '../get_type';
+import { SerializableState } from '../../../../kibana_utils/common';
+import { ErrorLike } from '../../util';
const name = 'error';
export type ExpressionValueError = ExpressionValueBoxed<
'error',
{
- error: {
- message: string;
- type?: string;
- name?: string;
- stack?: string;
- original?: Error;
- };
- info?: unknown;
+ error: ErrorLike;
+ info?: SerializableState;
}
>;
diff --git a/src/plugins/expressions/common/expression_types/specs/range.ts b/src/plugins/expressions/common/expression_types/specs/range.ts
index 3d7170cf715d7..53fd4894fd2be 100644
--- a/src/plugins/expressions/common/expression_types/specs/range.ts
+++ b/src/plugins/expressions/common/expression_types/specs/range.ts
@@ -26,6 +26,7 @@ export interface Range {
type: typeof name;
from: number;
to: number;
+ label?: string;
}
export const range: ExpressionTypeDefinition = {
@@ -41,7 +42,7 @@ export const range: ExpressionTypeDefinition = {
},
to: {
render: (value: Range): ExpressionValueRender<{ text: string }> => {
- const text = `from ${value.from} to ${value.to}`;
+ const text = value?.label || `from ${value.from} to ${value.to}`;
return {
type: 'render',
as: 'text',
diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts
index b5c98fada07c4..4a87fd9e7f331 100644
--- a/src/plugins/expressions/common/service/expressions_services.ts
+++ b/src/plugins/expressions/common/service/expressions_services.ts
@@ -23,6 +23,8 @@ import { ExpressionAstExpression } from '../ast';
import { ExecutionContract } from '../execution/execution_contract';
import { AnyExpressionTypeDefinition } from '../expression_types';
import { AnyExpressionFunctionDefinition } from '../expression_functions';
+import { SavedObjectReference } from '../../../../core/types';
+import { PersistableState } from '../../../kibana_utils/common';
/**
* The public contract that `ExpressionsService` provides to other plugins
@@ -154,7 +156,7 @@ export interface ExpressionServiceParams {
*
* so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`.
*/
-export class ExpressionsService {
+export class ExpressionsService implements PersistableState {
public readonly executor: Executor;
public readonly renderers: ExpressionRendererRegistry;
@@ -256,6 +258,36 @@ export class ExpressionsService {
return fork;
};
+ /**
+ * Extracts telemetry from expression AST
+ * @param state expression AST to extract references from
+ */
+ public readonly telemetry = (
+ state: ExpressionAstExpression,
+ telemetryData: Record = {}
+ ) => {
+ return this.executor.telemetry(state, telemetryData);
+ };
+
+ /**
+ * Extracts saved object references from expression AST
+ * @param state expression AST to extract references from
+ * @returns new expression AST with references removed and array of references
+ */
+ public readonly extract = (state: ExpressionAstExpression) => {
+ return this.executor.extract(state);
+ };
+
+ /**
+ * Injects saved object references into expression AST
+ * @param state expression AST to update
+ * @param references array of saved object references
+ * @returns new expression AST with references injected
+ */
+ public readonly inject = (state: ExpressionAstExpression, references: SavedObjectReference[]) => {
+ return this.executor.inject(state, references);
+ };
+
/**
* Returns Kibana Platform *setup* life-cycle contract. Useful to return the
* same contract on server-side and browser-side.
diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts
index 9bdab74efd6f9..293afd46d4de5 100644
--- a/src/plugins/expressions/common/util/create_error.ts
+++ b/src/plugins/expressions/common/util/create_error.ts
@@ -19,9 +19,17 @@
import { ExpressionValueError } from '../../common';
-type ErrorLike = Partial>;
+export type SerializedError = {
+ name: string;
+ message: string;
+ stack?: string;
+};
-export const createError = (err: string | Error | ErrorLike): ExpressionValueError => ({
+export type ErrorLike = SerializedError & {
+ original?: SerializedError;
+};
+
+export const createError = (err: string | ErrorLike): ExpressionValueError => ({
type: 'error',
error: {
stack:
@@ -32,6 +40,11 @@ export const createError = (err: string | Error | ErrorLike): ExpressionValueErr
: undefined,
message: typeof err === 'string' ? err : String(err.message),
name: typeof err === 'object' ? err.name || 'Error' : 'Error',
- original: err instanceof Error ? err : undefined,
+ original:
+ err instanceof Error
+ ? err
+ : typeof err === 'object' && 'original' in err && err.original instanceof Error
+ ? err.original
+ : undefined,
},
});
diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md
index 763147df6d922..c7b6190b96ed7 100644
--- a/src/plugins/expressions/public/public.api.md
+++ b/src/plugins/expressions/public/public.api.md
@@ -189,10 +189,11 @@ export interface ExecutionState