[examples] expressions explorer (#88344)

This commit is contained in:
Peter Pisljar 2021-01-22 19:52:47 +01:00 committed by GitHub
parent 49d95f6fb1
commit d81ab83c16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1085 additions and 3 deletions

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) &gt; [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md)
## ExpressionsInspectorAdapter.ast property
<b>Signature:</b>
```typescript
get ast(): any;
```

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) &gt; [logAST](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md)
## ExpressionsInspectorAdapter.logAST() method
<b>Signature:</b>
```typescript
logAST(ast: any): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| ast | <code>any</code> | |
<b>Returns:</b>
`void`

View file

@ -0,0 +1,24 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md)
## ExpressionsInspectorAdapter class
<b>Signature:</b>
```typescript
export declare class ExpressionsInspectorAdapter extends EventEmitter
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) | | <code>any</code> | |
## Methods
| Method | Modifiers | Description |
| --- | --- | --- |
| [logAST(ast)](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) | | |

View file

@ -16,6 +16,7 @@
| [ExpressionRenderer](./kibana-plugin-plugins-expressions-public.expressionrenderer.md) | |
| [ExpressionRendererRegistry](./kibana-plugin-plugins-expressions-public.expressionrendererregistry.md) | |
| [ExpressionRenderHandler](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.md) | |
| [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) | |
| [ExpressionsPublicPlugin](./kibana-plugin-plugins-expressions-public.expressionspublicplugin.md) | |
| [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | <code>ExpressionsService</code> class is used for multiple purposes:<!-- -->1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. <code>ExpressionsService</code> defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. <code>ExpressionsService</code> creates a bound version of all exported contract functions. 5. Functions are bound the way there are:<!-- -->\`\`\`<!-- -->ts registerFunction = (...args: Parameters<!-- -->&lt;<!-- -->Executor\['registerFunction'\]<!-- -->&gt; ): ReturnType<!-- -->&lt;<!-- -->Executor\['registerFunction'\]<!-- -->&gt; =<!-- -->&gt; this.executor.registerFunction(...args); \`\`\`<!-- -->so that JSDoc appears in developers IDE when they use those <code>plugins.expressions.registerFunction(</code>. |
| [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | |

View file

@ -0,0 +1,8 @@
## expressions explorer
This example expressions explorer app shows how to:
- to run expression
- to render expression output
- emit events from expression renderer and handle them
To run this example, use the command `yarn start --run-examples`.

View file

@ -0,0 +1,10 @@
{
"id": "expressionsExplorer",
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": false,
"ui": true,
"requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"],
"optionalPlugins": [],
"requiredBundles": []
}

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { createAction } from '../../../../src/plugins/ui_actions/public';
export const ACTION_NAVIGATE = 'ACTION_NAVIGATE';
export const createNavigateAction = () =>
createAction({
id: ACTION_NAVIGATE,
type: ACTION_NAVIGATE,
getDisplayName: () => 'Navigate',
execute: async (event: any) => {
window.location.href = event.href;
},
});

View file

@ -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
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { Trigger } from '../../../../src/plugins/ui_actions/public';
export const NAVIGATE_TRIGGER_ID = 'NAVIGATE_TRIGGER_ID';
export const navigateTrigger: Trigger = {
id: NAVIGATE_TRIGGER_ID,
};

View file

@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React, { useState } from 'react';
import {
EuiFlexItem,
EuiFlexGroup,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
import {
ExpressionsStart,
ReactExpressionRenderer,
ExpressionsInspectorAdapter,
} from '../../../src/plugins/expressions/public';
import { ExpressionEditor } from './editor/expression_editor';
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger';
interface Props {
expressions: ExpressionsStart;
actions: UiActionsStart;
}
export function ActionsExpressionsExample({ expressions, actions }: Props) {
const [expression, updateExpression] = useState(
'button name="click me" href="http://www.google.com"'
);
const expressionChanged = (value: string) => {
updateExpression(value);
};
const inspectorAdapters = {
expression: new ExpressionsInspectorAdapter(),
};
const handleEvents = (event: any) => {
if (event.id !== 'NAVIGATE') return;
// enrich event context with some extra data
event.baseUrl = 'http://www.google.com';
actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value);
};
return (
<EuiPageBody>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Actions from expression renderers</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>
Here you can play with sample `button` which takes a url as configuration and
displays a button which emits custom BUTTON_CLICK trigger to which we have attached
a custom action which performs the navigation.
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiPanel data-test-subj="expressionEditor" paddingSize="none" role="figure">
<ExpressionEditor value={expression} onChange={expressionChanged} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="none" role="figure">
<ReactExpressionRenderer
expression={expression}
debug={true}
inspectorAdapters={inspectorAdapters}
onEvent={handleEvents}
renderError={(message: any) => {
return <div>{message}</div>;
}}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {
EuiPage,
EuiPageHeader,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiSpacer,
EuiText,
EuiLink,
} from '@elastic/eui';
import { AppMountParameters } from '../../../src/core/public';
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
import { RunExpressionsExample } from './run_expressions';
import { RenderExpressionsExample } from './render_expressions';
import { ActionsExpressionsExample } from './actions_and_expressions';
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
interface Props {
expressions: ExpressionsStart;
inspector: InspectorStart;
actions: UiActionsStart;
}
const ExpressionsExplorer = ({ expressions, inspector, actions }: Props) => {
return (
<EuiPage>
<EuiPageBody>
<EuiPageHeader>Expressions Explorer</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiText>
<p>
There are a couple of ways to run the expressions. Below some of the options are
demonstrated. You can read more about it{' '}
<EuiLink
href={
'https://github.com/elastic/kibana/blob/master/src/plugins/expressions/README.asciidoc'
}
>
here
</EuiLink>
</p>
</EuiText>
<EuiSpacer />
<RunExpressionsExample expressions={expressions} inspector={inspector} />
<EuiSpacer />
<RenderExpressionsExample expressions={expressions} inspector={inspector} />
<EuiSpacer />
<ActionsExpressionsExample expressions={expressions} actions={actions} />
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};
export const renderApp = (props: Props, { element }: AppMountParameters) => {
ReactDOM.render(<ExpressionsExplorer {...props} />, element);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React from 'react';
import { EuiCodeEditor } from '@elastic/eui';
interface Props {
value: string;
onChange: (value: string) => void;
}
export function ExpressionEditor({ value, onChange }: Props) {
return (
<EuiCodeEditor
mode="javascript"
theme="github"
width="100%"
value={value}
onChange={onChange}
setOptions={{
fontSize: '14px',
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
}}
onBlur={() => {}}
aria-label="Code Editor"
/>
);
}

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/common';
interface Arguments {
href: string;
name: string;
}
export type ExpressionFunctionButton = ExpressionFunctionDefinition<
'button',
unknown,
Arguments,
unknown
>;
export const buttonFn: ExpressionFunctionButton = {
name: 'button',
args: {
href: {
help: i18n.translate('expressions.functions.font.args.href', {
defaultMessage: 'Link to which to navigate',
}),
types: ['string'],
required: true,
},
name: {
help: i18n.translate('expressions.functions.font.args.name', {
defaultMessage: 'Name of the button',
}),
types: ['string'],
default: 'button',
},
},
help: 'Configures the button',
fn: (input: unknown, args: Arguments) => {
return {
type: 'render',
as: 'button',
value: args,
};
},
};

View file

@ -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
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { ExpressionsExplorerPlugin } from './plugin';
export const plugin = () => new ExpressionsExplorerPlugin();

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React, { useState } from 'react';
import { EuiTreeView, EuiDescriptionList, EuiCodeBlock, EuiText, EuiSpacer } from '@elastic/eui';
interface Props {
ast: any;
}
const decorateAst = (ast: any, nodeClicked: any) => {
return ast.chain.map((link: any) => {
return {
id: link.function + Math.random(),
label: link.function,
callback: () => {
nodeClicked(link.debug);
},
children: Object.keys(link.arguments).reduce((result: any, key: string) => {
if (typeof link.arguments[key] === 'object') {
// result[key] = decorateAst(link.arguments[key]);
}
return result;
}, []),
};
});
};
const prepareNode = (key: string, value: any) => {
if (key === 'args') {
return (
<EuiCodeBlock language="json" fontSize="m" paddingSize="m" isCopyable>
{JSON.stringify(value, null, '\t')}
</EuiCodeBlock>
);
} else if (key === 'output' || key === 'input') {
return (
<EuiCodeBlock language="json" fontSize="m" paddingSize="m" isCopyable>
{JSON.stringify(value, null, '\t')}
</EuiCodeBlock>
);
} else if (key === 'success') {
return value ? 'true' : 'false';
} else return <span>{value}</span>;
};
export function AstDebugView({ ast }: Props) {
const [nodeInfo, setNodeInfo] = useState([] as any[]);
const items = decorateAst(ast, (node: any) => {
setNodeInfo(
Object.keys(node).map((key) => ({
title: key,
description: prepareNode(key, node[key]),
}))
);
});
return (
<div>
<EuiText>List of executed expression functions:</EuiText>
<EuiTreeView
items={items}
display="compressed"
expandByDefault
showExpansionArrows
aria-label="Document Outline"
/>
<EuiSpacer />
<EuiText>Details of selected function:</EuiText>
<EuiDescriptionList type="column" listItems={nodeInfo} />
</div>
);
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { InspectorViewProps, Adapters } from '../../../../src/plugins/inspector/public';
import { AstDebugView } from './ast_debug_view';
interface ExpressionsInspectorViewComponentState {
ast: any;
adapters: Adapters;
}
class ExpressionsInspectorViewComponent extends Component<
InspectorViewProps,
ExpressionsInspectorViewComponentState
> {
static propTypes = {
adapters: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
};
state = {} as ExpressionsInspectorViewComponentState;
static getDerivedStateFromProps(
nextProps: Readonly<InspectorViewProps>,
state: ExpressionsInspectorViewComponentState
) {
if (state && nextProps.adapters === state.adapters) {
return null;
}
const { ast } = nextProps.adapters.expression;
return {
adapters: nextProps.adapters,
ast,
};
}
onUpdateData = (ast: any) => {
this.setState({
ast,
});
};
componentDidMount() {
this.props.adapters.expression!.on('change', this.onUpdateData);
}
componentWillUnmount() {
this.props.adapters.expression!.removeListener('change', this.onUpdateData);
}
static renderNoData() {
return (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="data.inspector.table.noDataAvailableTitle"
defaultMessage="No data available"
/>
</h2>
}
body={
<React.Fragment>
<p>
<FormattedMessage
id="data.inspector.table.noDataAvailableDescription"
defaultMessage="The element did not provide any data."
/>
</p>
</React.Fragment>
}
/>
);
}
render() {
if (!this.state.ast) {
return ExpressionsInspectorViewComponent.renderNoData();
}
return <AstDebugView ast={this.state.ast} />;
}
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export default ExpressionsInspectorViewComponent;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React, { lazy } from 'react';
const ExpressionsInspectorViewComponent = lazy(() => import('./expressions_inspector_view'));
export const getExpressionsInspectorViewComponentWrapper = () => {
return (props: any) => {
return <ExpressionsInspectorViewComponent adapters={props.adapters} title={props.title} />;
};
};

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { Adapters, InspectorViewDescription } from '../../../../src/plugins/inspector/public';
import { getExpressionsInspectorViewComponentWrapper } from './expressions_inspector_view_wrapper';
export const getExpressionsInspectorViewDescription = (): InspectorViewDescription => ({
title: i18n.translate('data.inspector.table.dataTitle', {
defaultMessage: 'Expression',
}),
order: 100,
help: i18n.translate('data.inspector.table..dataDescriptionTooltip', {
defaultMessage: 'View the expression behind the visualization',
}),
shouldShow(adapters: Adapters) {
return Boolean(adapters.expression);
},
component: getExpressionsInspectorViewComponentWrapper(),
});

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public';
import { DeveloperExamplesSetup } from '../../developer_examples/public';
import { ExpressionsSetup, ExpressionsStart } from '../../../src/plugins/expressions/public';
import {
Setup as InspectorSetup,
Start as InspectorStart,
} from '../../../src/plugins/inspector/public';
import { getExpressionsInspectorViewDescription } from './inspector';
import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public';
import { NAVIGATE_TRIGGER_ID, navigateTrigger } from './actions/navigate_trigger';
import { ACTION_NAVIGATE, createNavigateAction } from './actions/navigate_action';
import { buttonRenderer } from './renderers/button';
import { buttonFn } from './functions/button';
interface StartDeps {
expressions: ExpressionsStart;
inspector: InspectorStart;
uiActions: UiActionsStart;
}
interface SetupDeps {
uiActions: UiActionsSetup;
expressions: ExpressionsSetup;
inspector: InspectorSetup;
developerExamples: DeveloperExamplesSetup;
}
export class ExpressionsExplorerPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
public setup(core: CoreSetup<StartDeps>, deps: SetupDeps) {
// register custom inspector adapter & view
deps.inspector.registerView(getExpressionsInspectorViewDescription());
// register custom actions
deps.uiActions.registerTrigger(navigateTrigger);
deps.uiActions.registerAction(createNavigateAction());
deps.uiActions.attachAction(NAVIGATE_TRIGGER_ID, ACTION_NAVIGATE);
// register custom functions and renderers
deps.expressions.registerRenderer(buttonRenderer);
deps.expressions.registerFunction(buttonFn);
core.application.register({
id: 'expressionsExplorer',
title: 'Expressions Explorer',
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
const [, depsStart] = await core.getStartServices();
const { renderApp } = await import('./app');
return renderApp(
{
expressions: depsStart.expressions,
inspector: depsStart.inspector,
actions: depsStart.uiActions,
},
params
);
},
});
deps.developerExamples.register({
appId: 'expressionsExplorer',
title: 'Expressions',
description: `Expressions is a plugin that allows to execute Kibana expressions and render content using expression renderers. This example plugin showcases various usage scenarios.`,
links: [
{
label: 'README',
href: 'https://github.com/elastic/kibana/blob/master/src/plugins/expressions/README.md',
iconType: 'logoGithub',
size: 's',
target: '_blank',
},
],
});
}
public start() {}
public stop() {}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React, { useState } from 'react';
import {
EuiFlexItem,
EuiFlexGroup,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiPanel,
EuiText,
EuiTitle,
EuiButton,
} from '@elastic/eui';
import {
ExpressionsStart,
ReactExpressionRenderer,
ExpressionsInspectorAdapter,
} from '../../../src/plugins/expressions/public';
import { ExpressionEditor } from './editor/expression_editor';
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
interface Props {
expressions: ExpressionsStart;
inspector: InspectorStart;
}
export function RenderExpressionsExample({ expressions, inspector }: Props) {
const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"');
const expressionChanged = (value: string) => {
updateExpression(value);
};
const inspectorAdapters = {
expression: new ExpressionsInspectorAdapter(),
};
return (
<EuiPageBody>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Render expressions</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>
In the below editor you can enter your expression and render it. Using
ReactExpressionRenderer component makes that very easy.
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
onClick={() => {
inspector.open(inspectorAdapters);
}}
>
Open Inspector
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiPanel data-test-subj="expressionEditor" paddingSize="none" role="figure">
<ExpressionEditor value={expression} onChange={expressionChanged} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel data-test-subj="expressionRender" paddingSize="none" role="figure">
<ReactExpressionRenderer
expression={expression}
debug={true}
inspectorAdapters={inspectorAdapters}
renderError={(message: any) => {
return <div>{message}</div>;
}}
/>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import ReactDOM from 'react-dom';
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { ExpressionRenderDefinition } from '../../../../src/plugins/expressions/common/expression_renderers';
export const buttonRenderer: ExpressionRenderDefinition<any> = {
name: 'button',
displayName: 'Button',
reuseDomNode: true,
render(domNode, config, handlers) {
const buttonClick = () => {
handlers.event({
id: 'NAVIGATE',
value: {
href: config.href,
},
});
};
const renderDebug = () => (
<div style={{ width: domNode.offsetWidth, height: domNode.offsetHeight }}>
<EuiButton data-test-subj="testExpressionButton" onClick={buttonClick}>
{config.name}
</EuiButton>
</div>
);
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
},
};

View file

@ -0,0 +1,118 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import React, { useState, useEffect, useMemo } from 'react';
import {
EuiCodeBlock,
EuiFlexItem,
EuiFlexGroup,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiPanel,
EuiText,
EuiTitle,
EuiButton,
} from '@elastic/eui';
import {
ExpressionsStart,
ExpressionsInspectorAdapter,
} from '../../../src/plugins/expressions/public';
import { ExpressionEditor } from './editor/expression_editor';
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
interface Props {
expressions: ExpressionsStart;
inspector: InspectorStart;
}
export function RunExpressionsExample({ expressions, inspector }: Props) {
const [expression, updateExpression] = useState('markdown "## expressions explorer"');
const [result, updateResult] = useState({});
const expressionChanged = (value: string) => {
updateExpression(value);
};
const inspectorAdapters = useMemo(
() => ({
expression: new ExpressionsInspectorAdapter(),
}),
[]
);
useEffect(() => {
const runExpression = async () => {
const execution = expressions.execute(expression, null, {
debug: true,
inspectorAdapters,
});
const data: any = await execution.getData();
updateResult(data);
};
runExpression();
}, [expression, expressions, inspectorAdapters]);
return (
<EuiPageBody>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Run expressions</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText>
In the below editor you can enter your expression and execute it. Using
expressions.execute allows you to easily run the expression.
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton
onClick={() => {
inspector.open(inspectorAdapters);
}}
>
Open Inspector
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiPanel data-test-subj="expressionEditor" paddingSize="none" role="figure">
<ExpressionEditor value={expression} onChange={expressionChanged} />
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem>
<EuiPanel paddingSize="none" role="figure">
<EuiCodeBlock
language="json"
fontSize="m"
paddingSize="m"
isCopyable
data-test-subj="expressionResult"
>
{JSON.stringify(result, null, '\t')}
</EuiCodeBlock>
</EuiPanel>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
}

View file

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"../../typings/**/*",
],
"exclude": [],
"references": [
{ "path": "../../src/core/tsconfig.json" },
{ "path": "../../src/plugins/kibana_react/tsconfig.json" },
]
}

View file

@ -29,6 +29,7 @@ import { getByAlias } from '../util/get_by_alias';
import { ExecutionContract } from './execution_contract';
import { ExpressionExecutionParams } from '../service';
import { TablesAdapter } from '../util/tables_adapter';
import { ExpressionsInspectorAdapter } from '../util/expressions_inspector_adapter';
/**
* AbortController is not available in Node until v15, so we
@ -63,6 +64,7 @@ export interface ExecutionParams {
const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({
requests: new RequestAdapter(),
tables: new TablesAdapter(),
expression: new ExpressionsInspectorAdapter(),
});
export class Execution<
@ -208,6 +210,9 @@ export class Execution<
this.firstResultFuture.promise
.then(
(result) => {
if (this.context.inspectorAdapters.expression) {
this.context.inspectorAdapters.expression.logAST(this.state.get().ast);
}
this.state.transitions.setResult(result);
},
(error) => {

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { EventEmitter } from 'events';
export class ExpressionsInspectorAdapter extends EventEmitter {
private _ast: any = {};
public logAST(ast: any): void {
this._ast = ast;
this.emit('change', this._ast);
}
public get ast() {
return this._ast;
}
}

View file

@ -9,3 +9,4 @@
export * from './create_error';
export * from './get_by_alias';
export * from './tables_adapter';
export * from './expressions_inspector_adapter';

View file

@ -107,4 +107,5 @@ export {
ExpressionsServiceSetup,
ExpressionsServiceStart,
TablesAdapter,
ExpressionsInspectorAdapter,
} from '../common';

View file

@ -551,6 +551,16 @@ export class ExpressionRenderHandler {
update$: Observable<UpdateValue | null>;
}
// Warning: (ae-missing-release-tag) "ExpressionsInspectorAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class ExpressionsInspectorAdapter extends EventEmitter {
// (undocumented)
get ast(): any;
// (undocumented)
logAST(ast: any): void;
}
// Warning: (ae-missing-release-tag) "ExpressionsPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)

View file

@ -19,6 +19,7 @@ export default async function ({ readConfigFile }) {
require.resolve('./ui_actions'),
require.resolve('./state_sync'),
require.resolve('./routing'),
require.resolve('./expressions_explorer'),
],
services: {
...functionalConfig.get('services'),

View file

@ -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
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import expect from '@kbn/expect';
import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
// eslint-disable-next-line import/no-default-export
export default function ({ getService }: PluginFunctionalProviderContext) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const browser = getService('browser');
describe('', () => {
it('runs expression', async () => {
await retry.try(async () => {
const text = await testSubjects.getVisibleText('expressionResult');
expect(text).to.be(
'{\n "type": "error",\n "error": {\n "message": "Function markdown could not be found.",\n "name": "fn not found"\n }\n}'
);
});
});
it('renders expression', async () => {
await retry.try(async () => {
const text = await testSubjects.getVisibleText('expressionRender');
expect(text).to.be('Function markdown could not be found.');
});
});
it('emits an action and navigates', async () => {
await testSubjects.click('testExpressionButton');
await retry.try(async () => {
const text = await browser.getCurrentUrl();
expect(text).to.be('https://www.google.com/?gws_rd=ssl');
});
});
});
}

View file

@ -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
* and the Server Side Public License, v 1; you may not use this file except in
* compliance with, at your election, the Elastic License or the Server Side
* Public License, v 1.
*/
import { PluginFunctionalProviderContext } from 'test/plugin_functional/services';
// eslint-disable-next-line import/no-default-export
export default function ({
getService,
getPageObjects,
loadTestFile,
}: PluginFunctionalProviderContext) {
const browser = getService('browser');
const PageObjects = getPageObjects(['common', 'header']);
describe('expressions explorer', function () {
before(async () => {
await browser.setWindowSize(1300, 900);
await PageObjects.common.navigateToApp('expressionsExplorer');
});
loadTestFile(require.resolve('./expressions'));
});
}

View file

@ -26,9 +26,11 @@ export const debug: RendererFactory<any> = () => ({
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
handlers.onResize(() => {
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
});
if (handlers.onResize) {
handlers.onResize(() => {
ReactDOM.render(renderDebug(), domNode, () => handlers.done());
});
}
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
},