mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Embeddables Rebuild] Remove legacy examples (#181635)
PR does the following 1. removes legacy example embeddables 2. Replaces FilterDebugger legacy embeddable with react embeddable 3. Moves FilterDebugger react embeddable to portable_dashboards_example since that is where its used 4. Fixes DashboardContainer.addNewPanel to return ApiType for react embeddables. ### test instructions 1. start kibana `yarn start --run-examples` 2. Open kibana menu and click "Developer examples" 3. Use developer examples search bar to find "Portable Dashboards" example and click on the card 4. Verify first example that shows control filters works --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
91cfb36040
commit
f39dc96f83
30 changed files with 132 additions and 730 deletions
|
@ -5,7 +5,7 @@
|
|||
"description": "Example app that shows how to register custom embeddables",
|
||||
"plugin": {
|
||||
"id": "embeddableExamples",
|
||||
"server": true,
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"requiredPlugins": [
|
||||
"dataViews",
|
||||
|
@ -17,7 +17,6 @@
|
|||
"fieldFormats",
|
||||
"developerExamples"
|
||||
],
|
||||
"requiredBundles": ["presentationUtil"],
|
||||
"extraPublicDirs": ["public/hello_world"]
|
||||
"requiredBundles": ["presentationUtil"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
FilterDebuggerEmbeddableInput,
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
} from './filter_debugger_embeddable_factory';
|
||||
import { FilterDebuggerEmbeddableComponent } from './filter_debugger_embeddable_component';
|
||||
|
||||
export class FilterDebuggerEmbeddable extends Embeddable<FilterDebuggerEmbeddableInput> {
|
||||
public readonly type = FILTER_DEBUGGER_EMBEDDABLE;
|
||||
|
||||
private domNode?: HTMLElement;
|
||||
|
||||
constructor(initialInput: FilterDebuggerEmbeddableInput, parent?: IContainer) {
|
||||
super(initialInput, {}, parent);
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
if (this.domNode) {
|
||||
ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
this.domNode = node;
|
||||
ReactDOM.render(<FilterDebuggerEmbeddableComponent embeddable={this} />, node);
|
||||
}
|
||||
|
||||
public reload() {}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.domNode) ReactDOM.unmountComponentAtNode(this.domNode);
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiPanel, EuiTitle, EuiCodeBlock, EuiSpacer } from '@elastic/eui';
|
||||
import { compareFilters, COMPARE_ALL_OPTIONS, type Filter, type Query } from '@kbn/es-query';
|
||||
|
||||
import { FilterDebuggerEmbeddable } from './filter_debugger_embeddable';
|
||||
|
||||
interface Props {
|
||||
embeddable: FilterDebuggerEmbeddable;
|
||||
}
|
||||
|
||||
export function FilterDebuggerEmbeddableComponent({ embeddable }: Props) {
|
||||
const [filters, setFilters] = useState<Filter[]>();
|
||||
const [query, setQuery] = useState<Query>();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = embeddable
|
||||
.getInput$()
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
({ filters: filtersA, query: queryA }, { filters: filtersB, query: queryB }) => {
|
||||
return (
|
||||
JSON.stringify(queryA) === JSON.stringify(queryB) &&
|
||||
compareFilters(filtersA ?? [], filtersB ?? [], COMPARE_ALL_OPTIONS)
|
||||
);
|
||||
}
|
||||
)
|
||||
)
|
||||
.subscribe(({ filters: newFilters, query: newQuery }) => {
|
||||
setFilters(newFilters);
|
||||
setQuery(newQuery);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [embeddable]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
css={css`
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
`}
|
||||
className="eui-yScrollWithShadows"
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiTitle>
|
||||
<h2>Filters</h2>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock language="JSON">{JSON.stringify(filters, undefined, 1)}</EuiCodeBlock>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle>
|
||||
<h2>Query</h2>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock language="JSON">{JSON.stringify(query, undefined, 1)}</EuiCodeBlock>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
IContainer,
|
||||
EmbeddableInput,
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { type Filter, type Query } from '@kbn/es-query';
|
||||
import { FilterDebuggerEmbeddable } from './filter_debugger_embeddable';
|
||||
|
||||
export const FILTER_DEBUGGER_EMBEDDABLE = 'filterDebuggerEmbeddable';
|
||||
export interface FilterDebuggerEmbeddableInput extends EmbeddableInput {
|
||||
filters?: Filter[];
|
||||
query?: Query;
|
||||
}
|
||||
|
||||
export type FilterDebuggerEmbeddableFactory = EmbeddableFactory;
|
||||
export class FilterDebuggerEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
|
||||
public readonly type = FILTER_DEBUGGER_EMBEDDABLE;
|
||||
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async create(initialInput: FilterDebuggerEmbeddableInput, parent?: IContainer) {
|
||||
return new FilterDebuggerEmbeddable(initialInput, parent);
|
||||
}
|
||||
|
||||
public canCreateNew() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return 'Filter debugger';
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './filter_debugger_embeddable';
|
||||
export * from './filter_debugger_embeddable_factory';
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Embeddable, EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
export const HELLO_WORLD_EMBEDDABLE = 'HELLO_WORLD_EMBEDDABLE';
|
||||
|
||||
export class HelloWorldEmbeddable extends Embeddable {
|
||||
// The type of this embeddable. This will be used to find the appropriate factory
|
||||
// to instantiate this kind of embeddable.
|
||||
public readonly type = HELLO_WORLD_EMBEDDABLE;
|
||||
|
||||
constructor(initialInput: EmbeddableInput, parent?: IContainer) {
|
||||
super(
|
||||
// Input state is irrelevant to this embeddable, just pass it along.
|
||||
initialInput,
|
||||
// Initial output state - this embeddable does not do anything with output, so just
|
||||
// pass along an empty object.
|
||||
{},
|
||||
// Optional parent component, this embeddable can optionally be rendered inside a container.
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render yourself at the dom node using whatever framework you like, angular, react, or just plain
|
||||
* vanilla js.
|
||||
* @param node
|
||||
*/
|
||||
public render(node: HTMLElement) {
|
||||
node.innerHTML =
|
||||
'<div data-test-subj="helloWorldEmbeddable" data-render-complete="true">HELLO WORLD!</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly relevant for time based embeddables which need to update data
|
||||
* even if EmbeddableInput has not changed at all.
|
||||
*/
|
||||
public reload() {}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
IContainer,
|
||||
EmbeddableInput,
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from './hello_world_embeddable';
|
||||
|
||||
export type HelloWorldEmbeddableFactory = EmbeddableFactory;
|
||||
export class HelloWorldEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition {
|
||||
public readonly type = HELLO_WORLD_EMBEDDABLE;
|
||||
|
||||
/**
|
||||
* In our simple example, we let everyone have permissions to edit this. Most
|
||||
* embeddables should check the UI Capabilities service to be sure of
|
||||
* the right permissions.
|
||||
*/
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async create(initialInput: EmbeddableInput, parent?: IContainer) {
|
||||
return new HelloWorldEmbeddable(initialInput, parent);
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('embeddableExamples.helloworld.displayName', {
|
||||
defaultMessage: 'hello world',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './hello_world_embeddable';
|
||||
export * from './hello_world_embeddable_factory';
|
|
@ -6,21 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export type { HelloWorldEmbeddableFactory } from './hello_world';
|
||||
export {
|
||||
HELLO_WORLD_EMBEDDABLE,
|
||||
HelloWorldEmbeddable,
|
||||
HelloWorldEmbeddableFactoryDefinition,
|
||||
} from './hello_world';
|
||||
export type { ListContainerFactory } from './list_container';
|
||||
export { ListContainer, LIST_CONTAINER } from './list_container';
|
||||
|
||||
export { SIMPLE_EMBEDDABLE } from './migrations';
|
||||
export {
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
FilterDebuggerEmbeddableFactoryDefinition,
|
||||
} from './filter_debugger';
|
||||
|
||||
import { EmbeddableExamplesPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new EmbeddableExamplesPlugin();
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { ListContainer, LIST_CONTAINER } from './list_container';
|
||||
export type { ListContainerFactory } from './list_container_factory';
|
||||
export { ListContainerFactoryDefinition } from './list_container_factory';
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Container, ContainerInput, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import { ListContainerComponent } from './list_container_component';
|
||||
|
||||
export const LIST_CONTAINER = 'LIST_CONTAINER';
|
||||
|
||||
export class ListContainer extends Container<{}, ContainerInput> {
|
||||
public readonly type = LIST_CONTAINER;
|
||||
private node?: HTMLElement;
|
||||
|
||||
constructor(input: ContainerInput, embeddableServices: EmbeddableStart) {
|
||||
super(input, { embeddableLoaded: {} }, embeddableServices.getEmbeddableFactory);
|
||||
}
|
||||
|
||||
getInheritedInput() {
|
||||
return {
|
||||
viewMode: this.input.viewMode,
|
||||
};
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
this.node = node;
|
||||
ReactDOM.render(<ListContainerComponent embeddable={this} />, node);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiSpacer, EuiFlexItem, EuiText, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
IContainer,
|
||||
withEmbeddableSubscription,
|
||||
ContainerInput,
|
||||
ContainerOutput,
|
||||
EmbeddablePanel,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
|
||||
interface Props {
|
||||
embeddable: IContainer;
|
||||
input: ContainerInput;
|
||||
output: ContainerOutput;
|
||||
}
|
||||
|
||||
function renderList(embeddable: IContainer, panels: ContainerInput['panels']) {
|
||||
let number = 0;
|
||||
const list = Object.values(panels).map((panel) => {
|
||||
number++;
|
||||
return (
|
||||
<EuiPanel key={number.toString()}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h3>{number}</h3>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EmbeddablePanel
|
||||
embeddable={() => embeddable.untilEmbeddableLoaded(panel.explicitInput.id)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
export function ListContainerComponentInner({ embeddable, input }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<h2 data-test-subj="listContainerTitle">{embeddable.getTitle()}</h2>
|
||||
<EuiSpacer size="l" />
|
||||
{renderList(embeddable, input.panels)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// You don't have to use this helper wrapper, but it handles a lot of the React boilerplate for
|
||||
// embeddables, like setting up the subscriptions to cause the component to refresh whenever
|
||||
// anything on input or output state changes. If you don't want that to happen (for example
|
||||
// if you expect something on input or output state to change frequently that your react
|
||||
// component does not care about, then you should probably hook this up manually).
|
||||
export const ListContainerComponent = withEmbeddableSubscription<
|
||||
ContainerInput,
|
||||
ContainerOutput,
|
||||
IContainer
|
||||
>(ListContainerComponentInner);
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
ContainerInput,
|
||||
EmbeddableStart,
|
||||
EmbeddableFactory,
|
||||
ContainerOutput,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { LIST_CONTAINER, ListContainer } from './list_container';
|
||||
|
||||
interface StartServices {
|
||||
embeddableServices: EmbeddableStart;
|
||||
}
|
||||
|
||||
export type ListContainerFactory = EmbeddableFactory<ContainerInput, ContainerOutput>;
|
||||
export class ListContainerFactoryDefinition
|
||||
implements EmbeddableFactoryDefinition<ContainerInput, ContainerOutput>
|
||||
{
|
||||
public readonly type = LIST_CONTAINER;
|
||||
public readonly isContainerType = true;
|
||||
|
||||
constructor(private getStartServices: () => Promise<StartServices>) {}
|
||||
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public create = async (initialInput: ContainerInput) => {
|
||||
const { embeddableServices } = await this.getStartServices();
|
||||
return new ListContainer(initialInput, embeddableServices);
|
||||
};
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('embeddableExamples.searchableListContainer.displayName', {
|
||||
defaultMessage: 'List container',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './migrations_embeddable';
|
||||
export * from './migrations_embeddable_factory';
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { MigrateFunction } from '@kbn/kibana-utils-plugin/common/persistable_state';
|
||||
import { EmbeddableInput } from '@kbn/embeddable-plugin/common';
|
||||
import { SimpleEmbeddableInput } from './migrations_embeddable_factory';
|
||||
|
||||
// before 7.3.0 this embeddable received a very simple input with a variable named `number`
|
||||
type SimpleEmbeddableInputV1 = EmbeddableInput & {
|
||||
number: number;
|
||||
};
|
||||
|
||||
type SimpleEmbeddable730MigrateFn = MigrateFunction<SimpleEmbeddableInputV1, SimpleEmbeddableInput>;
|
||||
|
||||
// when migrating old state we'll need to set a default title, or we should make title optional in the new state
|
||||
const defaultTitle = 'no title';
|
||||
|
||||
export const migrateToVersion2: SimpleEmbeddable730MigrateFn = (state) => {
|
||||
const newState: SimpleEmbeddableInput = { ...state, title: defaultTitle, value: state.number };
|
||||
return newState;
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
|
||||
import { SIMPLE_EMBEDDABLE, SimpleEmbeddableInput } from '.';
|
||||
|
||||
export class SimpleEmbeddable extends Embeddable<SimpleEmbeddableInput> {
|
||||
// The type of this embeddable. This will be used to find the appropriate factory
|
||||
// to instantiate this kind of embeddable.
|
||||
public readonly type = SIMPLE_EMBEDDABLE;
|
||||
|
||||
constructor(initialInput: SimpleEmbeddableInput, parent?: IContainer) {
|
||||
super(
|
||||
// Input state is irrelevant to this embeddable, just pass it along.
|
||||
initialInput,
|
||||
// Initial output state - this embeddable does not do anything with output, so just
|
||||
// pass along an empty object.
|
||||
{},
|
||||
// Optional parent component, this embeddable can optionally be rendered inside a container.
|
||||
parent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render yourself at the dom node using whatever framework you like, angular, react, or just plain
|
||||
* vanilla js.
|
||||
* @param node
|
||||
*/
|
||||
public render(node: HTMLElement) {
|
||||
const input = this.getInput();
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
node.innerHTML = `<div data-test-subj="simpleEmbeddable">${input.title} ${input.value}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is mostly relevant for time based embeddables which need to update data
|
||||
* even if EmbeddableInput has not changed at all.
|
||||
*/
|
||||
public reload() {}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
|
||||
import {
|
||||
IContainer,
|
||||
EmbeddableInput,
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableFactory,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { SimpleEmbeddable } from './migrations_embeddable';
|
||||
import { migrateToVersion2 } from './migration_definitions';
|
||||
|
||||
export const SIMPLE_EMBEDDABLE = 'SIMPLE_EMBEDDABLE';
|
||||
|
||||
// in 7.3.0 we added `title` to the input and renamed the `number` variable to `value`
|
||||
export type SimpleEmbeddableInput = EmbeddableInput & {
|
||||
title: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type SimpleEmbeddableFactory = EmbeddableFactory;
|
||||
export class SimpleEmbeddableFactoryDefinition
|
||||
implements EmbeddableFactoryDefinition<SimpleEmbeddableInput>
|
||||
{
|
||||
public readonly type = SIMPLE_EMBEDDABLE;
|
||||
public latestVersion = '2';
|
||||
|
||||
// we need to provide migration function every time we change the interface of our state
|
||||
public readonly migrations = {
|
||||
'2': migrateToVersion2,
|
||||
};
|
||||
|
||||
public extract(state: EmbeddableStateWithType) {
|
||||
// this embeddable does not store references to other saved objects
|
||||
return { state, references: [] };
|
||||
}
|
||||
|
||||
public inject(state: EmbeddableStateWithType) {
|
||||
// this embeddable does not store references to other saved objects
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* In our simple example, we let everyone have permissions to edit this. Most
|
||||
* embeddables should check the UI Capabilities service to be sure of
|
||||
* the right permissions.
|
||||
*/
|
||||
public async isEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async create(initialInput: SimpleEmbeddableInput, parent?: IContainer) {
|
||||
return new SimpleEmbeddable(initialInput, parent);
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('embeddableExamples.migrations.displayName', {
|
||||
defaultMessage: 'simple migration embeddable',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,28 +18,7 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
|||
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import {
|
||||
HelloWorldEmbeddableFactory,
|
||||
HELLO_WORLD_EMBEDDABLE,
|
||||
HelloWorldEmbeddableFactoryDefinition,
|
||||
} from './hello_world';
|
||||
|
||||
import {
|
||||
LIST_CONTAINER,
|
||||
ListContainerFactoryDefinition,
|
||||
ListContainerFactory,
|
||||
} from './list_container';
|
||||
|
||||
import {
|
||||
SIMPLE_EMBEDDABLE,
|
||||
SimpleEmbeddableFactory,
|
||||
SimpleEmbeddableFactoryDefinition,
|
||||
} from './migrations';
|
||||
import {
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
FilterDebuggerEmbeddableFactory,
|
||||
FilterDebuggerEmbeddableFactoryDefinition,
|
||||
} from './filter_debugger';
|
||||
import { registerCreateEuiMarkdownAction } from './react_embeddables/eui_markdown/create_eui_markdown_action';
|
||||
import { registerCreateFieldListAction } from './react_embeddables/field_list/create_field_list_action';
|
||||
import { registerAddSearchPanelAction } from './react_embeddables/search/register_add_search_panel_action';
|
||||
|
@ -63,52 +42,12 @@ export interface StartDeps {
|
|||
fieldFormats: FieldFormatsStart;
|
||||
}
|
||||
|
||||
interface ExampleEmbeddableFactories {
|
||||
getHelloWorldEmbeddableFactory: () => HelloWorldEmbeddableFactory;
|
||||
getListContainerEmbeddableFactory: () => ListContainerFactory;
|
||||
getMigrationsEmbeddableFactory: () => SimpleEmbeddableFactory;
|
||||
getFilterDebuggerEmbeddableFactory: () => FilterDebuggerEmbeddableFactory;
|
||||
}
|
||||
|
||||
export interface StartApi {
|
||||
createSampleData: () => Promise<void>;
|
||||
factories: ExampleEmbeddableFactories;
|
||||
}
|
||||
|
||||
export class EmbeddableExamplesPlugin implements Plugin<void, StartApi, SetupDeps, StartDeps> {
|
||||
private exampleEmbeddableFactories: Partial<ExampleEmbeddableFactories> = {};
|
||||
|
||||
export class EmbeddableExamplesPlugin implements Plugin<void, void, SetupDeps, StartDeps> {
|
||||
public setup(core: CoreSetup<StartDeps>, { embeddable, developerExamples }: SetupDeps) {
|
||||
setupApp(core, developerExamples);
|
||||
|
||||
this.exampleEmbeddableFactories.getHelloWorldEmbeddableFactory =
|
||||
embeddable.registerEmbeddableFactory(
|
||||
HELLO_WORLD_EMBEDDABLE,
|
||||
new HelloWorldEmbeddableFactoryDefinition()
|
||||
);
|
||||
|
||||
this.exampleEmbeddableFactories.getMigrationsEmbeddableFactory =
|
||||
embeddable.registerEmbeddableFactory(
|
||||
SIMPLE_EMBEDDABLE,
|
||||
new SimpleEmbeddableFactoryDefinition()
|
||||
);
|
||||
|
||||
this.exampleEmbeddableFactories.getListContainerEmbeddableFactory =
|
||||
embeddable.registerEmbeddableFactory(
|
||||
LIST_CONTAINER,
|
||||
new ListContainerFactoryDefinition(async () => ({
|
||||
embeddableServices: (await core.getStartServices())[1].embeddable,
|
||||
}))
|
||||
);
|
||||
|
||||
this.exampleEmbeddableFactories.getFilterDebuggerEmbeddableFactory =
|
||||
embeddable.registerEmbeddableFactory(
|
||||
FILTER_DEBUGGER_EMBEDDABLE,
|
||||
new FilterDebuggerEmbeddableFactoryDefinition()
|
||||
);
|
||||
}
|
||||
|
||||
public start(core: CoreStart, deps: StartDeps): StartApi {
|
||||
public start(core: CoreStart, deps: StartDeps) {
|
||||
registerCreateFieldListAction(deps.uiActions);
|
||||
registerReactEmbeddableFactory(FIELD_LIST_ID, async () => {
|
||||
const { getFieldListFactory } = await import(
|
||||
|
@ -132,11 +71,6 @@ export class EmbeddableExamplesPlugin implements Plugin<void, StartApi, SetupDep
|
|||
);
|
||||
return getSearchEmbeddableFactory(deps);
|
||||
});
|
||||
|
||||
return {
|
||||
createSampleData: async () => {},
|
||||
factories: this.exampleEmbeddableFactories as ExampleEmbeddableFactories,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginInitializer } from '@kbn/core/server';
|
||||
|
||||
export const plugin: PluginInitializer<void, void> = async () => {
|
||||
const { EmbeddableExamplesPlugin } = await import('./plugin');
|
||||
return new EmbeddableExamplesPlugin();
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
|
||||
import { EmbeddableSetup } from '@kbn/embeddable-plugin/server';
|
||||
|
||||
export interface EmbeddableExamplesSetupDependencies {
|
||||
embeddable: EmbeddableSetup;
|
||||
}
|
||||
|
||||
export class EmbeddableExamplesPlugin
|
||||
implements Plugin<void, void, EmbeddableExamplesSetupDependencies>
|
||||
{
|
||||
public setup(core: CoreSetup, { embeddable }: EmbeddableExamplesSetupDependencies) {}
|
||||
|
||||
public start(core: CoreStart) {}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -14,7 +14,6 @@
|
|||
"exclude": ["target/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/presentation-publishing",
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
"embeddable",
|
||||
"navigation",
|
||||
"unifiedSearch",
|
||||
"developerExamples",
|
||||
"embeddableExamples"
|
||||
"developerExamples"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { DashboardListingTable } from '@kbn/dashboard-plugin/public';
|
|||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
||||
|
||||
import { DualReduxExample } from './dual_redux_example';
|
||||
import { PortableDashboardsExampleStartDeps } from './plugin';
|
||||
import { StartDeps } from './plugin';
|
||||
import { StaticByValueExample } from './static_by_value_example';
|
||||
import { StaticByReferenceExample } from './static_by_reference_example';
|
||||
import { DynamicByReferenceExample } from './dynamically_add_panels_example';
|
||||
|
@ -27,7 +27,7 @@ const DASHBOARD_DEMO_PATH = '/dashboardDemo';
|
|||
const DASHBOARD_LIST_PATH = '/listingDemo';
|
||||
|
||||
export const renderApp = async (
|
||||
{ data, dashboard }: PortableDashboardsExampleStartDeps,
|
||||
{ data, dashboard }: StartDeps,
|
||||
{ element, history }: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
|
@ -42,8 +42,8 @@ const PortableDashboardsDemos = ({
|
|||
dashboard,
|
||||
history,
|
||||
}: {
|
||||
data: PortableDashboardsExampleStartDeps['data'];
|
||||
dashboard: PortableDashboardsExampleStartDeps['dashboard'];
|
||||
data: StartDeps['data'];
|
||||
dashboard: StartDeps['dashboard'];
|
||||
history: AppMountParameters['history'];
|
||||
}) => {
|
||||
return (
|
||||
|
@ -69,8 +69,8 @@ const DashboardsDemo = ({
|
|||
dashboard,
|
||||
}: {
|
||||
history: AppMountParameters['history'];
|
||||
data: PortableDashboardsExampleStartDeps['data'];
|
||||
dashboard: PortableDashboardsExampleStartDeps['dashboard'];
|
||||
data: StartDeps['data'];
|
||||
dashboard: StartDeps['dashboard'];
|
||||
}) => {
|
||||
const { loading, value: dataviewResults } = useAsync(async () => {
|
||||
const dataViews = await data.dataViews.find('kibana_sample_data_logs');
|
||||
|
|
|
@ -7,3 +7,5 @@
|
|||
*/
|
||||
|
||||
export const PLUGIN_ID = 'portableDashboardExamples';
|
||||
|
||||
export const FILTER_DEBUGGER_EMBEDDABLE_ID = 'FILTER_DEBUGGER_EMBEDDABLE_ID';
|
||||
|
|
|
@ -13,12 +13,13 @@ import type { DataView } from '@kbn/data-views-plugin/public';
|
|||
import { EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { controlGroupInputBuilder } from '@kbn/controls-plugin/public';
|
||||
import { getDefaultControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { FILTER_DEBUGGER_EMBEDDABLE } from '@kbn/embeddable-examples-plugin/public';
|
||||
import {
|
||||
AwaitingDashboardAPI,
|
||||
DashboardRenderer,
|
||||
DashboardCreationOptions,
|
||||
} from '@kbn/dashboard-plugin/public';
|
||||
import { apiHasUniqueId } from '@kbn/presentation-publishing';
|
||||
import { FILTER_DEBUGGER_EMBEDDABLE_ID } from './constants';
|
||||
|
||||
export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView }) => {
|
||||
const [dashboard, setDashboard] = useState<AwaitingDashboardAPI>();
|
||||
|
@ -27,14 +28,23 @@ export const DashboardWithControlsExample = ({ dataView }: { dataView: DataView
|
|||
useEffect(() => {
|
||||
if (!dashboard) return;
|
||||
(async () => {
|
||||
const embeddable = await dashboard.addNewEmbeddable(FILTER_DEBUGGER_EMBEDDABLE, {});
|
||||
const prevPanelState = dashboard.getExplicitInput().panels[embeddable.id];
|
||||
const api = await dashboard.addNewPanel(
|
||||
{
|
||||
panelType: FILTER_DEBUGGER_EMBEDDABLE_ID,
|
||||
initialState: {},
|
||||
},
|
||||
true
|
||||
);
|
||||
if (!apiHasUniqueId(api)) {
|
||||
return;
|
||||
}
|
||||
const prevPanelState = dashboard.getExplicitInput().panels[api.uuid];
|
||||
// resize the new panel so that it fills up the entire width of the dashboard
|
||||
dashboard.updateInput({
|
||||
panels: {
|
||||
[embeddable.id]: {
|
||||
[api.uuid]: {
|
||||
...prevPanelState,
|
||||
gridData: { i: embeddable.id, x: 0, y: 0, w: 48, h: 12 },
|
||||
gridData: { i: api.uuid, x: 0, y: 0, w: 48, h: 12 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { DefaultEmbeddableApi, ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
PublishesUnifiedSearch,
|
||||
useStateFromPublishingSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { EuiCodeBlock, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { FILTER_DEBUGGER_EMBEDDABLE_ID } from './constants';
|
||||
|
||||
export type Api = DefaultEmbeddableApi<{}>;
|
||||
|
||||
export const factory: ReactEmbeddableFactory<{}, Api> = {
|
||||
type: FILTER_DEBUGGER_EMBEDDABLE_ID,
|
||||
deserializeState: () => {
|
||||
return {};
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const api = buildApi(
|
||||
{
|
||||
serializeState: () => {
|
||||
return {
|
||||
rawState: {},
|
||||
references: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const filters = useStateFromPublishingSubject(
|
||||
(parentApi as PublishesUnifiedSearch)?.filters$
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
css={css`
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
`}
|
||||
className="eui-yScrollWithShadows"
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiTitle>
|
||||
<h2>Filters</h2>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock language="JSON">{JSON.stringify(filters, undefined, 1)}</EuiCodeBlock>
|
||||
</EuiPanel>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -6,32 +6,28 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { AppMountParameters, CoreSetup, Plugin } from '@kbn/core/public';
|
||||
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DashboardStart } from '@kbn/dashboard-plugin/public';
|
||||
import { registerReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import img from './portable_dashboard_image.png';
|
||||
import { PLUGIN_ID } from './constants';
|
||||
import { FILTER_DEBUGGER_EMBEDDABLE_ID, PLUGIN_ID } from './constants';
|
||||
|
||||
interface SetupDeps {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
}
|
||||
|
||||
export interface PortableDashboardsExampleStartDeps {
|
||||
export interface StartDeps {
|
||||
dashboard: DashboardStart;
|
||||
data: DataPublicPluginStart;
|
||||
navigation: NavigationPublicPluginStart;
|
||||
}
|
||||
|
||||
export class PortableDashboardsExamplePlugin
|
||||
implements Plugin<void, void, SetupDeps, PortableDashboardsExampleStartDeps>
|
||||
{
|
||||
public setup(
|
||||
core: CoreSetup<PortableDashboardsExampleStartDeps>,
|
||||
{ developerExamples }: SetupDeps
|
||||
) {
|
||||
export class PortableDashboardsExamplePlugin implements Plugin<void, void, SetupDeps, StartDeps> {
|
||||
public setup(core: CoreSetup<StartDeps>, { developerExamples }: SetupDeps) {
|
||||
core.application.register({
|
||||
id: PLUGIN_ID,
|
||||
title: 'Portable dashboard examples',
|
||||
|
@ -51,7 +47,12 @@ export class PortableDashboardsExamplePlugin
|
|||
});
|
||||
}
|
||||
|
||||
public async start(core: CoreStart, { dashboard }: PortableDashboardsExampleStartDeps) {}
|
||||
public async start() {
|
||||
registerReactEmbeddableFactory(FILTER_DEBUGGER_EMBEDDABLE_ID, async () => {
|
||||
const { factory } = await import('./filter_debugger_embeddable');
|
||||
return factory;
|
||||
});
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
"@kbn/data-views-plugin",
|
||||
"@kbn/visualizations-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/embeddable-examples-plugin",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/shared-ux-router"
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/presentation-publishing"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -516,7 +516,7 @@ export class DashboardContainer
|
|||
};
|
||||
this.updateInput({ panels: { ...otherPanels, [newId]: newPanel } });
|
||||
onSuccess(newId, newPanel.explicitInput.title);
|
||||
return;
|
||||
return await this.untilReactEmbeddableLoaded<ApiType>(newId);
|
||||
}
|
||||
|
||||
const embeddableFactory = getEmbeddableFactory(panelPackage.panelType);
|
||||
|
|
|
@ -355,6 +355,31 @@ export abstract class Container<
|
|||
});
|
||||
}
|
||||
|
||||
public async untilReactEmbeddableLoaded<ApiType>(id: string): Promise<ApiType | undefined> {
|
||||
if (!this.input.panels[id]) {
|
||||
throw new PanelNotFoundError();
|
||||
}
|
||||
|
||||
if (this.children$.value[id]) {
|
||||
return this.children$.value[id] as ApiType;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const subscription = merge(this.children$, this.getInput$()).subscribe(() => {
|
||||
if (this.children$.value[id]) {
|
||||
subscription.unsubscribe();
|
||||
resolve(this.children$.value[id] as ApiType);
|
||||
}
|
||||
|
||||
// If we hit this, the panel was removed before the embeddable finished loading.
|
||||
if (this.input.panels[id] === undefined) {
|
||||
subscription.unsubscribe();
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async getExplicitInputIsEqual(lastInput: TContainerInput) {
|
||||
const { panels: lastPanels, ...restOfLastInput } = lastInput;
|
||||
const { panels: currentPanels, ...restOfCurrentInput } = this.getExplicitInput();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue