Skip to content

Commit

Permalink
Add ability to replace any panel in edit mode on the fly (#45095)
Browse files Browse the repository at this point in the history
* First version of change view functionality

* Adds the 'replace view' functionality to dashboard edit mode

* Fixed type_check errors

* Make action part of dashboard_embeddable_container

* Fixed import paths for type check errors

* Fixed i18n errors

* Renamed action to 'Replace panel' and adjusted jest tests to pass type check

* test: add functional tests

Closes #43900
  • Loading branch information
friol authored and nickofthyme committed Oct 23, 2019
1 parent b23cfbd commit c718972
Show file tree
Hide file tree
Showing 13 changed files with 636 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { PluginInitializerContext } from 'kibana/public';
import { PluginInitializerContext } from '../../../../../../core/public';
import { DashboardEmbeddableContainerPublicPlugin } from './plugin';

export * from './lib';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
*/

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

import {
IEmbeddable,
EmbeddableInput,
EmbeddableOutput,
} from '../../../../../../embeddable_api/public/np_ready/public';

import { IContainer } from '../../../../../../embeddable_api/public/np_ready/public';
import { NotificationsStart } from '../../../../../../../../core/public';

export async function openReplacePanelFlyout(options: {
embeddable: IContainer;
core: CoreStart;
savedObjectFinder: React.ComponentType<any>;
notifications: NotificationsStart;
panelToRemove: IEmbeddable<EmbeddableInput, EmbeddableOutput>;
}) {
const { embeddable, core, panelToRemove, savedObjectFinder, notifications } = options;
const flyoutSession = core.overlays.openFlyout(
<ReplacePanelFlyout
container={embeddable}
onClose={() => {
if (flyoutSession) {
flyoutSession.close();
}
}}
panelToRemove={panelToRemove}
savedObjectsFinder={savedObjectFinder}
notifications={notifications}
/>,
{
'data-test-subj': 'replacePanelFlyout',
}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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 { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_api';
import { ReplacePanelAction } from './replace_panel_action';
import { DashboardContainer } from '../embeddable';
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../../../../../../embeddable_api/public/np_ready/public/lib/test_samples';
import { DashboardOptions } from '../embeddable/dashboard_container_factory';

const embeddableFactories = new Map<string, EmbeddableFactory>();
embeddableFactories.set(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory({} as any, (() => null) as any, {} as any)
);

let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;

beforeEach(async () => {
const options: DashboardOptions = {
ExitFullScreenButton: () => null,
SavedObjectFinder: () => null,
application: {} as any,
embeddable: {
getEmbeddableFactory: (id: string) => embeddableFactories.get(id)!,
} as any,
inspector: {} as any,
notifications: {} as any,
overlays: {} as any,
savedObjectMetaData: {} as any,
uiActions: {} as any,
};
const input = getSampleDashboardInput({
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
},
});
container = new DashboardContainer(input, options);

const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'Kibana',
});

if (isErrorEmbeddable(contactCardEmbeddable)) {
throw new Error('Failed to create embeddable');
} else {
embeddable = contactCardEmbeddable;
}
});

test('Executes the replace panel action', async () => {
let core: any;
let SavedObjectFinder: any;
let notifications: any;
const action = new ReplacePanelAction(core, SavedObjectFinder, notifications);
action.execute({ embeddable });
});

test('Is not compatible when embeddable is not in a dashboard container', async () => {
let core: any;
let SavedObjectFinder: any;
let notifications: any;
const action = new ReplacePanelAction(core, SavedObjectFinder, notifications);
expect(
await action.isCompatible({
embeddable: new ContactCardEmbeddable(
{ firstName: 'sue', id: '123' },
{ execAction: (() => null) as any }
),
})
).toBe(false);
});

test('Execute throws an error when called with an embeddable not in a parent', async () => {
let core: any;
let SavedObjectFinder: any;
let notifications: any;
const action = new ReplacePanelAction(core, SavedObjectFinder, notifications);
async function check() {
await action.execute({ embeddable: container });
}
await expect(check()).rejects.toThrow(Error);
});

test('Returns title', async () => {
let core: any;
let SavedObjectFinder: any;
let notifications: any;
const action = new ReplacePanelAction(core, SavedObjectFinder, notifications);
expect(action.getDisplayName({ embeddable })).toBeDefined();
});

test('Returns an icon', async () => {
let core: any;
let SavedObjectFinder: any;
let notifications: any;
const action = new ReplacePanelAction(core, SavedObjectFinder, notifications);
expect(action.getIconType({ embeddable })).toBeDefined();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { CoreStart } from 'src/core/public';

import { IEmbeddable, ViewMode } from '../../../../../../embeddable_api/public/np_ready/public';
import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable';
import {
IAction,
IncompatibleActionError,
} from '../../../../../../../../plugins/ui_actions/public';
import { NotificationsStart } from '../../../../../../../../core/public';
import { openReplacePanelFlyout } from './open_replace_panel_flyout';

export const REPLACE_PANEL_ACTION = 'replacePanel';

function isDashboard(embeddable: IEmbeddable): embeddable is DashboardContainer {
return embeddable.type === DASHBOARD_CONTAINER_TYPE;
}

interface ActionContext {
embeddable: IEmbeddable;
}

export class ReplacePanelAction implements IAction<ActionContext> {
public readonly type = REPLACE_PANEL_ACTION;
public readonly id = REPLACE_PANEL_ACTION;
public order = 11;

constructor(
private core: CoreStart,
private savedobjectfinder: React.ComponentType<any>,
private notifications: NotificationsStart
) {}

public getDisplayName({ embeddable }: ActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
return i18n.translate('dashboardEmbeddableContainer.panel.removePanel.replacePanel', {
defaultMessage: 'Replace panel',
});
}

public getIconType({ embeddable }: ActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}
return 'kqlOperand';
}

public async isCompatible({ embeddable }: ActionContext) {
if (embeddable.getInput().viewMode) {
if (embeddable.getInput().viewMode === ViewMode.VIEW) {
return false;
}
}

return Boolean(embeddable.parent && isDashboard(embeddable.parent));
}

public async execute({ embeddable }: ActionContext) {
if (!embeddable.parent || !isDashboard(embeddable.parent)) {
throw new IncompatibleActionError();
}

const view = embeddable;
const dash = embeddable.parent;
openReplacePanelFlyout({
embeddable: dash,
core: this.core,
savedObjectFinder: this.savedobjectfinder,
notifications: this.notifications,
panelToRemove: view,
});
}
}
Loading

0 comments on commit c718972

Please sign in to comment.