adding markdown vis renderer (#75532) (#75628)

This commit is contained in:
Peter Pisljar 2020-08-21 16:14:37 +02:00 committed by GitHub
parent d57460d521
commit 4fdcf6bdbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 288 additions and 68 deletions

View file

@ -4,5 +4,5 @@
"ui": true,
"server": true,
"requiredPlugins": ["expressions", "visualizations"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "data", "charts"]
"requiredBundles": ["kibanaUtils", "kibanaReact", "data", "charts", "visualizations", "expressions"]
}

View file

@ -2,7 +2,7 @@
exports[`interpreter/functions#markdown returns an object with the correct structure 1`] = `
Object {
"as": "visualization",
"as": "markdown_vis",
"type": "render",
"value": Object {
"visConfig": Object {

View file

@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`markdown vis toExpressionAst function with params 1`] = `
Object {
"chain": Array [
Object {
"arguments": Object {
"font": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"size": Array [
15,
],
},
"function": "font",
"type": "function",
},
],
"type": "expression",
},
],
"markdown": Array [
"### my markdown",
],
"openLinksInNewTab": Array [
true,
],
},
"function": "markdownVis",
"type": "function",
},
],
"type": "expression",
}
`;
exports[`markdown vis toExpressionAst function without params 1`] = `
Object {
"chain": Array [
Object {
"arguments": Object {
"font": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"size": Array [
"undefined",
],
},
"function": "font",
"type": "function",
},
],
"type": "expression",
},
],
},
"function": "markdownVis",
"type": "function",
},
],
"type": "expression",
}
`;

View file

@ -26,12 +26,14 @@ interface RenderValue {
visConfig: MarkdownVisParams;
}
export const createMarkdownVisFn = (): ExpressionFunctionDefinition<
export type MarkdownVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
'markdownVis',
unknown,
Arguments,
Render<RenderValue>
> => ({
>;
export const createMarkdownVisFn = (): MarkdownVisExpressionFunctionDefinition => ({
name: 'markdownVis',
type: 'render',
inputTypes: [],
@ -65,7 +67,7 @@ export const createMarkdownVisFn = (): ExpressionFunctionDefinition<
fn(input, args) {
return {
type: 'render',
as: 'visualization',
as: 'markdown_vis',
value: {
visType: 'markdown',
visConfig: {

View file

@ -0,0 +1,57 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
import { MarkdownVisWrapper } from './markdown_vis_controller';
import { StartServicesGetter } from '../../kibana_utils/public';
export const getMarkdownRenderer = (start: StartServicesGetter) => {
const markdownVisRenderer: () => ExpressionRenderDefinition = () => ({
name: 'markdown_vis',
displayName: 'markdown visualization',
reuseDomNode: true,
render: async (domNode: HTMLElement, config: any, handlers: any) => {
const { visConfig } = config;
const I18nContext = await start().core.i18n.Context;
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
render(
<I18nContext>
<VisualizationContainer className="markdownVis">
<MarkdownVisWrapper
visParams={visConfig}
renderComplete={handlers.done}
fireEvent={handlers.event}
/>
</VisualizationContainer>
</I18nContext>,
domNode
);
},
});
return markdownVisRenderer;
};

View file

@ -19,10 +19,10 @@
import { i18n } from '@kbn/i18n';
import { MarkdownVisWrapper } from './markdown_vis_controller';
import { MarkdownOptions } from './markdown_options';
import { SettingsOptions } from './settings_options_lazy';
import { DefaultEditorSize } from '../../vis_default_editor/public';
import { toExpressionAst } from './to_ast';
export const markdownVisDefinition = {
name: 'markdown',
@ -32,8 +32,8 @@ export const markdownVisDefinition = {
description: i18n.translate('visTypeMarkdown.markdownDescription', {
defaultMessage: 'Create a document using markdown syntax',
}),
toExpressionAst,
visConfig: {
component: MarkdownVisWrapper,
defaults: {
fontSize: 12,
openLinksInNewTab: false,

View file

@ -25,13 +25,15 @@ describe('markdown vis controller', () => {
it('should set html from markdown params', () => {
const vis = {
params: {
openLinksInNewTab: false,
fontSize: 16,
markdown:
'This is a test of the [markdown](http://daringfireball.net/projects/markdown) vis.',
},
};
const wrapper = render(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={jest.fn()} />
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
);
expect(wrapper.find('a').text()).toBe('markdown');
});
@ -39,12 +41,14 @@ describe('markdown vis controller', () => {
it('should not render the html', () => {
const vis = {
params: {
openLinksInNewTab: false,
fontSize: 16,
markdown: 'Testing <a>html</a>',
},
};
const wrapper = render(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={jest.fn()} />
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
);
expect(wrapper.text()).toBe('Testing <a>html</a>\n');
});
@ -52,12 +56,14 @@ describe('markdown vis controller', () => {
it('should update the HTML when render again with changed params', () => {
const vis = {
params: {
openLinksInNewTab: false,
fontSize: 16,
markdown: 'Initial',
},
};
const wrapper = mount(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={jest.fn()} />
<MarkdownVisWrapper visParams={vis.params} renderComplete={jest.fn()} fireEvent={jest.fn()} />
);
expect(wrapper.text().trim()).toBe('Initial');
vis.params.markdown = 'Updated';
@ -66,52 +72,68 @@ describe('markdown vis controller', () => {
});
describe('renderComplete', () => {
const vis = {
params: {
openLinksInNewTab: false,
fontSize: 16,
markdown: 'test',
},
};
const renderComplete = jest.fn();
beforeEach(() => {
renderComplete.mockClear();
});
it('should be called on initial rendering', () => {
const vis = {
params: {
markdown: 'test',
},
};
const renderComplete = jest.fn();
mount(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={renderComplete} />
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
expect(renderComplete.mock.calls.length).toBe(1);
});
it('should be called on successive render when params change', () => {
const vis = {
params: {
markdown: 'test',
},
};
const renderComplete = jest.fn();
mount(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={renderComplete} />
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
expect(renderComplete.mock.calls.length).toBe(1);
renderComplete.mockClear();
vis.params.markdown = 'changed';
mount(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={renderComplete} />
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
expect(renderComplete.mock.calls.length).toBe(1);
});
it('should be called on successive render even without data change', () => {
const vis = {
params: {
markdown: 'test',
},
};
const renderComplete = jest.fn();
mount(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={renderComplete} />
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
expect(renderComplete.mock.calls.length).toBe(1);
renderComplete.mockClear();
mount(
<MarkdownVisWrapper vis={vis} visParams={vis.params} renderComplete={renderComplete} />
<MarkdownVisWrapper
visParams={vis.params}
renderComplete={renderComplete}
fireEvent={jest.fn()}
/>
);
expect(renderComplete.mock.calls.length).toBe(1);
});

View file

@ -22,7 +22,7 @@ import { Markdown } from '../../kibana_react/public';
import { MarkdownVisParams } from './types';
interface MarkdownVisComponentProps extends MarkdownVisParams {
renderComplete: () => {};
renderComplete: () => void;
}
/**
@ -80,7 +80,14 @@ class MarkdownVisComponent extends React.Component<MarkdownVisComponentProps> {
* The way React works, this wrapper nearly brings no overhead, but allows us
* to use proper lifecycle methods in the actual component.
*/
export function MarkdownVisWrapper(props: any) {
export interface MarkdownVisWrapperProps {
visParams: MarkdownVisParams;
fireEvent: (event: any) => void;
renderComplete: () => void;
}
export function MarkdownVisWrapper(props: MarkdownVisWrapperProps) {
return (
<MarkdownVisComponent
fontSize={props.visParams.fontSize}

View file

@ -26,6 +26,8 @@ import { createMarkdownVisFn } from './markdown_fn';
import { ConfigSchema } from '../config';
import './index.scss';
import { getMarkdownRenderer } from './markdown_renderer';
import { createStartServicesGetter } from '../../kibana_utils/public';
/** @internal */
export interface MarkdownPluginSetupDependencies {
@ -42,7 +44,9 @@ export class MarkdownPlugin implements Plugin<void, void> {
}
public setup(core: CoreSetup, { expressions, visualizations }: MarkdownPluginSetupDependencies) {
visualizations.createReactVisualization(markdownVisDefinition);
const start = createStartServicesGetter(core.getStartServices);
visualizations.createBaseVisualization(markdownVisDefinition);
expressions.registerRenderer(getMarkdownRenderer(start));
expressions.registerFunction(createMarkdownVisFn);
}

View file

@ -0,0 +1,54 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { toExpressionAst } from './to_ast';
import { Vis } from '../../visualizations/public';
describe('markdown vis toExpressionAst function', () => {
let vis: Vis;
beforeEach(() => {
vis = {
isHierarchical: () => false,
type: {},
params: {
percentageMode: false,
},
data: {
indexPattern: { id: '123' } as any,
aggs: {
getResponseAggs: () => [],
aggs: [],
} as any,
},
} as any;
});
it('without params', () => {
vis.params = {};
const actual = toExpressionAst(vis);
expect(actual).toMatchSnapshot();
});
it('with params', () => {
vis.params = { markdown: '### my markdown', fontSize: 15, openLinksInNewTab: true };
const actual = toExpressionAst(vis);
expect(actual).toMatchSnapshot();
});
});

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Vis } from '../../visualizations/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { MarkdownVisExpressionFunctionDefinition } from './markdown_fn';
export const toExpressionAst = (vis: Vis) => {
const { markdown, fontSize, openLinksInNewTab } = vis.params;
const markdownVis = buildExpressionFunction<MarkdownVisExpressionFunctionDefinition>(
'markdownVis',
{
markdown,
font: buildExpression(`font size=${fontSize}`),
openLinksInNewTab,
}
);
const ast = buildExpression([markdownVis]);
return ast.toAst();
};

View file

@ -4,8 +4,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls t
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' font={font size=12} openLinksInNewTab=true "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles pie function 1`] = `"kibana_pie visConfig='{\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"buckets\\":[1,2]}}' "`;
@ -34,6 +32,4 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' font={font size=12} openLinksInNewTab=true "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`;

View file

@ -123,23 +123,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
expect(actual).toMatchSnapshot();
});
it('handles markdown function', () => {
const params = {
markdown: '## hello _markdown_',
fontSize: 12,
openLinksInNewTab: true,
foo: 'bar',
};
const actual = buildPipelineVisFunction.markdown(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
it('handles undefined markdown function', () => {
const params = { fontSize: 12, openLinksInNewTab: true, foo: 'bar' };
const actual = buildPipelineVisFunction.markdown(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
describe('handles table function', () => {
it('without splits or buckets', () => {
const params = { foo: 'bar' };

View file

@ -269,17 +269,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
const interval = prepareString('interval', params.interval);
return `timelion_vis ${expression}${interval}`;
},
markdown: (params) => {
const { markdown, fontSize, openLinksInNewTab } = params;
let escapedMarkdown = '';
if (typeof markdown === 'string' || markdown instanceof String) {
escapedMarkdown = escapeString(markdown.toString());
}
let expr = `markdownvis '${escapedMarkdown}' `;
expr += prepareValue('font', `{font size=${fontSize}}`, true);
expr += prepareValue('openLinksInNewTab', openLinksInNewTab);
return expr;
},
table: (params, schemas) => {
const visConfig = {
...params,