mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* created an add to library action that turns 'by value' embeddables into 'by reference' embeddables
This commit is contained in:
parent
eeba9c72dd
commit
a618464de4
7 changed files with 328 additions and 16 deletions
|
@ -26,7 +26,6 @@ import {
|
|||
EmbeddableOutput,
|
||||
SavedObjectEmbeddableInput,
|
||||
ReferenceOrValueEmbeddable,
|
||||
Container,
|
||||
} from '../../../../src/plugins/embeddable/public';
|
||||
import { BookSavedObjectAttributes } from '../../common';
|
||||
import { BookEmbeddableComponent } from './book_component';
|
||||
|
@ -104,16 +103,13 @@ export class BookEmbeddable extends Embeddable<BookEmbeddableInput, BookEmbeddab
|
|||
};
|
||||
|
||||
getInputAsValueType = async (): Promise<BookByValueInput> => {
|
||||
const input =
|
||||
this.getRoot() && (this.getRoot() as Container).getInput().panels[this.id].explicitInput
|
||||
? ((this.getRoot() as Container).getInput().panels[this.id]
|
||||
.explicitInput as BookEmbeddableInput)
|
||||
: this.input;
|
||||
const input = this.attributeService.getExplicitInputFromEmbeddable(this);
|
||||
return this.attributeService.getInputAsValueType(input);
|
||||
};
|
||||
|
||||
getInputAsRefType = async (): Promise<BookByReferenceInput> => {
|
||||
return this.attributeService.getInputAsRefType(this.input, { showSaveModal: true });
|
||||
const input = this.attributeService.getExplicitInputFromEmbeddable(this);
|
||||
return this.attributeService.getInputAsRefType(input, { showSaveModal: true });
|
||||
};
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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, ReferenceOrValueEmbeddable } from '../../embeddable_plugin';
|
||||
import { DashboardContainer } from '../embeddable';
|
||||
import { getSampleDashboardInput } 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 { AddToLibraryAction } from '.';
|
||||
import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks';
|
||||
import { ViewMode } from '../../../../embeddable/public';
|
||||
|
||||
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 & ReferenceOrValueEmbeddable;
|
||||
let coreStart: CoreStart;
|
||||
beforeEach(async () => {
|
||||
coreStart = coreMock.createStart();
|
||||
|
||||
const containerOptions = {
|
||||
ExitFullScreenButton: () => null,
|
||||
SavedObjectFinder: () => null,
|
||||
application: {} as any,
|
||||
embeddable: start,
|
||||
inspector: {} as any,
|
||||
notifications: {} as any,
|
||||
overlays: coreStart.overlays,
|
||||
savedObjectMetaData: {} as any,
|
||||
uiActions: {} as any,
|
||||
};
|
||||
|
||||
container = new DashboardContainer(getSampleDashboardInput(), containerOptions);
|
||||
|
||||
const contactCardEmbeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
ContactCardEmbeddable
|
||||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'Kibanana',
|
||||
});
|
||||
|
||||
if (isErrorEmbeddable(contactCardEmbeddable)) {
|
||||
throw new Error('Failed to create embeddable');
|
||||
} else {
|
||||
embeddable = embeddablePluginMock.mockRefOrValEmbeddable<
|
||||
ContactCardEmbeddable,
|
||||
ContactCardEmbeddableInput
|
||||
>(contactCardEmbeddable, {
|
||||
mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: contactCardEmbeddable.id },
|
||||
mockedByValueInput: { firstName: 'Kibanana', id: contactCardEmbeddable.id },
|
||||
});
|
||||
embeddable.updateInput({ viewMode: ViewMode.EDIT });
|
||||
}
|
||||
});
|
||||
|
||||
test('Add to library is compatible when embeddable on dashboard has value type input', async () => {
|
||||
const action = new AddToLibraryAction();
|
||||
embeddable.updateInput(await embeddable.getInputAsValueType());
|
||||
expect(await action.isCompatible({ embeddable })).toBe(true);
|
||||
});
|
||||
|
||||
test('Add to library is not compatible when embeddable input is by reference', async () => {
|
||||
const action = new AddToLibraryAction();
|
||||
embeddable.updateInput(await embeddable.getInputAsRefType());
|
||||
expect(await action.isCompatible({ embeddable })).toBe(false);
|
||||
});
|
||||
|
||||
test('Add to library is not compatible when view mode is set to view', async () => {
|
||||
const action = new AddToLibraryAction();
|
||||
embeddable.updateInput(await embeddable.getInputAsRefType());
|
||||
embeddable.updateInput({ viewMode: ViewMode.VIEW });
|
||||
expect(await action.isCompatible({ embeddable })).toBe(false);
|
||||
});
|
||||
|
||||
test('Add to library is not compatible when embeddable is not in a dashboard container', async () => {
|
||||
let orphanContactCard = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
ContactCardEmbeddable
|
||||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'Orphan',
|
||||
});
|
||||
orphanContactCard = embeddablePluginMock.mockRefOrValEmbeddable<
|
||||
ContactCardEmbeddable,
|
||||
ContactCardEmbeddableInput
|
||||
>(orphanContactCard, {
|
||||
mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id },
|
||||
mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id },
|
||||
});
|
||||
const action = new AddToLibraryAction();
|
||||
expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false);
|
||||
});
|
||||
|
||||
test('Add to library 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 AddToLibraryAction();
|
||||
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('Add to library returns reference type input', async () => {
|
||||
const complicatedAttributes = {
|
||||
attribute1: 'The best attribute',
|
||||
attribute2: 22,
|
||||
attribute3: ['array', 'of', 'strings'],
|
||||
attribute4: { nestedattribute: 'hello from the nest' },
|
||||
};
|
||||
|
||||
embeddable = embeddablePluginMock.mockRefOrValEmbeddable<ContactCardEmbeddable>(embeddable, {
|
||||
mockedByReferenceInput: { savedObjectId: 'testSavedObjectId', id: embeddable.id },
|
||||
mockedByValueInput: { attributes: complicatedAttributes, id: embeddable.id },
|
||||
});
|
||||
const dashboard = embeddable.getRoot() as IContainer;
|
||||
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
|
||||
const action = new AddToLibraryAction();
|
||||
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).toBeUndefined();
|
||||
expect(newPanel.explicitInput.savedObjectId).toBe('testSavedObjectId');
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin';
|
||||
import { ViewMode, PanelState, IEmbeddable } from '../../embeddable_plugin';
|
||||
import {
|
||||
PanelNotFoundError,
|
||||
EmbeddableInput,
|
||||
isReferenceOrValueEmbeddable,
|
||||
} from '../../../../embeddable/public';
|
||||
import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..';
|
||||
|
||||
export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary';
|
||||
|
||||
export interface AddToLibraryActionContext {
|
||||
embeddable: IEmbeddable;
|
||||
}
|
||||
|
||||
export class AddToLibraryAction implements ActionByType<typeof ACTION_ADD_TO_LIBRARY> {
|
||||
public readonly type = ACTION_ADD_TO_LIBRARY;
|
||||
public readonly id = ACTION_ADD_TO_LIBRARY;
|
||||
public order = 15;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public getDisplayName({ embeddable }: AddToLibraryActionContext) {
|
||||
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
return i18n.translate('dashboard.panel.AddToLibrary', {
|
||||
defaultMessage: 'Add to library',
|
||||
});
|
||||
}
|
||||
|
||||
public getIconType({ embeddable }: AddToLibraryActionContext) {
|
||||
if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
return 'folderCheck';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: AddToLibraryActionContext) {
|
||||
return Boolean(
|
||||
embeddable.getInput()?.viewMode !== ViewMode.VIEW &&
|
||||
embeddable.getRoot() &&
|
||||
embeddable.getRoot().isContainer &&
|
||||
embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE &&
|
||||
isReferenceOrValueEmbeddable(embeddable) &&
|
||||
!embeddable.inputIsRefType(embeddable.getInput())
|
||||
);
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: AddToLibraryActionContext) {
|
||||
if (!isReferenceOrValueEmbeddable(embeddable)) {
|
||||
throw new IncompatibleActionError();
|
||||
}
|
||||
|
||||
const newInput = await embeddable.getInputAsRefType();
|
||||
|
||||
embeddable.updateInput(newInput);
|
||||
|
||||
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: { ...newInput, id: uuid.v4() },
|
||||
};
|
||||
dashboard.replacePanel(panelToReplace, newPanel);
|
||||
}
|
||||
}
|
|
@ -33,7 +33,12 @@ export {
|
|||
ACTION_CLONE_PANEL,
|
||||
} from './clone_panel_action';
|
||||
export {
|
||||
AddToLibraryAction,
|
||||
AddToLibraryActionContext,
|
||||
ACTION_ADD_TO_LIBRARY,
|
||||
} from './add_to_library_action';
|
||||
export {
|
||||
UnlinkFromLibraryAction,
|
||||
UnlinkFromLibraryActionContext,
|
||||
ACTION_UNLINK_FROM_LIBRARY,
|
||||
UnlinkFromLibraryAction,
|
||||
} from './unlink_from_library_action';
|
||||
|
|
|
@ -30,13 +30,21 @@ import {
|
|||
SimpleSavedObject,
|
||||
I18nStart,
|
||||
NotificationsStart,
|
||||
OverlayStart,
|
||||
} from '../../../../core/public';
|
||||
import {
|
||||
SavedObjectSaveModal,
|
||||
showSaveModal,
|
||||
OnSaveProps,
|
||||
SaveResult,
|
||||
checkForDuplicateTitle,
|
||||
} from '../../../saved_objects/public';
|
||||
import {
|
||||
EmbeddableStart,
|
||||
EmbeddableFactory,
|
||||
EmbeddableFactoryNotFoundError,
|
||||
Container,
|
||||
} from '../../../embeddable/public';
|
||||
|
||||
/**
|
||||
* The attribute service is a shared, generic service that embeddables can use to provide the functionality
|
||||
|
@ -49,12 +57,22 @@ export class AttributeService<
|
|||
ValType extends EmbeddableInput & { attributes: SavedObjectAttributes },
|
||||
RefType extends SavedObjectEmbeddableInput
|
||||
> {
|
||||
private embeddableFactory: EmbeddableFactory;
|
||||
|
||||
constructor(
|
||||
private type: string,
|
||||
private savedObjectsClient: SavedObjectsClientContract,
|
||||
private overlays: OverlayStart,
|
||||
private i18nContext: I18nStart['Context'],
|
||||
private toasts: NotificationsStart['toasts']
|
||||
) {}
|
||||
private toasts: NotificationsStart['toasts'],
|
||||
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']
|
||||
) {
|
||||
const factory = getEmbeddableFactory(this.type);
|
||||
if (!factory) {
|
||||
throw new EmbeddableFactoryNotFoundError(this.type);
|
||||
}
|
||||
this.embeddableFactory = factory;
|
||||
}
|
||||
|
||||
public async unwrapAttributes(input: RefType | ValType): Promise<SavedObjectAttributes> {
|
||||
if (this.inputIsRefType(input)) {
|
||||
|
@ -105,6 +123,15 @@ export class AttributeService<
|
|||
return isSavedObjectEmbeddableInput(input);
|
||||
};
|
||||
|
||||
public getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType {
|
||||
return embeddable.getRoot() &&
|
||||
(embeddable.getRoot() as Container).getInput().panels[embeddable.id].explicitInput
|
||||
? ((embeddable.getRoot() as Container).getInput().panels[embeddable.id].explicitInput as
|
||||
| ValType
|
||||
| RefType)
|
||||
: (embeddable.getInput() as ValType | RefType);
|
||||
}
|
||||
|
||||
getInputAsValueType = async (input: ValType | RefType): Promise<ValType> => {
|
||||
if (!this.inputIsRefType(input)) {
|
||||
return input;
|
||||
|
@ -124,16 +151,31 @@ export class AttributeService<
|
|||
if (this.inputIsRefType(input)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return new Promise<RefType>((resolve, reject) => {
|
||||
const onSave = async (props: OnSaveProps): Promise<SaveResult> => {
|
||||
await checkForDuplicateTitle(
|
||||
{
|
||||
title: props.newTitle,
|
||||
copyOnSave: false,
|
||||
lastSavedTitle: '',
|
||||
getEsType: () => this.type,
|
||||
getDisplayName: this.embeddableFactory.getDisplayName,
|
||||
},
|
||||
props.isTitleDuplicateConfirmed,
|
||||
props.onTitleDuplicate,
|
||||
{
|
||||
savedObjectsClient: this.savedObjectsClient,
|
||||
overlays: this.overlays,
|
||||
}
|
||||
);
|
||||
try {
|
||||
input.attributes.title = props.newTitle;
|
||||
const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType;
|
||||
const newAttributes = { ...input.attributes };
|
||||
newAttributes.title = props.newTitle;
|
||||
const wrappedInput = (await this.wrapAttributes(newAttributes, true)) as RefType;
|
||||
resolve(wrappedInput);
|
||||
return { id: wrappedInput.savedObjectId };
|
||||
} catch (error) {
|
||||
reject();
|
||||
reject(error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -95,6 +95,11 @@ import { addEmbeddableToDashboardUrl } from './url_utils/url_helper';
|
|||
import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder';
|
||||
import { UrlGeneratorState } from '../../share/public';
|
||||
import { AttributeService } from '.';
|
||||
import {
|
||||
AddToLibraryAction,
|
||||
ACTION_ADD_TO_LIBRARY,
|
||||
AddToLibraryActionContext,
|
||||
} from './application/actions/add_to_library_action';
|
||||
|
||||
declare module '../../share/public' {
|
||||
export interface UrlGeneratorStateMapping {
|
||||
|
@ -155,6 +160,7 @@ declare module '../../../plugins/ui_actions/public' {
|
|||
[ACTION_EXPAND_PANEL]: ExpandPanelActionContext;
|
||||
[ACTION_REPLACE_PANEL]: ReplacePanelActionContext;
|
||||
[ACTION_CLONE_PANEL]: ClonePanelActionContext;
|
||||
[ACTION_ADD_TO_LIBRARY]: AddToLibraryActionContext;
|
||||
[ACTION_UNLINK_FROM_LIBRARY]: UnlinkFromLibraryActionContext;
|
||||
}
|
||||
}
|
||||
|
@ -406,6 +412,7 @@ export class DashboardPlugin
|
|||
const {
|
||||
uiActions,
|
||||
data: { indexPatterns, search },
|
||||
embeddable,
|
||||
} = plugins;
|
||||
|
||||
const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings);
|
||||
|
@ -424,6 +431,9 @@ export class DashboardPlugin
|
|||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id);
|
||||
|
||||
if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) {
|
||||
const addToLibraryAction = new AddToLibraryAction();
|
||||
uiActions.registerAction(addToLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id);
|
||||
const unlinkFromLibraryAction = new UnlinkFromLibraryAction();
|
||||
uiActions.registerAction(unlinkFromLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id);
|
||||
|
@ -452,8 +462,10 @@ export class DashboardPlugin
|
|||
new AttributeService(
|
||||
type,
|
||||
core.savedObjects.client,
|
||||
core.overlays,
|
||||
core.i18n.Context,
|
||||
core.notifications.toasts
|
||||
core.notifications.toasts,
|
||||
embeddable.getEmbeddableFactory
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@ import {
|
|||
EmbeddableStateTransfer,
|
||||
IEmbeddable,
|
||||
EmbeddablePanel,
|
||||
EmbeddableInput,
|
||||
SavedObjectEmbeddableInput,
|
||||
ReferenceOrValueEmbeddable,
|
||||
} from '.';
|
||||
import { EmbeddablePublicPlugin } from './plugin';
|
||||
import { coreMock } from '../../../core/public/mocks';
|
||||
|
@ -35,7 +38,6 @@ import { dataPluginMock } from '../../data/public/mocks';
|
|||
|
||||
import { inspectorPluginMock } from '../../inspector/public/mocks';
|
||||
import { uiActionsPluginMock } from '../../ui_actions/public/mocks';
|
||||
import { SavedObjectEmbeddableInput, ReferenceOrValueEmbeddable, EmbeddableInput } from './lib';
|
||||
|
||||
export type Setup = jest.Mocked<EmbeddableSetup>;
|
||||
export type Start = jest.Mocked<EmbeddableStart>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue