Skip to content

Commit

Permalink
[Lens] Implement deep linking and embedding (#84416)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Jan 12, 2021
1 parent 7451288 commit 94b02d9
Show file tree
Hide file tree
Showing 19 changed files with 506 additions and 15 deletions.
5 changes: 5 additions & 0 deletions x-pack/examples/embedded_lens_example/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/consistent-type-definitions": 0
}
}
13 changes: 13 additions & 0 deletions x-pack/examples/embedded_lens_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Embedded Lens examples

To run this example plugin, use the command `yarn start --run-examples`.

This example shows how to embed Lens into other applications. Using the `EmbeddableComponent` of the `lens` start plugin,
you can pass in a valid Lens configuration which will get rendered the same way Lens dashboard panels work. Updating the
configuration will reload the embedded visualization.

## Link to editor

It is possible to use the same configuration and the `navigateToPrefilledEditor` method to navigate the current user to a
prefilled Lens editor so they can manipulate the configuration on their own and even save the results to a dashboard.
Make sure to always check permissions using `canUseEditor` whether the current user has permissions to access Lens.
15 changes: 15 additions & 0 deletions x-pack/examples/embedded_lens_example/kibana.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "embeddedLensExample",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["embedded_lens_example"],
"server": false,
"ui": true,
"requiredPlugins": [
"lens",
"data",
"developerExamples"
],
"optionalPlugins": [],
"requiredBundles": []
}
14 changes: 14 additions & 0 deletions x-pack/examples/embedded_lens_example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "embedded_lens_example",
"version": "1.0.0",
"main": "target/examples/embedded_lens_example",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../scripts/kbn.js",
"build": "rm -rf './target' && ../../../node_modules/.bin/tsc"
}
}
180 changes: 180 additions & 0 deletions x-pack/examples/embedded_lens_example/public/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState } from 'react';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
import { IndexPattern } from 'src/plugins/data/public';
import { CoreStart } from 'kibana/public';
import { TypedLensByValueInput } from '../../../plugins/lens/public';
import { StartDependencies } from './plugin';

// Generate a Lens state based on some app-specific input parameters.
// `TypedLensByValueInput` can be used for type-safety - it uses the same interfaces as Lens-internal code.
function getLensAttributes(
defaultIndexPattern: IndexPattern,
color: string
): TypedLensByValueInput['attributes'] {
return {
visualizationType: 'lnsXY',
title: 'Prefilled from example app',
references: [
{
id: defaultIndexPattern.id!,
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: defaultIndexPattern.id!,
name: 'indexpattern-datasource-layer-layer1',
type: 'index-pattern',
},
],
state: {
datasourceStates: {
indexpattern: {
layers: {
layer1: {
columnOrder: ['col1', 'col2'],
columns: {
col2: {
dataType: 'number',
isBucketed: false,
label: 'Count of records',
operationType: 'count',
scale: 'ratio',
sourceField: 'Records',
},
col1: {
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: { interval: 'auto' },
scale: 'interval',
sourceField: defaultIndexPattern.timeFieldName!,
},
},
},
},
},
},
filters: [],
query: { language: 'kuery', query: '' },
visualization: {
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
fittingFunction: 'None',
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
layers: [
{
accessors: ['col2'],
layerId: 'layer1',
seriesType: 'bar_stacked',
xAccessor: 'col1',
yConfig: [{ forAccessor: 'col2', color }],
},
],
legend: { isVisible: true, position: 'right' },
preferredSeriesType: 'bar_stacked',
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
valueLabels: 'hide',
},
},
};
}

