mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Dashboard] Remove clone by reference (#164108)
Removes all clone by reference functionality on the Dashboard. This means that we no longer have any `savedObjectsClient` usage in the browser side.
This commit is contained in:
parent
90b6e4dbe0
commit
011ae97061
14 changed files with 105 additions and 408 deletions
|
@ -16,17 +16,21 @@ import {
|
|||
import { CoreStart } from '@kbn/core/public';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks';
|
||||
import { ErrorEmbeddable, IContainer, isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
ErrorEmbeddable,
|
||||
IContainer,
|
||||
isErrorEmbeddable,
|
||||
ReferenceOrValueEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { DashboardPanelState } from '../../common';
|
||||
import { ClonePanelAction } from './clone_panel_action';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { buildMockDashboard, getSampleDashboardPanel } from '../mocks';
|
||||
import { DashboardContainer } from '../dashboard_container/embeddable/dashboard_container';
|
||||
|
||||
let container: DashboardContainer;
|
||||
let byRefOrValEmbeddable: ContactCardEmbeddable;
|
||||
let genericEmbeddable: ContactCardEmbeddable;
|
||||
let byRefOrValEmbeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable;
|
||||
let coreStart: CoreStart;
|
||||
beforeEach(async () => {
|
||||
coreStart = coreMock.createStart();
|
||||
|
@ -58,20 +62,22 @@ beforeEach(async () => {
|
|||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'RefOrValEmbeddable',
|
||||
});
|
||||
const genericContactCardEmbeddable = await container.addNewEmbeddable<
|
||||
|
||||
const nonRefOrValueContactCard = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
ContactCardEmbeddable
|
||||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'NotRefOrValEmbeddable',
|
||||
firstName: 'Not a refOrValEmbeddable',
|
||||
});
|
||||
|
||||
if (
|
||||
isErrorEmbeddable(refOrValContactCardEmbeddable) ||
|
||||
isErrorEmbeddable(genericContactCardEmbeddable)
|
||||
isErrorEmbeddable(nonRefOrValueContactCard)
|
||||
) {
|
||||
throw new Error('Failed to create embeddables');
|
||||
} else {
|
||||
genericEmbeddable = nonRefOrValueContactCard;
|
||||
byRefOrValEmbeddable = embeddablePluginMock.mockRefOrValEmbeddable<
|
||||
ContactCardEmbeddable,
|
||||
ContactCardEmbeddableInput
|
||||
|
@ -80,14 +86,14 @@ beforeEach(async () => {
|
|||
savedObjectId: 'testSavedObjectId',
|
||||
id: refOrValContactCardEmbeddable.id,
|
||||
},
|
||||
mockedByValueInput: { firstName: 'Kibanana', id: refOrValContactCardEmbeddable.id },
|
||||
mockedByValueInput: { firstName: 'RefOrValEmbeddable', id: refOrValContactCardEmbeddable.id },
|
||||
});
|
||||
genericEmbeddable = genericContactCardEmbeddable;
|
||||
jest.spyOn(byRefOrValEmbeddable, 'getInputAsValueType');
|
||||
}
|
||||
});
|
||||
|
||||
test('Clone is incompatible with Error Embeddables', async () => {
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
const action = new ClonePanelAction();
|
||||
const errorEmbeddable = new ErrorEmbeddable('Wow what an awful error', { id: ' 404' }, container);
|
||||
expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false);
|
||||
});
|
||||
|
@ -96,134 +102,65 @@ test('Clone adds a new embeddable', async () => {
|
|||
const dashboard = byRefOrValEmbeddable.getRoot() as IContainer;
|
||||
const originalPanelCount = Object.keys(dashboard.getInput().panels).length;
|
||||
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
const action = new ClonePanelAction();
|
||||
await action.execute({ embeddable: byRefOrValEmbeddable });
|
||||
|
||||
expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount + 1);
|
||||
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('placeholder');
|
||||
// let the placeholder load
|
||||
await dashboard.untilEmbeddableLoaded(newPanelId!);
|
||||
await new Promise((r) => process.nextTick(r)); // Allow the current loop of the event loop to run to completion
|
||||
// now wait for the full embeddable to replace it
|
||||
const loadedPanel = await dashboard.untilEmbeddableLoaded(newPanelId!);
|
||||
expect(loadedPanel.type).toEqual(byRefOrValEmbeddable.type);
|
||||
expect(newPanel.type).toEqual(byRefOrValEmbeddable.type);
|
||||
});
|
||||
|
||||
test('Clones a RefOrVal embeddable by value', async () => {
|
||||
const dashboard = byRefOrValEmbeddable.getRoot() as IContainer;
|
||||
const panel = dashboard.getInput().panels[byRefOrValEmbeddable.id] as DashboardPanelState;
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
// @ts-ignore
|
||||
const newPanel = await action.cloneEmbeddable(panel, byRefOrValEmbeddable);
|
||||
expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(0);
|
||||
expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(0);
|
||||
expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(0);
|
||||
expect(newPanel.type).toEqual(byRefOrValEmbeddable.type);
|
||||
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
|
||||
const action = new ClonePanelAction();
|
||||
await action.execute({ embeddable: byRefOrValEmbeddable });
|
||||
const newPanelId = Object.keys(container.getInput().panels).find(
|
||||
(key) => !originalPanelKeySet.has(key)
|
||||
);
|
||||
|
||||
const originalFirstName = (
|
||||
container.getInput().panels[byRefOrValEmbeddable.id].explicitInput as ContactCardEmbeddableInput
|
||||
).firstName;
|
||||
|
||||
const newFirstName = (
|
||||
container.getInput().panels[newPanelId!].explicitInput as ContactCardEmbeddableInput
|
||||
).firstName;
|
||||
|
||||
expect(byRefOrValEmbeddable.getInputAsValueType).toHaveBeenCalled();
|
||||
|
||||
expect(originalFirstName).toEqual(newFirstName);
|
||||
expect(container.getInput().panels[newPanelId!].type).toEqual(byRefOrValEmbeddable.type);
|
||||
});
|
||||
|
||||
test('Clones a non-RefOrVal embeddable by value if the panel does not have a savedObjectId', async () => {
|
||||
test('Clones a non RefOrVal embeddable by value', async () => {
|
||||
const dashboard = genericEmbeddable.getRoot() as IContainer;
|
||||
const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState;
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
// @ts-ignore
|
||||
const newPanelWithoutId = await action.cloneEmbeddable(panel, genericEmbeddable);
|
||||
expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(0);
|
||||
expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(0);
|
||||
expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(0);
|
||||
expect(newPanelWithoutId.type).toEqual(genericEmbeddable.type);
|
||||
});
|
||||
const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels));
|
||||
const action = new ClonePanelAction();
|
||||
await action.execute({ embeddable: genericEmbeddable });
|
||||
const newPanelId = Object.keys(container.getInput().panels).find(
|
||||
(key) => !originalPanelKeySet.has(key)
|
||||
);
|
||||
|
||||
test('Clones a non-RefOrVal embeddable by reference if the panel has a savedObjectId', async () => {
|
||||
const dashboard = genericEmbeddable.getRoot() as IContainer;
|
||||
const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState;
|
||||
panel.explicitInput.savedObjectId = 'holySavedObjectBatman';
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
// @ts-ignore
|
||||
const newPanel = await action.cloneEmbeddable(panel, genericEmbeddable);
|
||||
expect(coreStart.savedObjects.client.get).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.savedObjects.client.find).toHaveBeenCalledTimes(1);
|
||||
expect(coreStart.savedObjects.client.create).toHaveBeenCalledTimes(1);
|
||||
expect(newPanel.type).toEqual(genericEmbeddable.type);
|
||||
});
|
||||
const originalFirstName = (
|
||||
container.getInput().panels[genericEmbeddable.id].explicitInput as ContactCardEmbeddableInput
|
||||
).firstName;
|
||||
|
||||
test('Gets a unique title from the saved objects library', async () => {
|
||||
const dashboard = genericEmbeddable.getRoot() as IContainer;
|
||||
const panel = dashboard.getInput().panels[genericEmbeddable.id] as DashboardPanelState;
|
||||
panel.explicitInput.savedObjectId = 'holySavedObjectBatman';
|
||||
coreStart.savedObjects.client.find = jest.fn().mockImplementation(({ search }) => {
|
||||
if (search === '"testFirstClone"') {
|
||||
return {
|
||||
savedObjects: [
|
||||
{
|
||||
attributes: { title: 'testFirstClone' },
|
||||
get: jest.fn().mockReturnValue('testFirstClone'),
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
};
|
||||
} else if (search === '"testBeforePageLimit"') {
|
||||
return {
|
||||
savedObjects: [
|
||||
{
|
||||
attributes: { title: 'testBeforePageLimit (copy 9)' },
|
||||
get: jest.fn().mockReturnValue('testBeforePageLimit (copy 9)'),
|
||||
},
|
||||
],
|
||||
total: 10,
|
||||
};
|
||||
} else if (search === '"testMaxLogic"') {
|
||||
return {
|
||||
savedObjects: [
|
||||
{
|
||||
attributes: { title: 'testMaxLogic (copy 10000)' },
|
||||
get: jest.fn().mockReturnValue('testMaxLogic (copy 10000)'),
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
};
|
||||
} else if (search === '"testAfterPageLimit"') {
|
||||
return { total: 11 };
|
||||
}
|
||||
});
|
||||
const newFirstName = (
|
||||
container.getInput().panels[newPanelId!].explicitInput as ContactCardEmbeddableInput
|
||||
).firstName;
|
||||
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testFirstClone')).toEqual(
|
||||
'testFirstClone (copy)'
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testBeforePageLimit')).toEqual(
|
||||
'testBeforePageLimit (copy 10)'
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testBeforePageLimit (copy 9)')).toEqual(
|
||||
'testBeforePageLimit (copy 10)'
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testMaxLogic')).toEqual(
|
||||
'testMaxLogic (copy 10001)'
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit')).toEqual(
|
||||
'testAfterPageLimit (copy 11)'
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit (copy 10)')).toEqual(
|
||||
'testAfterPageLimit (copy 11)'
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(genericEmbeddable, 'testAfterPageLimit (copy 10000)')).toEqual(
|
||||
'testAfterPageLimit (copy 11)'
|
||||
);
|
||||
expect(originalFirstName).toEqual(newFirstName);
|
||||
expect(container.getInput().panels[newPanelId!].type).toEqual(genericEmbeddable.type);
|
||||
});
|
||||
|
||||
test('Gets a unique title from the dashboard', async () => {
|
||||
const dashboard = genericEmbeddable.getRoot() as DashboardContainer;
|
||||
const action = new ClonePanelAction(coreStart.savedObjects);
|
||||
const dashboard = byRefOrValEmbeddable.getRoot() as DashboardContainer;
|
||||
const action = new ClonePanelAction();
|
||||
|
||||
// @ts-ignore
|
||||
expect(await action.getCloneTitle(byRefOrValEmbeddable, '')).toEqual('');
|
||||
|
|
|
@ -6,11 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// TODO Remove this usage of the SavedObjectsStart contract.
|
||||
import { SavedObjectsStart } from '@kbn/core/public';
|
||||
import { filter, map, max } from 'lodash';
|
||||
|
||||
import {
|
||||
ViewMode,
|
||||
|
@ -18,21 +15,17 @@ import {
|
|||
IEmbeddable,
|
||||
PanelNotFoundError,
|
||||
EmbeddableInput,
|
||||
SavedObjectEmbeddableInput,
|
||||
isErrorEmbeddable,
|
||||
isReferenceOrValueEmbeddable,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import type { SavedObject } from '@kbn/saved-objects-plugin/public';
|
||||
|
||||
import {
|
||||
placePanelBeside,
|
||||
IPanelPlacementBesideArgs,
|
||||
} from '../dashboard_container/component/panel/dashboard_panel_placement';
|
||||
import { type DashboardPanelState } from '../../common';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { createPanelState } from '../dashboard_container/component/panel';
|
||||
import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings';
|
||||
import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../dashboard_container';
|
||||
import { placePanelBeside } from '../dashboard_container/component/panel/dashboard_panel_placement';
|
||||
|
||||
export const ACTION_CLONE_PANEL = 'clonePanel';
|
||||
|
||||
|
@ -47,7 +40,7 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
|
|||
|
||||
private toastsService;
|
||||
|
||||
constructor(private savedObjects: SavedObjectsStart) {
|
||||
constructor() {
|
||||
({
|
||||
notifications: { toasts: this.toastsService },
|
||||
} = pluginServices.getServices());
|
||||
|
@ -89,8 +82,37 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
|
|||
throw new PanelNotFoundError();
|
||||
}
|
||||
|
||||
dashboard.showPlaceholderUntil(
|
||||
this.cloneEmbeddable(panelToClone, embeddable),
|
||||
const clonedPanelState: PanelState<EmbeddableInput> = await (async () => {
|
||||
const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || '');
|
||||
const id = uuidv4();
|
||||
if (isReferenceOrValueEmbeddable(embeddable)) {
|
||||
return {
|
||||
type: embeddable.type,
|
||||
explicitInput: {
|
||||
...(await embeddable.getInputAsValueType()),
|
||||
hidePanelTitles: panelToClone.explicitInput.hidePanelTitles,
|
||||
title: newTitle,
|
||||
id,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: embeddable.type,
|
||||
explicitInput: {
|
||||
...panelToClone.explicitInput,
|
||||
title: newTitle,
|
||||
id,
|
||||
},
|
||||
};
|
||||
})();
|
||||
this.toastsService.addSuccess({
|
||||
title: dashboardClonePanelActionStrings.getSuccessMessage(),
|
||||
'data-test-subj': 'addObjectToContainerSuccess',
|
||||
});
|
||||
|
||||
const { otherPanels, newPanel } = createPanelState(
|
||||
clonedPanelState,
|
||||
dashboard.getInput().panels,
|
||||
placePanelBeside,
|
||||
{
|
||||
width: panelToClone.gridData.w,
|
||||
|
@ -98,8 +120,15 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
|
|||
currentPanels: dashboard.getInput().panels,
|
||||
placeBesideId: panelToClone.explicitInput.id,
|
||||
scrollToPanel: true,
|
||||
} as IPanelPlacementBesideArgs
|
||||
}
|
||||
);
|
||||
|
||||
dashboard.updateInput({
|
||||
panels: {
|
||||
...otherPanels,
|
||||
[newPanel.explicitInput.id]: newPanel,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async getCloneTitle(embeddable: IEmbeddable, rawTitle: string) {
|
||||
|
@ -109,109 +138,20 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
|
|||
const cloneRegex = new RegExp(`\\(${clonedTag}\\)`, 'g');
|
||||
const cloneNumberRegex = new RegExp(`\\(${clonedTag} [0-9]+\\)`, 'g');
|
||||
const baseTitle = rawTitle.replace(cloneNumberRegex, '').replace(cloneRegex, '').trim();
|
||||
let similarTitles: string[];
|
||||
if (
|
||||
isReferenceOrValueEmbeddable(embeddable) ||
|
||||
!_.has(embeddable.getExplicitInput(), 'savedObjectId')
|
||||
) {
|
||||
const dashboard: DashboardContainer = embeddable.getRoot() as DashboardContainer;
|
||||
similarTitles = _.filter(await dashboard.getPanelTitles(), (title: string) => {
|
||||
return title.startsWith(baseTitle);
|
||||
});
|
||||
} else {
|
||||
const perPage = 10;
|
||||
const similarSavedObjects = await this.savedObjects.client.find<SavedObject>({
|
||||
type: embeddable.type,
|
||||
perPage,
|
||||
fields: ['title'],
|
||||
searchFields: ['title'],
|
||||
search: `"${baseTitle}"`,
|
||||
});
|
||||
if (similarSavedObjects.total <= perPage) {
|
||||
similarTitles = similarSavedObjects.savedObjects.map((savedObject) => {
|
||||
return savedObject.get('title');
|
||||
});
|
||||
} else {
|
||||
similarTitles = [baseTitle + ` (${clonedTag} ${similarSavedObjects.total - 1})`];
|
||||
}
|
||||
}
|
||||
const dashboard: DashboardContainer = embeddable.getRoot() as DashboardContainer;
|
||||
const similarTitles = filter(await dashboard.getPanelTitles(), (title: string) => {
|
||||
return title.startsWith(baseTitle);
|
||||
});
|
||||
|
||||
const cloneNumbers = _.map(similarTitles, (title: string) => {
|
||||
const cloneNumbers = map(similarTitles, (title: string) => {
|
||||
if (title.match(cloneRegex)) return 0;
|
||||
const cloneTag = title.match(cloneNumberRegex);
|
||||
return cloneTag ? parseInt(cloneTag[0].replace(/[^0-9.]/g, ''), 10) : -1;
|
||||
});
|
||||
const similarBaseTitlesCount = _.max(cloneNumbers) || 0;
|
||||
const similarBaseTitlesCount = max(cloneNumbers) || 0;
|
||||
|
||||
return similarBaseTitlesCount < 0
|
||||
? baseTitle + ` (${clonedTag})`
|
||||
: baseTitle + ` (${clonedTag} ${similarBaseTitlesCount + 1})`;
|
||||
}
|
||||
|
||||
private async addCloneToLibrary(
|
||||
embeddable: IEmbeddable,
|
||||
objectIdToClone: string
|
||||
): Promise<string> {
|
||||
// TODO: Remove this entire functionality. See https://github.com/elastic/kibana/issues/158632 for more info.
|
||||
const savedObjectToClone = await this.savedObjects.client.get<SavedObject>(
|
||||
embeddable.type,
|
||||
objectIdToClone
|
||||
);
|
||||
|
||||
// Clone the saved object
|
||||
const newTitle = await this.getCloneTitle(embeddable, savedObjectToClone.attributes.title);
|
||||
const clonedSavedObject = await this.savedObjects.client.create(
|
||||
embeddable.type,
|
||||
{
|
||||
..._.cloneDeep(savedObjectToClone.attributes),
|
||||
title: newTitle,
|
||||
},
|
||||
{ references: _.cloneDeep(savedObjectToClone.references) }
|
||||
);
|
||||
return clonedSavedObject.id;
|
||||
}
|
||||
|
||||
private async cloneEmbeddable(
|
||||
panelToClone: DashboardPanelState,
|
||||
embeddable: IEmbeddable
|
||||
): Promise<Partial<PanelState>> {
|
||||
let panelState: PanelState<EmbeddableInput>;
|
||||
if (isReferenceOrValueEmbeddable(embeddable)) {
|
||||
const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || '');
|
||||
panelState = {
|
||||
type: embeddable.type,
|
||||
explicitInput: {
|
||||
...(await embeddable.getInputAsValueType()),
|
||||
id: uuidv4(),
|
||||
title: newTitle,
|
||||
hidePanelTitles: panelToClone.explicitInput.hidePanelTitles,
|
||||
},
|
||||
version: panelToClone.version,
|
||||
};
|
||||
} else {
|
||||
panelState = {
|
||||
type: embeddable.type,
|
||||
explicitInput: {
|
||||
...panelToClone.explicitInput,
|
||||
id: uuidv4(),
|
||||
},
|
||||
version: panelToClone.version,
|
||||
};
|
||||
|
||||
// TODO Remove the entire `addCloneToLibrary` section from here.
|
||||
if (panelToClone.explicitInput.savedObjectId) {
|
||||
const clonedSavedObjectId = await this.addCloneToLibrary(
|
||||
embeddable,
|
||||
panelToClone.explicitInput.savedObjectId
|
||||
);
|
||||
(panelState.explicitInput as SavedObjectEmbeddableInput).savedObjectId =
|
||||
clonedSavedObjectId;
|
||||
}
|
||||
}
|
||||
this.toastsService.addSuccess({
|
||||
title: dashboardClonePanelActionStrings.getSuccessMessage(),
|
||||
'data-test-subj': 'addObjectToContainerSuccess',
|
||||
});
|
||||
return panelState;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export const buildAllDashboardActions = async ({
|
|||
}: BuildAllDashboardActionsProps) => {
|
||||
const { uiActions, share, presentationUtil, savedObjectsTaggingOss, contentManagement } = plugins;
|
||||
|
||||
const clonePanelAction = new ClonePanelAction(core.savedObjects);
|
||||
const clonePanelAction = new ClonePanelAction();
|
||||
uiActions.registerAction(clonePanelAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id);
|
||||
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
export { showSettings } from './show_settings';
|
||||
export { addFromLibrary } from './add_panel_from_library';
|
||||
export { runSaveAs, runQuickSave, runClone } from './run_save_functions';
|
||||
export { addOrUpdateEmbeddable, replacePanel, showPlaceholderUntil } from './panel_management';
|
||||
export { addOrUpdateEmbeddable, replacePanel } from './panel_management';
|
||||
|
|
|
@ -14,14 +14,8 @@ import {
|
|||
} from '@kbn/embeddable-plugin/public';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
IPanelPlacementArgs,
|
||||
PanelPlacementMethod,
|
||||
} from '../../component/panel/dashboard_panel_placement';
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { createPanelState } from '../../component/panel';
|
||||
import { DashboardContainer } from '../dashboard_container';
|
||||
import { PLACEHOLDER_EMBEDDABLE } from '../../../placeholder_embeddable';
|
||||
|
||||
export async function addOrUpdateEmbeddable<
|
||||
EEI extends EmbeddableInput = EmbeddableInput,
|
||||
|
@ -89,51 +83,3 @@ export async function replacePanel(
|
|||
await this.updateInput({ panels });
|
||||
return panelId;
|
||||
}
|
||||
|
||||
export function showPlaceholderUntil<TPlacementMethodArgs extends IPanelPlacementArgs>(
|
||||
this: DashboardContainer,
|
||||
newStateComplete: Promise<Partial<PanelState>>,
|
||||
placementMethod?: PanelPlacementMethod<TPlacementMethodArgs>,
|
||||
placementArgs?: TPlacementMethodArgs
|
||||
): void {
|
||||
const originalPanelState = {
|
||||
type: PLACEHOLDER_EMBEDDABLE,
|
||||
explicitInput: {
|
||||
id: uuidv4(),
|
||||
disabledActions: [
|
||||
'ACTION_CUSTOMIZE_PANEL',
|
||||
'CUSTOM_TIME_RANGE',
|
||||
'clonePanel',
|
||||
'replacePanel',
|
||||
'togglePanel',
|
||||
],
|
||||
},
|
||||
} as PanelState<EmbeddableInput>;
|
||||
|
||||
const { otherPanels, newPanel: placeholderPanelState } = createPanelState(
|
||||
originalPanelState,
|
||||
this.input.panels,
|
||||
placementMethod,
|
||||
placementArgs
|
||||
);
|
||||
|
||||
this.updateInput({
|
||||
panels: {
|
||||
...otherPanels,
|
||||
[placeholderPanelState.explicitInput.id]: placeholderPanelState,
|
||||
},
|
||||
});
|
||||
|
||||
// wait until the placeholder is ready, then replace it with new panel
|
||||
// this is useful as sometimes panels can load faster than the placeholder one (i.e. by value embeddables)
|
||||
this.untilEmbeddableLoaded(originalPanelState.explicitInput.id)
|
||||
.then(() => newStateComplete)
|
||||
.then(async (newPanelState: Partial<PanelState>) => {
|
||||
const panelId = await this.replacePanel(placeholderPanelState, newPanelState);
|
||||
|
||||
if (placementArgs?.scrollToPanel) {
|
||||
this.setScrollToPanelId(panelId);
|
||||
this.setHighlightPanelId(panelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ import {
|
|||
runQuickSave,
|
||||
replacePanel,
|
||||
addFromLibrary,
|
||||
showPlaceholderUntil,
|
||||
addOrUpdateEmbeddable,
|
||||
} from './api';
|
||||
|
||||
|
@ -312,7 +311,6 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
public addFromLibrary = addFromLibrary;
|
||||
|
||||
public replacePanel = replacePanel;
|
||||
public showPlaceholderUntil = showPlaceholderUntil;
|
||||
public addOrUpdateEmbeddable = addOrUpdateEmbeddable;
|
||||
|
||||
public forceRefresh(refreshControlGroup: boolean = true) {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { PlaceholderEmbeddableFactory } from './placeholder_embeddable_factory';
|
||||
|
||||
export const PLACEHOLDER_EMBEDDABLE = 'placeholder';
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { Embeddable, type EmbeddableInput, type IContainer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { PLACEHOLDER_EMBEDDABLE } from '.';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
|
||||
export class PlaceholderEmbeddable extends Embeddable {
|
||||
public readonly type = PLACEHOLDER_EMBEDDABLE;
|
||||
private node?: HTMLElement;
|
||||
|
||||
constructor(initialInput: EmbeddableInput, parent?: IContainer) {
|
||||
super(initialInput, {}, parent);
|
||||
this.input = initialInput;
|
||||
}
|
||||
public render(node: HTMLElement) {
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
this.node = node;
|
||||
|
||||
const {
|
||||
settings: {
|
||||
theme: { theme$ },
|
||||
},
|
||||
} = pluginServices.getServices();
|
||||
|
||||
const classes = classNames('embPanel', 'embPanel-isLoading');
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={theme$}>
|
||||
<div className={classes}>
|
||||
<EuiLoadingChart size="l" mono />
|
||||
</div>
|
||||
</KibanaThemeProvider>,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
public reload() {}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableInput,
|
||||
IContainer,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { PLACEHOLDER_EMBEDDABLE } from '.';
|
||||
|
||||
export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition {
|
||||
public readonly type = PLACEHOLDER_EMBEDDABLE;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public canCreateNew() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async create(initialInput: EmbeddableInput, parent?: IContainer) {
|
||||
const { PlaceholderEmbeddable } = await import('./placeholder_embeddable');
|
||||
return new PlaceholderEmbeddable(initialInput, parent);
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('dashboard.placeholder.factory.displayName', {
|
||||
defaultMessage: 'placeholder',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
## What is this for?
|
||||
|
||||
This Placeholder Embeddable is shown when a BY REFERENCE panel (a panel which is linked to a saved object) is cloned using the Dashboard Panel Clone action.
|
||||
|
||||
## Why was it made?
|
||||
|
||||
This was important for the first iteration of the clone feature so that something could be shown while the saved object was being duplicated, but later iterations of that feature automatically unlink panels on clone. By Value panels don't need a placeholder because they load much faster.
|
||||
|
||||
## Can I delete it?
|
||||
|
||||
Currently, the only embeddable type that cannot be loaded by value is the Discover Saved Search Embeddable. Without a placeholder embeddable, the dashboard wouldn't reflow at all until after the saved object clone operation is complete.
|
||||
|
||||
The placeholder embeddable should be removed as soon as the Discover Saved Search Embeddable can be saved By Value.
|
|
@ -67,7 +67,6 @@ import {
|
|||
SEARCH_SESSION_ID,
|
||||
} from './dashboard_constants';
|
||||
import { DashboardMountContextProps } from './dashboard_app/types';
|
||||
import { PlaceholderEmbeddableFactory } from './placeholder_embeddable';
|
||||
import type { FindDashboardsService } from './services/dashboard_content_management/types';
|
||||
import { CONTENT_ID, LATEST_VERSION } from '../common/content_management';
|
||||
|
||||
|
@ -220,9 +219,6 @@ export class DashboardPlugin
|
|||
dashboardContainerFactory.type,
|
||||
dashboardContainerFactory
|
||||
);
|
||||
|
||||
const placeholderFactory = new PlaceholderEmbeddableFactory();
|
||||
embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory);
|
||||
});
|
||||
|
||||
this.stopUrlTracking = () => {
|
||||
|
|
|
@ -1225,7 +1225,6 @@
|
|||
"dashboard.panel.title.clonedTag": "copier",
|
||||
"dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "Impossible de migrer les données du panneau pour une rétro-compatibilité \"6.1.0\". Le panneau ne contient pas les champs de colonne et/ou de ligne attendus.",
|
||||
"dashboard.panel.unlinkFromLibrary": "Dissocier de la bibliothèque",
|
||||
"dashboard.placeholder.factory.displayName": "paramètre fictif",
|
||||
"dashboard.resetChangesConfirmModal.confirmButtonLabel": "Réinitialiser le tableau de bord",
|
||||
"dashboard.resetChangesConfirmModal.resetChangesDescription": "Ce tableau de bord va revenir à son dernier état d'enregistrement. Vous risquez de perdre les modifications apportées aux filtres et aux requêtes.",
|
||||
"dashboard.resetChangesConfirmModal.resetChangesTitle": "Réinitialiser le tableau de bord ?",
|
||||
|
|
|
@ -1239,7 +1239,6 @@
|
|||
"dashboard.panel.title.clonedTag": "コピー",
|
||||
"dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません",
|
||||
"dashboard.panel.unlinkFromLibrary": "ライブラリからのリンクを解除",
|
||||
"dashboard.placeholder.factory.displayName": "プレースホルダー",
|
||||
"dashboard.resetChangesConfirmModal.confirmButtonLabel": "ダッシュボードをリセット",
|
||||
"dashboard.resetChangesConfirmModal.resetChangesDescription": "このダッシュボードは最後に保存された状態に戻ります。 フィルターとクエリの変更が失われる場合があります。",
|
||||
"dashboard.resetChangesConfirmModal.resetChangesTitle": "ダッシュボードをリセットしますか?",
|
||||
|
|
|
@ -1239,7 +1239,6 @@
|
|||
"dashboard.panel.title.clonedTag": "副本",
|
||||
"dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段",
|
||||
"dashboard.panel.unlinkFromLibrary": "取消与库的链接",
|
||||
"dashboard.placeholder.factory.displayName": "占位符",
|
||||
"dashboard.resetChangesConfirmModal.confirmButtonLabel": "重置仪表板",
|
||||
"dashboard.resetChangesConfirmModal.resetChangesDescription": "此仪表板将返回到其最后保存的状态。 您可能会丢失对筛选和查询的更改。",
|
||||
"dashboard.resetChangesConfirmModal.resetChangesTitle": "重置仪表板?",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue