Skip to content

Commit

Permalink
Got unlink action working properly. TODO: Use config option from #73870
Browse files Browse the repository at this point in the history
… to show the option
  • Loading branch information
ThomThomson committed Jul 30, 2020
1 parent a6888e3 commit b3c450d
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/plugins/dashboard/public/application/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ export {
ClonePanelActionContext,
ACTION_CLONE_PANEL,
} from './clone_panel_action';
export {
UnlinkFromLibraryActionContext,
ACTION_UNLINK_FROM_LIBRARY,
UnlinkFromLibraryAction,
} from './unlink_from_library_action';
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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, IContainer } from '../../embeddable_plugin';
import { DashboardContainer } from '../embeddable';
import { getSampleDashboardInput, getSampleDashboardPanel } from '../test_helpers';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../../embeddable_plugin_test_samples';
import { coreMock } from '../../../../../core/public/mocks';
import { CoreStart } from 'kibana/public';
import { UnlinkFromLibraryAction } from '.';

// eslint-disable-next-line
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';

const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory((() => null) as any, {} as any)
);
const start = doStart();

let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;
let coreStart: CoreStart;
beforeEach(async () => {
coreStart = coreMock.createStart();
coreStart.savedObjects.client = {
...coreStart.savedObjects.client,
get: jest.fn().mockImplementation(() => ({ attributes: { title: 'Holy moly' } })),
find: jest.fn().mockImplementation(() => ({ total: 15 })),
create: jest.fn().mockImplementation(() => ({ id: 'brandNewSavedObject' })),
};

const options = {
ExitFullScreenButton: () => null,
SavedObjectFinder: () => null,
application: {} as any,
embeddable: start,
inspector: {} as any,
notifications: {} as any,
overlays: coreStart.overlays,
savedObjectMetaData: {} as any,
uiActions: {} as any,
};
const input = getSampleDashboardInput({
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Kibanana', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
},
});
container = new DashboardContainer(input, options);

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

contactCardEmbeddable.updateInput({ savedObjectId: 'coolestSavedObjectId' });

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

test('Unlink replaces embeddableId but retains panel count', async () => {
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
const action = new UnlinkFromLibraryAction(coreStart);
await action.execute({ embeddable });
expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount);

const newPanelId = Object.keys(container.getInput().panels).find(
key => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
const newPanel = container.getInput().panels[newPanelId!];
expect(newPanel.type).toEqual(embeddable.type);
});

test('Unlink unwraps all attributes from savedObject', async () => {
const complicatedAttributes = {
attribute1: 'The best attribute',
attribute2: 22,
attribute3: ['array', 'of', 'strings'],
attribute4: { nestedattribute: 'hello from the nest' },
};

coreStart.savedObjects.client.get = jest.fn().mockImplementation(() => ({
attributes: complicatedAttributes,
}));
const dashboard = embeddable.getRoot() as IContainer;
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
const action = new UnlinkFromLibraryAction(coreStart);
await action.execute({ embeddable });
const newPanelId = Object.keys(container.getInput().panels).find(
key => !originalPanelKeySet.has(key)
);
expect(newPanelId).toBeDefined();
const newPanel = container.getInput().panels[newPanelId!];
expect(newPanel.type).toEqual(embeddable.type);
expect(newPanel.explicitInput.attributes).toEqual(complicatedAttributes);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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, SimpleSavedObject } from 'src/core/public';
import _ from 'lodash';
import uuid from 'uuid';
import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin';
import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin';
import {
PanelNotFoundError,
EmbeddableInput,
SavedObjectEmbeddableInput,
} from '../../../../embeddable/public';
import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..';

export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';

export interface UnlinkFromLibraryActionContext {
embeddable: IEmbeddable;
}

export class UnlinkFromLibraryAction implements ActionByType<typeof ACTION_UNLINK_FROM_LIBRARY> {
public readonly type = ACTION_UNLINK_FROM_LIBRARY;
public readonly id = ACTION_UNLINK_FROM_LIBRARY;
public order = 15;

constructor(private core: CoreStart) {}

public getDisplayName({ embeddable }: UnlinkFromLibraryActionContext) {
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
throw new IncompatibleActionError();
}
return i18n.translate('dashboard.panel.unlinkFromLibrary', {
defaultMessage: 'Unlink from visualize library',
});
}

public getIconType({ embeddable }: UnlinkFromLibraryActionContext) {
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
throw new IncompatibleActionError();
}
return 'folderExclamation';
}

public async isCompatible({ embeddable }: UnlinkFromLibraryActionContext) {
return Boolean(
embeddable.getInput()?.viewMode !== ViewMode.VIEW &&
embeddable.getRoot() &&
embeddable.getRoot().isContainer &&
embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE &&
(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId &&
embeddable.type === 'lens'
);
}

public async execute({ embeddable }: UnlinkFromLibraryActionContext) {
if (
!embeddable.getRoot() ||
!embeddable.getRoot().isContainer ||
!(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId
) {
throw new IncompatibleActionError();
}

const currentInput = embeddable.getInput() as SavedObjectEmbeddableInput;
const savedObject: SimpleSavedObject = await this.core.savedObjects.client.get(
embeddable.type,
currentInput.savedObjectId
);

const dashboard = embeddable.getRoot() as DashboardContainer;
const panelToReplace = dashboard.getInput().panels[embeddable.id] as DashboardPanelState;
if (!panelToReplace) {
throw new PanelNotFoundError();
}

const newPanel: PanelState<EmbeddableInput> = {
type: embeddable.type,
explicitInput: {
...panelToReplace.explicitInput,
savedObjectId: undefined,
id: uuid.v4(),
attributes: savedObject.attributes,
},
};
dashboard.replacePanel(panelToReplace, newPanel);
}
}
9 changes: 9 additions & 0 deletions src/plugins/dashboard/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ import {
RenderDeps,
ReplacePanelAction,
ReplacePanelActionContext,
ACTION_UNLINK_FROM_LIBRARY,
UnlinkFromLibraryActionContext,
UnlinkFromLibraryAction,
} from './application';
import {
createDashboardUrlGenerator,
Expand Down Expand Up @@ -133,6 +136,7 @@ declare module '../../../plugins/ui_actions/public' {
[ACTION_EXPAND_PANEL]: ExpandPanelActionContext;
[ACTION_REPLACE_PANEL]: ReplacePanelActionContext;
[ACTION_CLONE_PANEL]: ClonePanelActionContext;
[ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext;
}
}

Expand Down Expand Up @@ -396,6 +400,11 @@ export class DashboardPlugin
uiActions.registerAction(clonePanelAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id);

// TODO: once https://github.com/elastic/kibana/pull/73870 is merges, make this unlink from library action dependent on that config value.
const unlinkFromLibraryAction = new UnlinkFromLibraryAction(core);
uiActions.registerAction(unlinkFromLibraryAction);
uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id);

const savedDashboardLoader = createSavedDashboardLoader({
savedObjectsClient: core.savedObjects.client,
indexPatterns,
Expand Down

0 comments on commit b3c450d

Please sign in to comment.