export const App = (props: {
core: CoreStart;
plugins: StartDependencies;
defaultIndexPattern: IndexPattern | null;
}) => {
const [color, setColor] = useState('green');
const LensComponent = props.plugins.lens.EmbeddableComponent;
return (
<EuiPage>
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Embedded Lens vis</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody style={{ maxWidth: 800, margin: '0 auto' }}>
<p>
This app embeds a Lens visualization by specifying the configuration. Data fetching
and rendering is completely managed by Lens itself.
</p>
<p>
The Change color button will update the configuration by picking a new random color of
the series which causes Lens to re-render. The Edit button will take the current
configuration and navigate to a prefilled editor.
</p>
{props.defaultIndexPattern && props.defaultIndexPattern.isTimeBased() ? (
<>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => {
// eslint-disable-next-line no-bitwise
const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16);
setColor(newColor);
}}
>
Change color
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
isDisabled={!props.plugins.lens.canUseEditor()}
onClick={() => {
props.plugins.lens.navigateToPrefilledEditor({
id: '',
timeRange: {
from: 'now-5d',
to: 'now',
},
attributes: getLensAttributes(props.defaultIndexPattern!, color),
});
// eslint-disable-next-line no-bitwise
const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16);
setColor(newColor);
}}
>
Edit
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<LensComponent
id=""
style={{ height: 500 }}
timeRange={{
from: 'now-5d',
to: 'now',
}}
attributes={getLensAttributes(props.defaultIndexPattern, color)}
/>
</>
) : (
<p>This demo only works if your default index pattern is set and time based</p>
)}
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
};
9 changes: 9 additions & 0 deletions x-pack/examples/embedded_lens_example/public/index.ts
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import { EmbeddedLensExamplePlugin } from './plugin';

export const plugin = () => new EmbeddedLensExamplePlugin();
28 changes: 28 additions & 0 deletions x-pack/examples/embedded_lens_example/public/mount.tsx
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import * as React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { CoreSetup, AppMountParameters } from 'kibana/public';
import { StartDependencies } from './plugin';

export const mount = (coreSetup: CoreSetup<StartDependencies>) => async ({
element,
}: AppMountParameters) => {
const [core, plugins] = await coreSetup.getStartServices();
const { App } = await import('./app');

const deps = {
core,
plugins,
};

const defaultIndexPattern = await plugins.data.indexPatterns.getDefault();

const reactElement = <App {...deps} defaultIndexPattern={defaultIndexPattern} />;
render(reactElement, element);
return () => unmountComponentAtNode(element);
};
53 changes: 53 additions & 0 deletions x-pack/examples/embedded_lens_example/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -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;
* you may not use this file except in compliance with the Elastic License.
*/

import { Plugin, CoreSetup, AppNavLinkStatus } from '../../../../src/core/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { LensPublicStart } from '../../../plugins/lens/public';
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
import { mount } from './mount';

export interface SetupDependencies {
developerExamples: DeveloperExamplesSetup;
}

export interface StartDependencies {
data: DataPublicPluginStart;
lens: LensPublicStart;
}

export class EmbeddedLensExamplePlugin
implements Plugin<void, void, SetupDependencies, StartDependencies> {
public setup(core: CoreSetup<StartDependencies>, { developerExamples }: SetupDependencies) {
core.application.register({
id: 'embedded_lens_example',
title: 'Embedded Lens example',
navLinkStatus: AppNavLinkStatus.hidden,
mount: mount(core),
});

developerExamples.register({
appId: 'embedded_lens_example',
title: 'Embedded Lens',
description:
'Embed Lens visualizations into other applications and link to a pre-configured Lens editor to allow users to use visualizations in your app as starting points for further explorations.',
links: [
{
label: 'README',
href:
'https://github.com/elastic/kibana/tree/master/x-pack/examples/embedded_lens_example',
iconType: 'logoGithub',
size: 's',
target: '_blank',
},
],
});
}

public start() {}

public stop() {}
}
22 changes: 22 additions & 0 deletions x-pack/examples/embedded_lens_example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../typings/**/*"
],
"exclude": [],
"references": [
{ "path": "../../../src/core/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/share/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" }
]
}
2 changes: 1 addition & 1 deletion x-pack/plugins/lens/public/app_plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
EmbeddableStateTransfer,
} from '../../../../../src/plugins/embeddable/public';
import { TableInspectorAdapter } from '../editor_frame_service/types';
import { EditorFrameInstance } from '..';
import { EditorFrameInstance } from '../types';

export interface LensAppState {
isLoading: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,6 @@ describe('embeddable', () => {
searchSessionId: 'searchSessionId',
});

expect(expressionRenderer).toHaveBeenCalledTimes(1);

await new Promise((resolve) => setTimeout(resolve, 0));

expect(expressionRenderer).toHaveBeenCalledTimes(2);
Expand Down
Loading

0 comments on commit 94b02d9

Please sign in to comment.