mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Replaced usages of the EmbeddablePanel component with a shortcut panel API
This commit is contained in:
parent
4f8d5a40b9
commit
502e2c9f88
23 changed files with 192 additions and 239 deletions
|
@ -65,6 +65,7 @@ export function setStartServices(npStart: NpStart) {
|
|||
visualizationsServices.setCapabilities(npStart.core.application.capabilities);
|
||||
visualizationsServices.setHttp(npStart.core.http);
|
||||
visualizationsServices.setApplication(npStart.core.application);
|
||||
visualizationsServices.setEmbeddable(npStart.plugins.embeddable);
|
||||
visualizationsServices.setSavedObjects(npStart.core.savedObjects);
|
||||
visualizationsServices.setIndexPatterns(npStart.plugins.data.indexPatterns);
|
||||
visualizationsServices.setFilterManager(npStart.plugins.data.query.filterManager);
|
||||
|
|
|
@ -188,7 +188,11 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={this.options}>
|
||||
<DashboardViewport renderEmpty={this.renderEmpty} container={this} />
|
||||
<DashboardViewport
|
||||
renderEmpty={this.renderEmpty}
|
||||
container={this}
|
||||
PanelComponent={this.options.embeddable.EmbeddablePanel}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</I18nProvider>,
|
||||
dom
|
||||
|
|
|
@ -80,6 +80,7 @@ function prepare(props?: Partial<DashboardGridProps>) {
|
|||
dashboardContainer = new DashboardContainer(initialInput, options);
|
||||
const defaultTestProps: DashboardGridProps = {
|
||||
container: dashboardContainer,
|
||||
PanelComponent: () => <div />,
|
||||
kibana: null as any,
|
||||
intl: null as any,
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ import React from 'react';
|
|||
import { Subscription } from 'rxjs';
|
||||
import ReactGridLayout, { Layout } from 'react-grid-layout';
|
||||
import { GridData } from '../../../../common';
|
||||
import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin';
|
||||
import { ViewMode, EmbeddableChildPanel, EmbeddableStart } from '../../../embeddable_plugin';
|
||||
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants';
|
||||
import { DashboardPanelState } from '../types';
|
||||
import { withKibana } from '../../../../../kibana_react/public';
|
||||
|
@ -115,6 +115,7 @@ const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
|
|||
|
||||
export interface DashboardGridProps extends ReactIntl.InjectedIntlProps {
|
||||
kibana: DashboardReactContextValue;
|
||||
PanelComponent: EmbeddableStart['EmbeddablePanel'];
|
||||
container: DashboardContainer;
|
||||
}
|
||||
|
||||
|
@ -271,14 +272,7 @@ class DashboardGridUi extends React.Component<DashboardGridProps, State> {
|
|||
<EmbeddableChildPanel
|
||||
embeddableId={panel.explicitInput.id}
|
||||
container={this.props.container}
|
||||
getActions={this.props.kibana.services.uiActions.getTriggerCompatibleActions}
|
||||
getEmbeddableFactory={this.props.kibana.services.embeddable.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={this.props.kibana.services.embeddable.getEmbeddableFactories}
|
||||
overlays={this.props.kibana.services.overlays}
|
||||
application={this.props.kibana.services.application}
|
||||
notifications={this.props.kibana.services.notifications}
|
||||
inspector={this.props.kibana.services.inspector}
|
||||
SavedObjectFinder={this.props.kibana.services.SavedObjectFinder}
|
||||
PanelComponent={this.props.PanelComponent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -87,6 +87,7 @@ function getProps(
|
|||
dashboardContainer = new DashboardContainer(input, options);
|
||||
const defaultTestProps: DashboardViewportProps = {
|
||||
container: dashboardContainer,
|
||||
PanelComponent: () => <div />,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { PanelState } from '../../../embeddable_plugin';
|
||||
import { PanelState, EmbeddableStart } from '../../../embeddable_plugin';
|
||||
import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container';
|
||||
import { DashboardGrid } from '../grid';
|
||||
import { context } from '../../../../../kibana_react/public';
|
||||
|
@ -27,6 +27,7 @@ import { context } from '../../../../../kibana_react/public';
|
|||
export interface DashboardViewportProps {
|
||||
container: DashboardContainer;
|
||||
renderEmpty?: () => React.ReactNode;
|
||||
PanelComponent: EmbeddableStart['EmbeddablePanel'];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -120,7 +121,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
|
|||
}
|
||||
|
||||
private renderContainerScreen() {
|
||||
const { container } = this.props;
|
||||
const { container, PanelComponent } = this.props;
|
||||
const {
|
||||
isEmbeddedExternally,
|
||||
isFullScreenMode,
|
||||
|
@ -143,7 +144,7 @@ export class DashboardViewport extends React.Component<DashboardViewportProps, S
|
|||
toggleChrome={!isEmbeddedExternally}
|
||||
/>
|
||||
)}
|
||||
<DashboardGrid container={container} />
|
||||
<DashboardGrid container={container} PanelComponent={PanelComponent} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
// eslint-disable-next-line
|
||||
import { inspectorPluginMock } from '../../../../inspector/public/mocks';
|
||||
import { mount } from 'enzyme';
|
||||
import { embeddablePluginMock } from '../../mocks';
|
||||
import { embeddablePluginMock, createEmbeddablePanelMock } from '../../mocks';
|
||||
|
||||
test('EmbeddableChildPanel renders an embeddable when it is done loading', async () => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
@ -58,18 +58,17 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async
|
|||
|
||||
expect(newEmbeddable.id).toBeDefined();
|
||||
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
getEmbeddableFactory,
|
||||
inspector,
|
||||
});
|
||||
|
||||
const component = mount(
|
||||
<EmbeddableChildPanel
|
||||
container={container}
|
||||
embeddableId={newEmbeddable.id}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
application={{} as any}
|
||||
overlays={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
PanelComponent={testPanel}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -97,19 +96,9 @@ test(`EmbeddableChildPanel renders an error message if the factory doesn't exist
|
|||
{ getEmbeddableFactory } as any
|
||||
);
|
||||
|
||||
const testPanel = createEmbeddablePanelMock({ inspector });
|
||||
const component = mount(
|
||||
<EmbeddableChildPanel
|
||||
container={container}
|
||||
embeddableId={'1'}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={(() => []) as any}
|
||||
getEmbeddableFactory={(() => undefined) as any}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={{} as any}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
<EmbeddableChildPanel container={container} embeddableId={'1'} PanelComponent={testPanel} />
|
||||
);
|
||||
|
||||
await nextTick();
|
||||
|
|
|
@ -22,12 +22,7 @@ import React from 'react';
|
|||
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { UiActionsService } from 'src/plugins/ui_actions/public';
|
||||
|
||||
import { Start as InspectorStartContract } from 'src/plugins/inspector/public';
|
||||
import { ErrorEmbeddable, IEmbeddable } from '../embeddables';
|
||||
import { EmbeddablePanel } from '../panel';
|
||||
import { IContainer } from './i_container';
|
||||
import { EmbeddableStart } from '../../plugin';
|
||||
|
||||
|
@ -35,14 +30,7 @@ export interface EmbeddableChildPanelProps {
|
|||
embeddableId: string;
|
||||
className?: string;
|
||||
container: IContainer;
|
||||
getActions: UiActionsService['getTriggerCompatibleActions'];
|
||||
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
|
||||
getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
|
||||
overlays: CoreStart['overlays'];
|
||||
notifications: CoreStart['notifications'];
|
||||
application: CoreStart['application'];
|
||||
inspector: InspectorStartContract;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
PanelComponent: EmbeddableStart['EmbeddablePanel'];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -87,6 +75,7 @@ export class EmbeddableChildPanel extends React.Component<EmbeddableChildPanelPr
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { PanelComponent } = this.props;
|
||||
const classes = classNames('embPanel', {
|
||||
'embPanel-isLoading': this.state.loading,
|
||||
});
|
||||
|
@ -96,17 +85,7 @@ export class EmbeddableChildPanel extends React.Component<EmbeddableChildPanelPr
|
|||
{this.state.loading || !this.embeddable ? (
|
||||
<EuiLoadingChart size="l" mono />
|
||||
) : (
|
||||
<EmbeddablePanel
|
||||
embeddable={this.embeddable}
|
||||
getActions={this.props.getActions}
|
||||
getEmbeddableFactory={this.props.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={this.props.getAllEmbeddableFactories}
|
||||
overlays={this.props.overlays}
|
||||
application={this.props.application}
|
||||
notifications={this.props.notifications}
|
||||
inspector={this.props.inspector}
|
||||
SavedObjectFinder={this.props.SavedObjectFinder}
|
||||
/>
|
||||
<PanelComponent embeddable={this.embeddable} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -19,9 +19,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { UiActionsService } from 'src/plugins/ui_actions/public';
|
||||
import { Start as InspectorStartContract } from 'src/plugins/inspector/public';
|
||||
import { Container, ViewMode, ContainerInput } from '../..';
|
||||
import { HelloWorldContainerComponent } from './hello_world_container_component';
|
||||
import { EmbeddableStart } from '../../../plugin';
|
||||
|
@ -45,14 +42,8 @@ interface HelloWorldContainerInput extends ContainerInput {
|
|||
}
|
||||
|
||||
interface HelloWorldContainerOptions {
|
||||
getActions: UiActionsService['getTriggerCompatibleActions'];
|
||||
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
|
||||
getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
|
||||
overlays: CoreStart['overlays'];
|
||||
application: CoreStart['application'];
|
||||
notifications: CoreStart['notifications'];
|
||||
inspector: InspectorStartContract;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
panelComponent: EmbeddableStart['EmbeddablePanel'];
|
||||
}
|
||||
|
||||
export class HelloWorldContainer extends Container<InheritedInput, HelloWorldContainerInput> {
|
||||
|
@ -78,14 +69,7 @@ export class HelloWorldContainer extends Container<InheritedInput, HelloWorldCon
|
|||
<I18nProvider>
|
||||
<HelloWorldContainerComponent
|
||||
container={this}
|
||||
getActions={this.options.getActions}
|
||||
getAllEmbeddableFactories={this.options.getAllEmbeddableFactories}
|
||||
getEmbeddableFactory={this.options.getEmbeddableFactory}
|
||||
overlays={this.options.overlays}
|
||||
application={this.options.application}
|
||||
notifications={this.options.notifications}
|
||||
inspector={this.options.inspector}
|
||||
SavedObjectFinder={this.options.SavedObjectFinder}
|
||||
panelComponent={this.options.panelComponent}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
node
|
||||
|
|
|
@ -20,22 +20,12 @@ import React, { Component, RefObject } from 'react';
|
|||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { UiActionsService } from 'src/plugins/ui_actions/public';
|
||||
import { Start as InspectorStartContract } from 'src/plugins/inspector/public';
|
||||
import { IContainer, PanelState, EmbeddableChildPanel } from '../..';
|
||||
import { EmbeddableStart } from '../../../plugin';
|
||||
|
||||
interface Props {
|
||||
container: IContainer;
|
||||
getActions: UiActionsService['getTriggerCompatibleActions'];
|
||||
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
|
||||
getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
|
||||
overlays: CoreStart['overlays'];
|
||||
application: CoreStart['application'];
|
||||
notifications: CoreStart['notifications'];
|
||||
inspector: InspectorStartContract;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
panelComponent: EmbeddableStart['EmbeddablePanel'];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -108,14 +98,7 @@ export class HelloWorldContainerComponent extends Component<Props, State> {
|
|||
<EmbeddableChildPanel
|
||||
container={this.props.container}
|
||||
embeddableId={panelState.explicitInput.id}
|
||||
getActions={this.props.getActions}
|
||||
getEmbeddableFactory={this.props.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={this.props.getAllEmbeddableFactories}
|
||||
overlays={this.props.overlays}
|
||||
notifications={this.props.notifications}
|
||||
application={this.props.application}
|
||||
inspector={this.props.inspector}
|
||||
SavedObjectFinder={this.props.SavedObjectFinder}
|
||||
PanelComponent={this.props.panelComponent}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -16,14 +16,20 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
EmbeddableStart,
|
||||
EmbeddableSetup,
|
||||
EmbeddableSetupDependencies,
|
||||
EmbeddableStartDependencies,
|
||||
IEmbeddable,
|
||||
EmbeddablePanel,
|
||||
} from '.';
|
||||
import { EmbeddablePublicPlugin } from './plugin';
|
||||
import { coreMock } from '../../../core/public/mocks';
|
||||
import { UiActionsService } from './lib/ui_actions';
|
||||
import { CoreStart } from '../../../core/public';
|
||||
import { Start as InspectorStart } from '../../inspector/public';
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { inspectorPluginMock } from '../../inspector/public/mocks';
|
||||
|
@ -33,6 +39,42 @@ import { uiActionsPluginMock } from '../../ui_actions/public/mocks';
|
|||
export type Setup = jest.Mocked<EmbeddableSetup>;
|
||||
export type Start = jest.Mocked<EmbeddableStart>;
|
||||
|
||||
interface CreateEmbeddablePanelMockArgs {
|
||||
getActions: UiActionsService['getTriggerCompatibleActions'];
|
||||
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
|
||||
getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
|
||||
overlays: CoreStart['overlays'];
|
||||
notifications: CoreStart['notifications'];
|
||||
application: CoreStart['application'];
|
||||
inspector: InspectorStart;
|
||||
SavedObjectFinder: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export const createEmbeddablePanelMock = ({
|
||||
getActions,
|
||||
getEmbeddableFactory,
|
||||
getAllEmbeddableFactories,
|
||||
overlays,
|
||||
notifications,
|
||||
application,
|
||||
inspector,
|
||||
SavedObjectFinder,
|
||||
}: Partial<CreateEmbeddablePanelMockArgs>) => {
|
||||
return ({ embeddable }: { embeddable: IEmbeddable }) => (
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={getActions || (() => Promise.resolve([]))}
|
||||
getAllEmbeddableFactories={getAllEmbeddableFactories || ((() => []) as any)}
|
||||
getEmbeddableFactory={getEmbeddableFactory || ((() => undefined) as any)}
|
||||
notifications={notifications || ({} as any)}
|
||||
application={application || ({} as any)}
|
||||
overlays={overlays || ({} as any)}
|
||||
inspector={inspector || ({} as any)}
|
||||
SavedObjectFinder={SavedObjectFinder || (() => null)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const createSetupContract = (): Setup => {
|
||||
const setupContract: Setup = {
|
||||
registerEmbeddableFactory: jest.fn(),
|
|
@ -31,8 +31,7 @@ import {
|
|||
FilterableEmbeddableInput,
|
||||
} from '../lib/test_samples';
|
||||
// eslint-disable-next-line
|
||||
import { inspectorPluginMock } from '../../../../plugins/inspector/public/mocks';
|
||||
import { esFilters } from '../../../../plugins/data/public';
|
||||
import { esFilters } from '../../../data/public';
|
||||
|
||||
test('ApplyFilterAction applies the filter to the root of the container tree', async () => {
|
||||
const { doStart, setup } = testPlugin();
|
||||
|
@ -95,26 +94,16 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a
|
|||
});
|
||||
|
||||
test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => {
|
||||
const { doStart, coreStart, setup } = testPlugin();
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
const { doStart, setup } = testPlugin();
|
||||
|
||||
const factory = new FilterableEmbeddableFactory();
|
||||
setup.registerEmbeddableFactory(factory.type, factory);
|
||||
const api = doStart();
|
||||
const applyFilterAction = createFilterAction();
|
||||
const parent = new HelloWorldContainer(
|
||||
{ id: 'root', panels: {} },
|
||||
{
|
||||
getActions: () => Promise.resolve([]),
|
||||
getEmbeddableFactory: api.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: api.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector,
|
||||
SavedObjectFinder: () => null,
|
||||
}
|
||||
);
|
||||
|
||||
const parent = new HelloWorldContainer({ id: 'root', panels: {} }, {
|
||||
getEmbeddableFactory: api.getEmbeddableFactory,
|
||||
} as any);
|
||||
const embeddable = await parent.addNewEmbeddable<
|
||||
FilterableContainerInput,
|
||||
EmbeddableOutput,
|
||||
|
@ -130,27 +119,17 @@ test('ApplyFilterAction is incompatible if the root container does not accept a
|
|||
});
|
||||
|
||||
test('trying to execute on incompatible context throws an error ', async () => {
|
||||
const { doStart, coreStart, setup } = testPlugin();
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
const { doStart, setup } = testPlugin();
|
||||
|
||||
const factory = new FilterableEmbeddableFactory();
|
||||
setup.registerEmbeddableFactory(factory.type, factory);
|
||||
|
||||
const api = doStart();
|
||||
const applyFilterAction = createFilterAction();
|
||||
const parent = new HelloWorldContainer(
|
||||
{ id: 'root', panels: {} },
|
||||
{
|
||||
getActions: () => Promise.resolve([]),
|
||||
getEmbeddableFactory: api.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: api.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector,
|
||||
SavedObjectFinder: () => null,
|
||||
}
|
||||
);
|
||||
|
||||
const parent = new HelloWorldContainer({ id: 'root', panels: {} }, {
|
||||
getEmbeddableFactory: api.getEmbeddableFactory,
|
||||
} as any);
|
||||
|
||||
const embeddable = await parent.addNewEmbeddable<
|
||||
FilterableContainerInput,
|
||||
|
|
|
@ -48,6 +48,7 @@ import { coreMock } from '../../../../core/public/mocks';
|
|||
import { testPlugin } from './test_plugin';
|
||||
import { of } from './helpers';
|
||||
import { esFilters, Filter } from '../../../../plugins/data/public';
|
||||
import { createEmbeddablePanelMock } from '../mocks';
|
||||
|
||||
async function creatHelloWorldContainerAndEmbeddable(
|
||||
containerInput: ContainerInput = { id: 'hello', panels: {} },
|
||||
|
@ -68,15 +69,18 @@ async function creatHelloWorldContainerAndEmbeddable(
|
|||
|
||||
const start = doStart();
|
||||
|
||||
const container = new HelloWorldContainer(containerInput, {
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
});
|
||||
|
||||
const container = new HelloWorldContainer(containerInput, {
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
panelComponent: testPanel,
|
||||
});
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
|
@ -88,7 +92,7 @@ async function creatHelloWorldContainerAndEmbeddable(
|
|||
throw new Error('Error adding embeddable');
|
||||
}
|
||||
|
||||
return { container, embeddable, coreSetup, coreStart, setup, start, uiActions };
|
||||
return { container, embeddable, coreSetup, coreStart, setup, start, uiActions, testPanel };
|
||||
}
|
||||
|
||||
test('Container initializes embeddables', async (done) => {
|
||||
|
@ -131,7 +135,8 @@ test('Container.addNewEmbeddable', async () => {
|
|||
});
|
||||
|
||||
test('Container.removeEmbeddable removes and cleans up', async (done) => {
|
||||
const { start, coreStart, uiActions } = await creatHelloWorldContainerAndEmbeddable();
|
||||
const { start, testPanel } = await creatHelloWorldContainerAndEmbeddable();
|
||||
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
|
@ -143,14 +148,8 @@ test('Container.removeEmbeddable removes and cleans up', async (done) => {
|
|||
},
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
|
@ -323,15 +322,17 @@ test(`Container updates its state when a child's input is updated`, async (done)
|
|||
// Make sure a brand new container built off the output of container also creates an embeddable
|
||||
// with "Dr.", not the default the embeddable was first added with. Makes sure changed input
|
||||
// is preserved with the container.
|
||||
const containerClone = new HelloWorldContainer(container.getInput(), {
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
notifications: coreStart.notifications,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
});
|
||||
const containerClone = new HelloWorldContainer(container.getInput(), {
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
panelComponent: testPanel,
|
||||
});
|
||||
const cloneSubscription = Rx.merge(
|
||||
containerClone.getOutput$(),
|
||||
|
@ -575,6 +576,14 @@ test('Container changes made directly after adding a new embeddable are propagat
|
|||
|
||||
const start = doStart();
|
||||
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
|
@ -582,14 +591,8 @@ test('Container changes made directly after adding a new embeddable are propagat
|
|||
viewMode: ViewMode.EDIT,
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -701,20 +704,22 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in
|
|||
coreMock.createStart()
|
||||
);
|
||||
const start = doStart();
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
panels: {},
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -731,6 +736,14 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
|
|||
const factory = new HelloWorldEmbeddableFactory();
|
||||
setup.registerEmbeddableFactory(factory.type, factory);
|
||||
const start = doStart();
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
|
@ -742,14 +755,8 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy
|
|||
},
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -771,6 +778,14 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
|
|||
setup.registerEmbeddableFactory(factory.type, factory);
|
||||
|
||||
const start = doStart();
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
|
@ -782,14 +797,8 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem
|
|||
},
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -812,6 +821,14 @@ test('adding a panel then subsequently removing it before its loaded removes the
|
|||
});
|
||||
setup.registerEmbeddableFactory(factory.type, factory);
|
||||
const start = doStart();
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
|
@ -823,14 +840,8 @@ test('adding a panel then subsequently removing it before its loaded removes the
|
|||
},
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import { testPlugin } from './test_plugin';
|
|||
import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal';
|
||||
import { mount } from 'enzyme';
|
||||
import { EmbeddableStart } from '../plugin';
|
||||
import { createEmbeddablePanelMock } from '../mocks';
|
||||
|
||||
let api: EmbeddableStart;
|
||||
let container: Container;
|
||||
|
@ -55,17 +56,20 @@ beforeEach(async () => {
|
|||
setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory);
|
||||
|
||||
api = doStart();
|
||||
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: api.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: api.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
container = new HelloWorldContainer(
|
||||
{ id: '123', panels: {} },
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: api.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: api.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
|
|
|
@ -36,6 +36,7 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world
|
|||
// eslint-disable-next-line
|
||||
import { coreMock } from '../../../../core/public/mocks';
|
||||
import { esFilters, Filter } from '../../../../plugins/data/public';
|
||||
import { createEmbeddablePanelMock } from '../mocks';
|
||||
|
||||
const { setup, doStart, coreStart, uiActions } = testPlugin(
|
||||
coreMock.createSetup(),
|
||||
|
@ -80,17 +81,19 @@ test('Explicit embeddable input mapped to undefined will default to inherited',
|
|||
});
|
||||
|
||||
test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async (done) => {
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{ id: 'hello', panels: {} },
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
notifications: coreStart.notifications,
|
||||
overlays: coreStart.overlays,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -121,6 +124,14 @@ test('Explicit embeddable input mapped to undefined with no inherited value will
|
|||
// but before the embeddable factory returns the embeddable, that the `inheritedChildInput` and
|
||||
// embeddable input comparisons won't cause explicit input to be set when it shouldn't.
|
||||
test('Explicit input tests in async situations', (done: () => void) => {
|
||||
const testPanel = createEmbeddablePanelMock({
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
overlays: coreStart.overlays,
|
||||
notifications: coreStart.notifications,
|
||||
application: coreStart.application,
|
||||
});
|
||||
const container = new HelloWorldContainer(
|
||||
{
|
||||
id: 'hello',
|
||||
|
@ -132,14 +143,8 @@ test('Explicit input tests in async situations', (done: () => void) => {
|
|||
},
|
||||
},
|
||||
{
|
||||
getActions: uiActions.getTriggerCompatibleActions,
|
||||
getAllEmbeddableFactories: start.getEmbeddableFactories,
|
||||
getEmbeddableFactory: start.getEmbeddableFactory,
|
||||
notifications: coreStart.notifications,
|
||||
overlays: coreStart.overlays,
|
||||
application: coreStart.application,
|
||||
inspector: {} as any,
|
||||
SavedObjectFinder: () => null,
|
||||
panelComponent: testPanel,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ const createInstance = async () => {
|
|||
inspector: inspectorPluginMock.createStartContract(),
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
application: applicationServiceMock.createStartContract(),
|
||||
embeddable: embeddablePluginMock.createStartContract(),
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
setChrome,
|
||||
setOverlays,
|
||||
setSavedSearchLoader,
|
||||
setEmbeddable,
|
||||
} from './services';
|
||||
import {
|
||||
VISUALIZE_EMBEDDABLE_TYPE,
|
||||
|
@ -54,7 +55,7 @@ import {
|
|||
createVisEmbeddableFromObject,
|
||||
} from './embeddable';
|
||||
import { ExpressionsSetup, ExpressionsStart } from '../../expressions/public';
|
||||
import { EmbeddableSetup } from '../../embeddable/public';
|
||||
import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public';
|
||||
import { visualization as visualizationFunction } from './expressions/visualization_function';
|
||||
import { visualization as visualizationRenderer } from './expressions/visualization_renderer';
|
||||
import { range as rangeExpressionFunction } from './expression_functions/range';
|
||||
|
@ -104,6 +105,7 @@ export interface VisualizationsSetupDeps {
|
|||
export interface VisualizationsStartDeps {
|
||||
data: DataPublicPluginStart;
|
||||
expressions: ExpressionsStart;
|
||||
embeddable: EmbeddableStart;
|
||||
inspector: InspectorStart;
|
||||
uiActions: UiActionsStart;
|
||||
application: ApplicationStart;
|
||||
|
@ -153,11 +155,12 @@ export class VisualizationsPlugin
|
|||
|
||||
public start(
|
||||
core: CoreStart,
|
||||
{ data, expressions, uiActions }: VisualizationsStartDeps
|
||||
{ data, expressions, uiActions, embeddable }: VisualizationsStartDeps
|
||||
): VisualizationsStart {
|
||||
const types = this.types.start();
|
||||
setI18n(core.i18n);
|
||||
setTypes(types);
|
||||
setEmbeddable(embeddable);
|
||||
setApplication(core.application);
|
||||
setCapabilities(core.application.capabilities);
|
||||
setHttp(core.http);
|
||||
|
|
|
@ -40,6 +40,7 @@ import { ExpressionsStart } from '../../../plugins/expressions/public';
|
|||
import { UiActionsStart } from '../../../plugins/ui_actions/public';
|
||||
import { SavedVisualizationsLoader } from './saved_visualizations';
|
||||
import { SavedObjectLoader } from '../../saved_objects/public';
|
||||
import { EmbeddableStart } from '../../embeddable/public';
|
||||
|
||||
export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings');
|
||||
|
||||
|
@ -49,6 +50,8 @@ export const [getHttp, setHttp] = createGetterSetter<HttpStart>('Http');
|
|||
|
||||
export const [getApplication, setApplication] = createGetterSetter<ApplicationStart>('Application');
|
||||
|
||||
export const [getEmbeddable, setEmbeddable] = createGetterSetter<EmbeddableStart>('Embeddable');
|
||||
|
||||
export const [getSavedObjects, setSavedObjects] = createGetterSetter<SavedObjectsStart>(
|
||||
'SavedObjects'
|
||||
);
|
||||
|
|
|
@ -11,8 +11,5 @@
|
|||
"visualizations",
|
||||
"dashboard"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"home",
|
||||
"share"
|
||||
]
|
||||
"optionalPlugins": ["home", "share"]
|
||||
}
|
||||
|
|
|
@ -120,7 +120,6 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState
|
|||
|
||||
const originatingApp = $route.current.params[EMBEDDABLE_ORIGINATING_APP_PARAM];
|
||||
removeQueryParam(history, EMBEDDABLE_ORIGINATING_APP_PARAM);
|
||||
|
||||
$scope.getOriginatingApp = () => originatingApp;
|
||||
|
||||
const visStateToEditorState = () => {
|
||||
|
|
|
@ -11,12 +11,10 @@ import { StartDeps } from '../../plugin';
|
|||
import {
|
||||
IEmbeddable,
|
||||
EmbeddableFactory,
|
||||
EmbeddablePanel,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
} from '../../../../../../src/plugins/embeddable/public';
|
||||
import { EmbeddableExpression } from '../../expression_types/embeddable';
|
||||
import { RendererStrings } from '../../../i18n';
|
||||
import { getSavedObjectFinder } from '../../../../../../src/plugins/saved_objects/public';
|
||||
import { embeddableInputToExpression } from './embeddable_input_to_expression';
|
||||
import { EmbeddableInput } from '../../expression_types';
|
||||
import { RendererHandlers } from '../../../types';
|
||||
|
@ -38,17 +36,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
|
|||
style={{ width: domNode.offsetWidth, height: domNode.offsetHeight, cursor: 'auto' }}
|
||||
>
|
||||
<I18nContext>
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddableObject}
|
||||
getActions={plugins.uiActions.getTriggerCompatibleActions}
|
||||
getEmbeddableFactory={plugins.embeddable.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={plugins.embeddable.getEmbeddableFactories}
|
||||
notifications={core.notifications}
|
||||
application={core.application}
|
||||
overlays={core.overlays}
|
||||
inspector={plugins.inspector}
|
||||
SavedObjectFinder={getSavedObjectFinder(core.savedObjects, core.uiSettings)}
|
||||
/>
|
||||
<plugins.embeddable.EmbeddablePanel embeddable={embeddableObject} />
|
||||
</I18nContext>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public';
|
||||
import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public';
|
||||
import { EmbeddableSetup } from 'src/plugins/embeddable/public';
|
||||
import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public';
|
||||
import { VisualizationsSetup } from 'src/plugins/visualizations/public';
|
||||
import { NavigationPublicPluginStart } from 'src/plugins/navigation/public';
|
||||
|
@ -38,7 +38,6 @@ export interface LensPluginSetupDependencies {
|
|||
|
||||
export interface LensPluginStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
embeddable: EmbeddableStart;
|
||||
expressions: ExpressionsStart;
|
||||
navigation: NavigationPublicPluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
|
|
|
@ -9,10 +9,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import { createPortalNode, InPortal } from 'react-reverse-portal';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import {
|
||||
EmbeddablePanel,
|
||||
ErrorEmbeddable,
|
||||
} from '../../../../../../../src/plugins/embeddable/public';
|
||||
import { ErrorEmbeddable } from '../../../../../../../src/plugins/embeddable/public';
|
||||
import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
|
||||
import { getIndexPatternTitleIdMapping } from '../../../common/hooks/api/helpers';
|
||||
import { useIndexPatterns } from '../../../common/hooks/use_index_patterns';
|
||||
|
@ -28,7 +25,6 @@ import { SetQuery } from './types';
|
|||
import { MapEmbeddable } from '../../../../../../legacy/plugins/maps/public';
|
||||
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
|
||||
import { useKibana, useUiSetting$ } from '../../../common/lib/kibana';
|
||||
import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public';
|
||||
|
||||
interface EmbeddableMapProps {
|
||||
maintainRatio?: boolean;
|
||||
|
@ -198,18 +194,7 @@ export const EmbeddedMapComponent = ({
|
|||
|
||||
<EmbeddableMap maintainRatio={!isIndexError}>
|
||||
{embeddable != null ? (
|
||||
<EmbeddablePanel
|
||||
data-test-subj="embeddable-panel"
|
||||
embeddable={embeddable}
|
||||
getActions={services.uiActions.getTriggerCompatibleActions}
|
||||
getEmbeddableFactory={services.embeddable.getEmbeddableFactory}
|
||||
getAllEmbeddableFactories={services.embeddable.getEmbeddableFactories}
|
||||
notifications={services.notifications}
|
||||
overlays={services.overlays}
|
||||
inspector={services.inspector}
|
||||
application={services.application}
|
||||
SavedObjectFinder={getSavedObjectFinder(services.savedObjects, services.uiSettings)}
|
||||
/>
|
||||
<services.embeddable.EmbeddablePanel embeddable={embeddable} />
|
||||
) : !isLoading && isIndexError ? (
|
||||
<IndexPatternsMissingPrompt data-test-subj="missing-prompt" />
|
||||
) : (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue