-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expression executor service #36885
Expression executor service #36885
Conversation
Pinging @elastic/kibana-app-arch |
💚 Build Succeeded |
💚 Build Succeeded |
💚 Build Succeeded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some initial comments, will take a deeper look tomorrow
src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx
Outdated
Show resolved
Hide resolved
src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx
Outdated
Show resolved
Hide resolved
} | ||
|
||
public setup() { | ||
return { | ||
indexPatterns: this.indexPatterns.setup(), | ||
search: this.search.setup(), | ||
query: this.query.setup(), | ||
expressionExecutor: this.expressionExecutor.setup(null, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets name it interpreter service
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good - this means we will merge it with the current interpreter plugin, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also be in favor of simply calling it expressions
instead, since it will also house stuff that isn't explicitly related to the interpreter. (e.g. the fromExpression
and toExpression
methods are technically more related to the parser than the interpreter)
Then usage could look like this
data.expressions.toExpression(myAst);
data.expressions.fromExpression(myString);
data.expressions.interpreter.run(myExpression);
(or maybe data.expressions.interpret(myExpression) ?)
data.expressions.interpreter.getInterpreter();
data.expressions.interpreter.getRegistries(); // or whatever
I'm not against calling it interpreter
service either, I'm just thinking about what would be the easiest thing to explain to people and most understandable for a dev.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
++
data.expressions.fromAst()
and data.expression.toAst()
probably make more sense then
data.expressions.run()
is also fine and data.expressions.ExpressionRenderer
component
it would be nice if we could have data.expressions.interpreter
instead of data.expressions.getInterpreter()
and then the registries:
data.expressions.functionRegistry
, data.expressions.renderersRegistry
, data.expressions.typesRegistry
|
||
import { CoreSetup } from 'kibana/public'; | ||
import { useRef, useEffect } from 'react'; | ||
import React from 'react'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react shouldn't be part of the API, we can have react components wrapping the api but pls put in separate file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we don't do stateful components at all? Or is this just about the React import in the service itself?
The nice thing about delivering components like that is that they are usable directly (<ExpressionRenderer expression="..." />
) instead of having to pass the stateful thing in (e.g. <ExpressionRenderer runFn={plugins.expressionExecutor.run} expression="..." />
)
We can postpone this and I just remove the react component for now - there is already a pretty similar component in Lens which can handle the integration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah agreed with @ppisljar here -- I'd suggest providing a run
function to just run the expression, and a in a separate file a "convenience" method for React, which uses run
under the hood.
Then both can be returned from setup
as part of the public contract. This way people have the option of using the React wrapper or the run
method directly. Also some people might want to run an expression and get the output without having to use a renderer.
💔 Build Failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't tested, but did a pass through the code and added some ideas.
} | ||
|
||
public setup() { | ||
return { | ||
indexPatterns: this.indexPatterns.setup(), | ||
search: this.search.setup(), | ||
query: this.query.setup(), | ||
expressionExecutor: this.expressionExecutor.setup(null, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also be in favor of simply calling it expressions
instead, since it will also house stuff that isn't explicitly related to the interpreter. (e.g. the fromExpression
and toExpression
methods are technically more related to the parser than the interpreter)
Then usage could look like this
data.expressions.toExpression(myAst);
data.expressions.fromExpression(myString);
data.expressions.interpreter.run(myExpression);
(or maybe data.expressions.interpret(myExpression) ?)
data.expressions.interpreter.getInterpreter();
data.expressions.interpreter.getRegistries(); // or whatever
I'm not against calling it interpreter
service either, I'm just thinking about what would be the easiest thing to explain to people and most understandable for a dev.
search: SearchSetup; | ||
query: QuerySetup; | ||
} | ||
|
||
export { ExpressionExecutorSetup } from './expression_executor'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this needs to be exported directly since downstream consumers will just need the DataSetup
interface, unless there was a reason you needed it by itself @flash1293?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't necessarily need it by itself, I just planned to save the instance of this service in my consumer plugin like private expressionsService: ExpressionsServiceSetup;
.
If this isn't exported directly I have to plug it out of the DataSetup like private expressionsService: DataSetup['expressionsService'];
.
But that would also work, I very happy to follow best practices here. Reading your other comments, these best practices are exporting only the leaf types which are actually passed around, so I will change it to that (the react component and its props plus the signature of the "raw" run function).
@@ -25,15 +25,15 @@ import { getInterpreter } from 'plugins/interpreter/interpreter'; | |||
import { Adapters } from 'ui/inspector'; | |||
import { Filters, Query, TimeRange } from 'ui/visualize'; | |||
|
|||
interface InitialContextObject { | |||
export interface InitialContextObject { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are these being consumed? Are you exporting so that you can get access to them from lens directly?
I'm working on moving expression types to OSS so there might be some overlap here... initially I will expose these types from the interpreter plugin, but that will ultimately get moved to data
as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are used in the expressions service inside data
to help with the local interim types there because atm there are not other types available. +1 for more complete expression types. As soon as your PR is merged I will fix the type references in the expressions service.
onDestroy: (fn: () => void) => void; | ||
} | ||
|
||
export interface RenderFunction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these just exported for the purposes of testing, or is the intent to allow them to be imported by downstream consumers?
If we want them to be imported downstream, the convention we & core have been using is to re-export the types from the top-level plugin definition, so that people don't need to know the path to the directory to import from.
e.g.
import { data } from 'plugins/data';
// we want to avoid this as it makes your directory structure part of your public contract
import { RenderFunction } from 'plugins/data/expression_executor';
// so this is how we've been solving the issue so far
import { data, RenderFunction } from 'plugins/data';
// (the platform team has also discussed eventually namespacing any types by service if it becomes confusing. e.g. Expressions.RenderFunction)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks. These types are only exported for the tests, I changed my code to re-export public stuff in index
export class ExpressionExecutorService { | ||
private interpreterInstance: Interpreter | null = null; | ||
|
||
// TODO core won't ever be null once this is switched to the new platform |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If _core
isn't actually needed as part of this service, I wonder if we should just be passing it into setup
as explicitly null
or undefined
? That way you know at a glance that the expression executor doesn't rely on any core services, without digging into the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a misunderstanding from my side, sorry. I thought the individual services were planned to become plugins in the future because of the very similar call signature. I removed the core parameter completely, this also should make clear the service isn't depending on it
|
||
import { CoreSetup } from 'kibana/public'; | ||
import { useRef, useEffect } from 'react'; | ||
import React from 'react'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah agreed with @ppisljar here -- I'd suggest providing a run
function to just run the expression, and a in a separate file a "convenience" method for React, which uses run
under the hood.
Then both can be returned from setup
as part of the public contract. This way people have the option of using the React wrapper or the run
method directly. Also some people might want to run an expression and get the output without having to use a renderer.
@ppisljar @lukeelmers Thanks for your review, I think I addressed all the points. About naming: I renamed the service to "ExpressionsService", the functions are still called |
interpreter: { renderersRegistry, getInterpreter }, | ||
}: ExpressionsServiceDependencies) { | ||
const run = createRunFn( | ||
renderersRegistry, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should also expose interpreter
and renderersRegistry
but no need to do it in this PR.
ast, | ||
{ type: 'null' }, | ||
{ | ||
getInitialContext: () => ({}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be one of the properties to the function, whoever runs the expression might want to provide initial context.
typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; | ||
const response = await interpreter.interpretAst( | ||
ast, | ||
{ type: 'null' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
context should be one of the properties, needs to be provided from outside and we should default to null.
src/legacy/core_plugins/data/public/expressions/expressions_service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code LGTM, some comments, but we can also apply all those later when we migrate visualize embeddable handler to use this.
💚 Build Succeeded |
@ppisljar @lukeelmers I added an options parameter for context and getInitialContext if you want to take a final look. |
💚 Build Succeeded |
@flash1293 Will this be backported to 7.x? |
Merged after syncing with @flash1293
Summary
Adds a simple version of the expression executor service to the data plugin because this functionality will be used in a wide variety of plugins and apps in Kibana.
Context
This will be used in Lens to implement #36187
Over time this service is expected to grow (hopefully in a backwards compatible manner) into all the things outlined here #25374
The first simple API
run(expression, domElement)
will become a convenience function on top of these functions. It only provides a really simple API to run and render given expressions. Comments in the code mark unfinished parts which are not necessary to get this basic functionality.Integration
This is part of the data plugin which is currently a shimmed NP plugin. To get as close as possible to the NP way of doing things, the stateful dependencies (getInterpreter and renderersRegistry) are passed down from the main plugin entry point
src/legacy/core_plugins/data/public/index.ts
.As types are not available yet because the interpreter plugin itself is still JS, this integration uses local types for now. These are expected to be removed once the interpreter plugin is moved to TS.