Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use embeddable v2 #39126

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5b02da9
Final Embeddable API V2 PR
stacey-gammon Jun 19, 2019
cd05714
fix: import discover embeddable scss file
stacey-gammon Jul 9, 2019
f795efa
address code review comments
stacey-gammon Jul 9, 2019
fa11c17
Merge branch 'master' of github.com:elastic/kibana into 2019-06-17-us…
stacey-gammon Jul 10, 2019
7d42540
Add a functional test that would have caught the bug... will look to …
stacey-gammon Jul 10, 2019
3febe3c
Fix bug cause by async loading calls and changes to parent input whil…
stacey-gammon Jul 10, 2019
6be0d89
Merge branch 'master' of github.com:elastic/kibana into 2019-06-17-us…
stacey-gammon Jul 10, 2019
2478a8c
remove outdated readme in dashboard folder
stacey-gammon Jul 10, 2019
f26cada
need to always refresh dashboard container, not just when "dirty"
stacey-gammon Jul 10, 2019
9556c70
Merge branch 'master' of github.com:elastic/kibana into 2019-06-17-us…
stacey-gammon Jul 11, 2019
b0fc507
add a wait, this issue started appearing right when I added this to t…
stacey-gammon Jul 11, 2019
a420da2
Remove test that kills kibana ci so it's not a blocker. jest test was…
stacey-gammon Jul 11, 2019
a8c2c94
fix issues when panel is added then removed before it completes loading
stacey-gammon Jul 11, 2019
09ea5b2
fix logic error with maps embeddable and isLayerTOCOpen
stacey-gammon Jul 12, 2019
5c7168a
Merge branch 'master' of github.com:elastic/kibana into 2019-06-17-us…
stacey-gammon Jul 12, 2019
1baf731
Merge branch 'master' of github.com:elastic/kibana into 2019-06-17-us…
stacey-gammon Jul 12, 2019
4129d16
Merge branch 'master' of github.com:elastic/kibana into 2019-06-17-us…
stacey-gammon Jul 12, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

// TODO: uncomment once the duplicate styles are removed from the dashboard app itself.
// MUST STAY AT THE BOTTOM BECAUSE OF DARK THEME IMPORTS
// @import './embeddable/index';
@import './embeddable/index';
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import 'uiExports/embeddableActions';
import 'uiExports/embeddableFactories';

export {
DASHBOARD_GRID_COLUMN_COUNT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
FilterableEmbeddable,
} from '../test_samples/embeddables/filterable_embeddable';
import { ERROR_EMBEDDABLE_TYPE } from '../embeddables/error_embeddable';
import { Filter, FilterStateStore } from '@kbn/es-query';
import { PanelNotFoundError } from './panel_not_found_error';

jest.mock('ui/new_platform');
Expand Down Expand Up @@ -418,67 +417,6 @@ test('Test nested reactions', async done => {
embeddable.updateInput({ nameTitle: 'Dr.' });
});

test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: Filter = {
$state: { store: FilterStateStore.APP_STATE },
meta: { disabled: false, alias: 'name', negate: false },
query: { match: {} },
};
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [derivedFilter] },
embeddableFactories
);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});

if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}

embeddable.updateInput({ filters: [] });

expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);

embeddable.updateInput({ filters: undefined });

expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([
derivedFilter,
]);
});

test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async done => {
const container = new HelloWorldContainer({ id: 'hello', panels: {} }, embeddableFactories);

const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});

if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}

embeddable.updateInput({ filters: [] });

expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);

const subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe(() => {
if (embeddable.getInput().filters === undefined) {
subscription.unsubscribe();
done();
}
});

embeddable.updateInput({ filters: undefined });
});

test('Panel removed from input state', async done => {
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [] },
Expand Down Expand Up @@ -749,3 +687,47 @@ test('untilEmbeddableLoaded rejects with an error if child is subsequently remov

container.updateInput({ panels: {} });
});

test('adding a panel then subsequently removing it before its loaded removes the panel', async done => {
embeddableFactories.clear();
embeddableFactories.set(
CONTACT_CARD_EMBEDDABLE,
new SlowContactCardEmbeddableFactory({ loadTickCount: 1 })
);

const container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { id: '123', firstName: 'Sam', lastName: 'Tarley' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
},
embeddableFactories
);

// Final state should be that the panel is removed.
Rx.merge(container.getInput$(), container.getOutput$()).subscribe(() => {
if (
container.getInput().panels['123'] === undefined &&
container.getOutput().embeddableLoaded['123'] === undefined &&
container.getInput().panels['456'] !== undefined &&
container.getOutput().embeddableLoaded['456'] === true
) {
done();
}
});

container.updateInput({ panels: {} });

container.updateInput({
panels: {
'456': {
explicitInput: { id: '456', firstName: 'a', lastName: 'b' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -322,14 +322,13 @@ export abstract class Container<
// switch over to inline creation we can probably clean this up, and force EmbeddableFactory.create to always
// return an embeddable, or throw an error.
if (embeddable) {
// The factory creation process may ask the user for input to update or override any input coming
// from the container.
const input = embeddable.getInput();
const newOrChangedInput = getKeys(input)
.filter(key => input[key] !== inputForChild[key])
.reduce((res, key) => Object.assign(res, { [key]: input[key] }), {});

if (embeddable.getOutput().savedObjectId || Object.keys(newOrChangedInput).length > 0) {
// make sure the panel wasn't removed in the mean time, since the embeddable creation is async
if (!this.input.panels[panel.explicitInput.id]) {
embeddable.destroy();
return;
}

if (embeddable.getOutput().savedObjectId) {
this.updateInput({
panels: {
...this.input.panels,
Expand All @@ -340,7 +339,6 @@ export abstract class Container<
: undefined),
explicitInput: {
...this.input.panels[panel.explicitInput.id].explicitInput,
...newOrChangedInput,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import React from 'react';
import { EuiLoadingChart } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';

import { ErrorEmbeddable, IEmbeddable } from 'plugins/embeddable_api';

import { Subscription } from 'rxjs';
import { ErrorEmbeddable, IEmbeddable } from '../embeddables';

import { EmbeddablePanel } from '../panel';
import { IContainer } from './i_container';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import '../ui_capabilities.test.mocks';

import { skip } from 'rxjs/operators';
import {
CONTACT_CARD_EMBEDDABLE,
HelloWorldContainer,
FilterableContainer,
FILTERABLE_EMBEDDABLE,
FilterableEmbeddableFactory,
ContactCardEmbeddable,
SlowContactCardEmbeddableFactory,
HELLO_WORLD_EMBEDDABLE_TYPE,
HelloWorldEmbeddableFactory,
} from '../test_samples/index';
import { isErrorEmbeddable, EmbeddableOutput, EmbeddableFactory } from '../embeddables';
import {
FilterableEmbeddableInput,
FilterableEmbeddable,
} from '../test_samples/embeddables/filterable_embeddable';
import { Filter, FilterStateStore } from '@kbn/es-query';

jest.mock('ui/new_platform');

const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory());
embeddableFactories.set(
CONTACT_CARD_EMBEDDABLE,
new SlowContactCardEmbeddableFactory({ loadTickCount: 2 })
);
embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory());

test('Explicit embeddable input mapped to undefined will default to inherited', async () => {
const derivedFilter: Filter = {
$state: { store: FilterStateStore.APP_STATE },
meta: { disabled: false, alias: 'name', negate: false },
query: { match: {} },
};
const container = new FilterableContainer(
{ id: 'hello', panels: {}, filters: [derivedFilter] },
embeddableFactories
);
const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});

if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}

embeddable.updateInput({ filters: [] });

expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);

embeddable.updateInput({ filters: undefined });

expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([
derivedFilter,
]);
});

test('Explicit embeddable input mapped to undefined with no inherited value will get passed to embeddable', async done => {
const container = new HelloWorldContainer({ id: 'hello', panels: {} }, embeddableFactories);

const embeddable = await container.addNewEmbeddable<
FilterableEmbeddableInput,
EmbeddableOutput,
FilterableEmbeddable
>(FILTERABLE_EMBEDDABLE, {});

if (isErrorEmbeddable(embeddable)) {
throw new Error('Error adding embeddable');
}

embeddable.updateInput({ filters: [] });

expect(container.getInputForChild<FilterableEmbeddableInput>(embeddable.id).filters).toEqual([]);

const subscription = embeddable
.getInput$()
.pipe(skip(1))
.subscribe(() => {
if (embeddable.getInput().filters === undefined) {
subscription.unsubscribe();
done();
}
});

embeddable.updateInput({ filters: undefined });
});

// The goal is to make sure that if the container input changes after `onPanelAdded` is called
// 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 container = new HelloWorldContainer(
{
id: 'hello',
panels: {
'123': {
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
},
},
lastName: 'bar',
},
embeddableFactories
);

container.updateInput({ lastName: 'lolol' });

const subscription = container.getOutput$().subscribe(() => {
if (container.getOutput().embeddableLoaded['123']) {
expect(container.getInput().panels['123'].explicitInput.lastName).toBeUndefined();
const embeddable = container.getChild<ContactCardEmbeddable>('123');
expect(embeddable).toBeDefined();
expect(embeddable.getInput().lastName).toBe('lolol');
subscription.unsubscribe();
done();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,10 @@ function convertPanelActionToContextMenuItem({
'data-test-subj': `embeddablePanelAction-${action.id}`,
};

if (action.getHref(actionContext) === undefined) {
menuPanelItem.onClick = () => {
action.execute(actionContext);
closeMenu();
};
}
menuPanelItem.onClick = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change was necessary or the link actions that navigated the user to another page would result in the pop up menu still showing.

action.execute(actionContext);
closeMenu();
};

if (action.getHref(actionContext)) {
menuPanelItem.href = action.getHref(actionContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export abstract class Embeddable<

if (parent) {
this.parentSubscription = Rx.merge(parent.getInput$(), parent.getOutput$()).subscribe(() => {
// Make sure this panel hasn't been removed immediately after it was added, but before it finished loading.
if (!parent.getInput().panels[this.id]) return;

const newInput = parent.getInputForChild<TEmbeddableInput>(this.id);
this.onResetInput(newInput);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@

import { Action } from './actions';
import { IEmbeddable } from './embeddables';
import { IContainer } from './containers';
import { Trigger } from './types';

export async function getActionsForTrigger(
actionRegistry: Map<string, Action>,
triggerRegistry: Map<string, Trigger>,
triggerId: string,
context: { embeddable: IEmbeddable; container?: IContainer }
context: { embeddable: IEmbeddable; triggerContext?: { [key: string]: unknown } }
) {
const trigger = triggerRegistry.get(triggerId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory<ContactCardE
modalSession.close();
resolve(undefined);
}}
onCreate={(input: { firstName: string; lastName: string }) => {
onCreate={(input: { firstName: string; lastName?: string }) => {
modalSession.close();
resolve(input);
}}
Expand Down
Loading