mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[dashboard] decouple "link to library" and "unlink from library" actions from AttributeService (#178584)
There are 2 paths for decoupling from AttributeService 1. replace existing actions with new actions that use `HasLibraryTransforms` interface. Update all embeddables to implement `HasLibraryTransforms` interfaces. 2. Rename existing actions as `legacy`. Create new actions that use `HasLibraryTransforms` interface. Provide a reference implementation for new actions. Convert embeddables to new `HasLibraryTransforms` interface as the embeddables get converted to react embeddables. Option 2 was chosen to limit scope and gradually convert embeddables to `HasLibraryTransforms` interface. This PR: 1. Remove `@kbn/presentation-library` package. Interfaces in this package have been rolled into `@kbn/presentation-publishing` package. 2. Rename existing interface as `HasLegacyLibraryTransforms` 3. Create new interface `HasLibraryTransforms` 4. Rename `AddToLibraryAction` action to `LegacyAddToLibraryAction`. Modify action to use `HasLegacyLibraryTransforms` interface and guards. 5. Rename `UnlinkFromLibraryAction` action to `LegacyUnlinkFromLibraryAction`. Modify action to use `HasLegacyLibraryTransforms` interface and guards. 6. Rename `LibraryNotificationAction` action to `LegacyLibraryNotificationAction`. Modify action to use `LegacyUnlinkFromLibraryAction`. 7. Create new `AddToLibraryAction`. Code action to use `HasLibraryTransforms` interface. 8. Create new `UnlinkFromLibraryAction`. Code action to use `HasLibraryTransforms` interface. 9. Create new `LibraryNotificationAction`. Code action to use `UnlinkFromLibraryAction` action. 10. Update MapEmbeddable to implement `HasLibraryTransforms` interface so that Map embeddable can be used to test new actions. ### Test 1. install sample web logs 2. create new dashboard 3. Click "Add panel" and select "Maps". 4. Click "Save and return". 5. Save dashboard. Inspect dashboard saved object. Verify panel is by-value and contains `attributes` in `panelsJSON` <img width="300" alt="Screenshot 2024-03-22 at 2 49 56 PM" src="49189613
-f7c4-435d-88ab-d9c8ceb1575f"> 6. Go back to dashboard and open context menu. Click "more" and then click "Save to library". 7. Save dashboard. Inspect dashboard saved object. Verify panel is by-reference and does not contain `attributes` in `panelsJSON`. <img width="300" alt="Screenshot 2024-03-22 at 2 52 19 PM" src="e3b2eace
-a48d-4dd0-a771-f22436d72935"> 8. Go to maps listing page. Verify map is displayed in listing page. Open map and verify it opens. Add some new layers and save map. 11. Go back to dashboard. Verify map contains new layers added to saved object. 12. Open context menu. Click "more" and then click "Unlink from library". 13. Save dashboard and verify map panel is now by-value again. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3712fa8978
commit
c8bfea57c1
42 changed files with 603 additions and 167 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -616,7 +616,6 @@ packages/kbn-plugin-helpers @elastic/kibana-operations
|
|||
examples/portable_dashboards_example @elastic/kibana-presentation
|
||||
examples/preboot_example @elastic/kibana-security @elastic/kibana-core
|
||||
packages/presentation/presentation_containers @elastic/kibana-presentation
|
||||
packages/presentation/presentation_library @elastic/kibana-presentation
|
||||
src/plugins/presentation_panel @elastic/kibana-presentation
|
||||
packages/presentation/presentation_publishing @elastic/kibana-presentation
|
||||
src/plugins/presentation_util @elastic/kibana-presentation
|
||||
|
|
|
@ -625,7 +625,6 @@
|
|||
"@kbn/portable-dashboards-example": "link:examples/portable_dashboards_example",
|
||||
"@kbn/preboot-example-plugin": "link:examples/preboot_example",
|
||||
"@kbn/presentation-containers": "link:packages/presentation/presentation_containers",
|
||||
"@kbn/presentation-library": "link:packages/presentation/presentation_library",
|
||||
"@kbn/presentation-panel-plugin": "link:src/plugins/presentation_panel",
|
||||
"@kbn/presentation-publishing": "link:packages/presentation/presentation_publishing",
|
||||
"@kbn/presentation-util-plugin": "link:src/plugins/presentation_util",
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# @kbn/presentation-library
|
||||
|
||||
Contains interfaces and type guards to be used to mediate the relationship between panels / charts, and a content library.
|
|
@ -1,13 +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 { type CanLinkToLibrary, apiCanLinkToLibrary } from './interfaces/can_link_to_library';
|
||||
export {
|
||||
type CanUnlinkFromLibrary,
|
||||
apiCanUnlinkFromLibrary,
|
||||
} from './interfaces/can_unlink_from_library';
|
|
@ -1,16 +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 interface CanLinkToLibrary {
|
||||
canLinkToLibrary: () => Promise<boolean>;
|
||||
linkToLibrary: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const apiCanLinkToLibrary = (api: unknown): api is CanLinkToLibrary =>
|
||||
typeof (api as CanLinkToLibrary).canLinkToLibrary === 'function' &&
|
||||
typeof (api as CanLinkToLibrary).linkToLibrary === 'function';
|
|
@ -1,16 +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 interface CanUnlinkFromLibrary {
|
||||
canUnlinkFromLibrary: () => Promise<boolean>;
|
||||
unlinkFromLibrary: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const apiCanUnlinkFromLibrary = (api: unknown): api is CanUnlinkFromLibrary =>
|
||||
typeof (api as CanUnlinkFromLibrary).canUnlinkFromLibrary === 'function' &&
|
||||
typeof (api as CanUnlinkFromLibrary).unlinkFromLibrary === 'function';
|
|
@ -1,13 +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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/packages/presentation/presentation_library'],
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/presentation-library",
|
||||
"owner": "@elastic/kibana-presentation"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "@kbn/presentation-library",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": ["jest", "node", "react"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["target/**/*"],
|
||||
"kbn_references": []
|
||||
}
|
|
@ -100,6 +100,12 @@ export {
|
|||
type PublishesWritablePanelTitle,
|
||||
} from './interfaces/titles/publishes_panel_title';
|
||||
export { initializeTitles, type SerializedTitles } from './interfaces/titles/titles_api';
|
||||
export {
|
||||
type HasLibraryTransforms,
|
||||
apiHasLibraryTransforms,
|
||||
type HasLegacyLibraryTransforms,
|
||||
apiHasLegacyLibraryTransforms,
|
||||
} from './interfaces/has_library_transforms';
|
||||
export {
|
||||
useBatchedPublishingSubjects,
|
||||
usePublishingSubject,
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 interface HasLibraryTransforms<StateT extends object = object> {
|
||||
//
|
||||
// Add to library methods
|
||||
//
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* True when embeddable is by-value and can be converted to by-reference
|
||||
*/
|
||||
canLinkToLibrary: () => Promise<boolean>;
|
||||
/**
|
||||
* Saves embeddable to library
|
||||
*
|
||||
* @returns {Promise<{ state: StateT; savedObjectId: string }>}
|
||||
* state: by-reference embeddable state replacing by-value embeddable state
|
||||
* savedObjectId: Saved object id for new saved object added to library
|
||||
*/
|
||||
saveStateToSavedObject: (title: string) => Promise<{ state: StateT; savedObjectId: string }>;
|
||||
checkForDuplicateTitle: (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => Promise<void>;
|
||||
|
||||
//
|
||||
// Unlink from library methods
|
||||
//
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
* True when embeddable is by-reference and can be converted to by-value
|
||||
*/
|
||||
canUnlinkFromLibrary: () => Promise<boolean>;
|
||||
/**
|
||||
*
|
||||
* @returns {StateT}
|
||||
* by-value embeddable state replacing by-reference embeddable state
|
||||
*/
|
||||
savedObjectAttributesToState: () => StateT;
|
||||
}
|
||||
|
||||
export const apiHasLibraryTransforms = <StateT extends object = object>(
|
||||
unknownApi: null | unknown
|
||||
): unknownApi is HasLibraryTransforms<StateT> => {
|
||||
return Boolean(
|
||||
unknownApi &&
|
||||
typeof (unknownApi as HasLibraryTransforms<StateT>).canLinkToLibrary === 'function' &&
|
||||
typeof (unknownApi as HasLibraryTransforms<StateT>).canUnlinkFromLibrary === 'function' &&
|
||||
typeof (unknownApi as HasLibraryTransforms<StateT>).saveStateToSavedObject === 'function' &&
|
||||
typeof (unknownApi as HasLibraryTransforms<StateT>).savedObjectAttributesToState ===
|
||||
'function' &&
|
||||
typeof (unknownApi as HasLibraryTransforms<StateT>).checkForDuplicateTitle === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use HasLibraryTransforms instead
|
||||
*/
|
||||
export type HasLegacyLibraryTransforms = Pick<
|
||||
HasLibraryTransforms,
|
||||
'canLinkToLibrary' | 'canUnlinkFromLibrary'
|
||||
> & {
|
||||
linkToLibrary: () => Promise<void>;
|
||||
unlinkFromLibrary: () => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use apiHasLibraryTransforms instead
|
||||
*/
|
||||
export const apiHasLegacyLibraryTransforms = (
|
||||
unknownApi: null | unknown
|
||||
): unknownApi is HasLegacyLibraryTransforms => {
|
||||
return Boolean(
|
||||
unknownApi &&
|
||||
typeof (unknownApi as HasLegacyLibraryTransforms).canLinkToLibrary === 'function' &&
|
||||
typeof (unknownApi as HasLegacyLibraryTransforms).canUnlinkFromLibrary === 'function' &&
|
||||
typeof (unknownApi as HasLegacyLibraryTransforms).linkToLibrary === 'function' &&
|
||||
typeof (unknownApi as HasLegacyLibraryTransforms).unlinkFromLibrary === 'function'
|
||||
);
|
||||
};
|
|
@ -6,27 +6,53 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { apiCanLinkToLibrary, CanLinkToLibrary } from '@kbn/presentation-library';
|
||||
import React from 'react';
|
||||
import {
|
||||
apiCanAccessViewMode,
|
||||
apiHasLibraryTransforms,
|
||||
EmbeddableApiContext,
|
||||
getPanelTitle,
|
||||
PublishesPanelTitle,
|
||||
CanAccessViewMode,
|
||||
getInheritedViewMode,
|
||||
HasLibraryTransforms,
|
||||
HasType,
|
||||
HasTypeDisplayName,
|
||||
apiHasType,
|
||||
HasUniqueId,
|
||||
HasParentApi,
|
||||
apiHasUniqueId,
|
||||
apiHasParentApi,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import {
|
||||
OnSaveProps,
|
||||
SavedObjectSaveModal,
|
||||
SaveResult,
|
||||
showSaveModal,
|
||||
} from '@kbn/saved-objects-plugin/public';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary';
|
||||
|
||||
export type AddPanelToLibraryActionApi = CanAccessViewMode &
|
||||
CanLinkToLibrary &
|
||||
Partial<PublishesPanelTitle>;
|
||||
HasType &
|
||||
HasUniqueId &
|
||||
HasLibraryTransforms &
|
||||
HasParentApi<Pick<PresentationContainer, 'replacePanel'>> &
|
||||
Partial<PublishesPanelTitle & HasTypeDisplayName>;
|
||||
|
||||
const isApiCompatible = (api: unknown | null): api is AddPanelToLibraryActionApi =>
|
||||
Boolean(apiCanAccessViewMode(api) && apiCanLinkToLibrary(api));
|
||||
Boolean(
|
||||
apiCanAccessViewMode(api) &&
|
||||
apiHasLibraryTransforms(api) &&
|
||||
apiHasType(api) &&
|
||||
apiHasUniqueId(api) &&
|
||||
apiHasParentApi(api) &&
|
||||
typeof (api.parentApi as PresentationContainer)?.replacePanel === 'function'
|
||||
);
|
||||
|
||||
export class AddToLibraryAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_ADD_TO_LIBRARY;
|
||||
|
@ -58,18 +84,53 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
|
|||
|
||||
public async execute({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
const panelTitle = getPanelTitle(embeddable);
|
||||
const title = getPanelTitle(embeddable);
|
||||
|
||||
try {
|
||||
await embeddable.linkToLibrary();
|
||||
const byRefState = await new Promise<object>((resolve, reject) => {
|
||||
const onSave = async (props: OnSaveProps): Promise<SaveResult> => {
|
||||
await embeddable.checkForDuplicateTitle(
|
||||
props.newTitle,
|
||||
props.isTitleDuplicateConfirmed,
|
||||
props.onTitleDuplicate
|
||||
);
|
||||
try {
|
||||
const { state, savedObjectId } = await embeddable.saveStateToSavedObject(
|
||||
props.newTitle
|
||||
);
|
||||
resolve({ ...state, title: props.newTitle });
|
||||
return { id: savedObjectId };
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
showSaveModal(
|
||||
<SavedObjectSaveModal
|
||||
onSave={onSave}
|
||||
onClose={() => {}}
|
||||
title={title ?? ''}
|
||||
showCopyOnSave={false}
|
||||
objectType={
|
||||
typeof embeddable.getTypeDisplayName === 'function'
|
||||
? embeddable.getTypeDisplayName()
|
||||
: embeddable.type
|
||||
}
|
||||
showDescription={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await embeddable.parentApi.replacePanel(embeddable.uuid, {
|
||||
panelType: embeddable.type,
|
||||
initialState: byRefState,
|
||||
});
|
||||
this.toastsService.addSuccess({
|
||||
title: dashboardAddToLibraryActionStrings.getSuccessMessage(
|
||||
panelTitle ? `'${panelTitle}'` : ''
|
||||
),
|
||||
title: dashboardAddToLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
|
||||
'data-test-subj': 'addPanelToLibrarySuccess',
|
||||
});
|
||||
} catch (e) {
|
||||
this.toastsService.addDanger({
|
||||
title: dashboardAddToLibraryActionStrings.getErrorMessage(panelTitle),
|
||||
title: dashboardAddToLibraryActionStrings.getErrorMessage(title),
|
||||
'data-test-subj': 'addPanelToLibraryError',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,14 +15,17 @@ import {
|
|||
|
||||
import { DashboardStartDependencies } from '../plugin';
|
||||
import { AddToLibraryAction } from './add_to_library_action';
|
||||
import { LegacyAddToLibraryAction } from './legacy_add_to_library_action';
|
||||
import { ClonePanelAction } from './clone_panel_action';
|
||||
import { CopyToDashboardAction } from './copy_to_dashboard_action';
|
||||
import { ExpandPanelAction } from './expand_panel_action';
|
||||
import { ExportCSVAction } from './export_csv_action';
|
||||
import { FiltersNotificationAction } from './filters_notification_action';
|
||||
import { LibraryNotificationAction } from './library_notification_action';
|
||||
import { LegacyLibraryNotificationAction } from './legacy_library_notification_action';
|
||||
import { ReplacePanelAction } from './replace_panel_action';
|
||||
import { UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||
import { LegacyUnlinkFromLibraryAction } from './legacy_unlink_from_library_action';
|
||||
import { LibraryNotificationAction } from './library_notification_action';
|
||||
|
||||
interface BuildAllDashboardActionsProps {
|
||||
core: CoreStart;
|
||||
|
@ -70,6 +73,10 @@ export const buildAllDashboardActions = async ({
|
|||
uiActions.registerAction(addToLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id);
|
||||
|
||||
const legacyAddToLibraryAction = new LegacyAddToLibraryAction();
|
||||
uiActions.registerAction(legacyAddToLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, legacyAddToLibraryAction.id);
|
||||
|
||||
const unlinkFromLibraryAction = new UnlinkFromLibraryAction();
|
||||
uiActions.registerAction(unlinkFromLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id);
|
||||
|
@ -78,6 +85,16 @@ export const buildAllDashboardActions = async ({
|
|||
uiActions.registerAction(libraryNotificationAction);
|
||||
uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id);
|
||||
|
||||
const legacyUnlinkFromLibraryAction = new LegacyUnlinkFromLibraryAction();
|
||||
uiActions.registerAction(legacyUnlinkFromLibraryAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, legacyUnlinkFromLibraryAction.id);
|
||||
|
||||
const legacyLibraryNotificationAction = new LegacyLibraryNotificationAction(
|
||||
legacyUnlinkFromLibraryAction
|
||||
);
|
||||
uiActions.registerAction(legacyLibraryNotificationAction);
|
||||
uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, legacyLibraryNotificationAction.id);
|
||||
|
||||
const copyToDashboardAction = new CopyToDashboardAction(core);
|
||||
uiActions.registerAction(copyToDashboardAction);
|
||||
uiActions.attachAction(CONTEXT_MENU_TRIGGER, copyToDashboardAction.id);
|
||||
|
|
|
@ -9,18 +9,23 @@
|
|||
import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { AddToLibraryAction, AddPanelToLibraryActionApi } from './add_to_library_action';
|
||||
import {
|
||||
LegacyAddToLibraryAction,
|
||||
LegacyAddPanelToLibraryActionApi,
|
||||
} from './legacy_add_to_library_action';
|
||||
|
||||
describe('Add to library action', () => {
|
||||
let action: AddToLibraryAction;
|
||||
let context: { embeddable: AddPanelToLibraryActionApi };
|
||||
let action: LegacyAddToLibraryAction;
|
||||
let context: { embeddable: LegacyAddPanelToLibraryActionApi };
|
||||
|
||||
beforeEach(() => {
|
||||
action = new AddToLibraryAction();
|
||||
action = new LegacyAddToLibraryAction();
|
||||
context = {
|
||||
embeddable: {
|
||||
linkToLibrary: jest.fn(),
|
||||
canLinkToLibrary: jest.fn().mockResolvedValue(true),
|
||||
unlinkFromLibrary: jest.fn(),
|
||||
canUnlinkFromLibrary: jest.fn().mockResolvedValue(true),
|
||||
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
panelTitle: new BehaviorSubject<string | undefined>('A very compatible API'),
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 {
|
||||
apiCanAccessViewMode,
|
||||
apiHasLegacyLibraryTransforms,
|
||||
EmbeddableApiContext,
|
||||
getPanelTitle,
|
||||
PublishesPanelTitle,
|
||||
CanAccessViewMode,
|
||||
getInheritedViewMode,
|
||||
HasLegacyLibraryTransforms,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export const ACTION_LEGACY_ADD_TO_LIBRARY = 'legacySaveToLibrary';
|
||||
|
||||
export type LegacyAddPanelToLibraryActionApi = CanAccessViewMode &
|
||||
HasLegacyLibraryTransforms &
|
||||
Partial<PublishesPanelTitle>;
|
||||
|
||||
const isApiCompatible = (api: unknown | null): api is LegacyAddPanelToLibraryActionApi =>
|
||||
Boolean(apiCanAccessViewMode(api) && apiHasLegacyLibraryTransforms(api));
|
||||
|
||||
export class LegacyAddToLibraryAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_LEGACY_ADD_TO_LIBRARY;
|
||||
public readonly id = ACTION_LEGACY_ADD_TO_LIBRARY;
|
||||
public order = 15;
|
||||
|
||||
private toastsService;
|
||||
|
||||
constructor() {
|
||||
({
|
||||
notifications: { toasts: this.toastsService },
|
||||
} = pluginServices.getServices());
|
||||
}
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return dashboardAddToLibraryActionStrings.getDisplayName();
|
||||
}
|
||||
|
||||
public getIconType({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return 'folderCheck';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) return false;
|
||||
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canLinkToLibrary());
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
const panelTitle = getPanelTitle(embeddable);
|
||||
try {
|
||||
await embeddable.linkToLibrary();
|
||||
this.toastsService.addSuccess({
|
||||
title: dashboardAddToLibraryActionStrings.getSuccessMessage(
|
||||
panelTitle ? `'${panelTitle}'` : ''
|
||||
),
|
||||
'data-test-subj': 'addPanelToLibrarySuccess',
|
||||
});
|
||||
} catch (e) {
|
||||
this.toastsService.addDanger({
|
||||
title: dashboardAddToLibraryActionStrings.getErrorMessage(panelTitle),
|
||||
'data-test-subj': 'addPanelToLibraryError',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,16 +9,16 @@
|
|||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { LibraryNotificationAction } from './library_notification_action';
|
||||
import { LegacyLibraryNotificationAction } from './legacy_library_notification_action';
|
||||
import {
|
||||
UnlinkFromLibraryAction,
|
||||
UnlinkPanelFromLibraryActionApi,
|
||||
} from './unlink_from_library_action';
|
||||
LegacyUnlinkFromLibraryAction,
|
||||
LegacyUnlinkPanelFromLibraryActionApi,
|
||||
} from './legacy_unlink_from_library_action';
|
||||
|
||||
describe('library notification action', () => {
|
||||
let action: LibraryNotificationAction;
|
||||
let unlinkAction: UnlinkFromLibraryAction;
|
||||
let context: { embeddable: UnlinkPanelFromLibraryActionApi };
|
||||
let action: LegacyLibraryNotificationAction;
|
||||
let unlinkAction: LegacyUnlinkFromLibraryAction;
|
||||
let context: { embeddable: LegacyUnlinkPanelFromLibraryActionApi };
|
||||
|
||||
let updateViewMode: (viewMode: ViewMode) => void;
|
||||
|
||||
|
@ -26,13 +26,15 @@ describe('library notification action', () => {
|
|||
const viewModeSubject = new BehaviorSubject<ViewMode>('edit');
|
||||
updateViewMode = (viewMode) => viewModeSubject.next(viewMode);
|
||||
|
||||
unlinkAction = new UnlinkFromLibraryAction();
|
||||
action = new LibraryNotificationAction(unlinkAction);
|
||||
unlinkAction = new LegacyUnlinkFromLibraryAction();
|
||||
action = new LegacyLibraryNotificationAction(unlinkAction);
|
||||
context = {
|
||||
embeddable: {
|
||||
viewMode: viewModeSubject,
|
||||
canUnlinkFromLibrary: jest.fn().mockResolvedValue(true),
|
||||
unlinkFromLibrary: jest.fn(),
|
||||
canLinkToLibrary: jest.fn().mockResolvedValue(true),
|
||||
linkToLibrary: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 {
|
||||
EmbeddableApiContext,
|
||||
getInheritedViewMode,
|
||||
getViewModeSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { LibraryNotificationPopover } from './library_notification_popover';
|
||||
import {
|
||||
legacyUnlinkActionIsCompatible,
|
||||
LegacyUnlinkFromLibraryAction,
|
||||
} from './legacy_unlink_from_library_action';
|
||||
import { dashboardLibraryNotificationStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export const LEGACY_ACTION_LIBRARY_NOTIFICATION = 'LEGACY_ACTION_LIBRARY_NOTIFICATION';
|
||||
|
||||
export class LegacyLibraryNotificationAction implements Action<EmbeddableApiContext> {
|
||||
public readonly id = LEGACY_ACTION_LIBRARY_NOTIFICATION;
|
||||
public readonly type = LEGACY_ACTION_LIBRARY_NOTIFICATION;
|
||||
public readonly order = 1;
|
||||
|
||||
constructor(private unlinkAction: LegacyUnlinkFromLibraryAction) {}
|
||||
|
||||
public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => {
|
||||
const { embeddable } = context;
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return <LibraryNotificationPopover unlinkAction={this.unlinkAction} api={embeddable} />;
|
||||
};
|
||||
|
||||
public couldBecomeCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
return legacyUnlinkActionIsCompatible(embeddable);
|
||||
}
|
||||
|
||||
public subscribeToCompatibilityChanges(
|
||||
{ embeddable }: EmbeddableApiContext,
|
||||
onChange: (isCompatible: boolean, action: LegacyLibraryNotificationAction) => void
|
||||
) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) return;
|
||||
|
||||
/**
|
||||
* TODO: Upgrade this action by subscribing to changes in the existance of a saved object id. Currently,
|
||||
* this is unnecessary because a link or unlink operation will cause the panel to unmount and remount.
|
||||
*/
|
||||
return getViewModeSubject(embeddable)?.subscribe((viewMode) => {
|
||||
embeddable.canUnlinkFromLibrary().then((canUnlink) => {
|
||||
onChange(viewMode === 'edit' && canUnlink, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return dashboardLibraryNotificationStrings.getDisplayName();
|
||||
}
|
||||
|
||||
public getIconType({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return 'folderCheck';
|
||||
}
|
||||
|
||||
public isCompatible = async ({ embeddable }: EmbeddableApiContext) => {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) return false;
|
||||
return getInheritedViewMode(embeddable) === 'edit' && embeddable.canUnlinkFromLibrary();
|
||||
};
|
||||
|
||||
public execute = async () => {};
|
||||
}
|
|
@ -10,21 +10,22 @@ import { PublishesViewMode, ViewMode } from '@kbn/presentation-publishing';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import {
|
||||
UnlinkFromLibraryAction,
|
||||
UnlinkPanelFromLibraryActionApi,
|
||||
} from './unlink_from_library_action';
|
||||
LegacyUnlinkFromLibraryAction,
|
||||
LegacyUnlinkPanelFromLibraryActionApi,
|
||||
} from './legacy_unlink_from_library_action';
|
||||
|
||||
describe('Unlink from library action', () => {
|
||||
let action: UnlinkFromLibraryAction;
|
||||
let context: { embeddable: UnlinkPanelFromLibraryActionApi };
|
||||
let action: LegacyUnlinkFromLibraryAction;
|
||||
let context: { embeddable: LegacyUnlinkPanelFromLibraryActionApi };
|
||||
|
||||
beforeEach(() => {
|
||||
action = new UnlinkFromLibraryAction();
|
||||
action = new LegacyUnlinkFromLibraryAction();
|
||||
context = {
|
||||
embeddable: {
|
||||
unlinkFromLibrary: jest.fn(),
|
||||
canUnlinkFromLibrary: jest.fn().mockResolvedValue(true),
|
||||
|
||||
linkToLibrary: jest.fn(),
|
||||
canLinkToLibrary: jest.fn().mockResolvedValue(true),
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
panelTitle: new BehaviorSubject<string | undefined>('A very compatible API'),
|
||||
},
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import {
|
||||
apiCanAccessViewMode,
|
||||
apiHasLegacyLibraryTransforms,
|
||||
CanAccessViewMode,
|
||||
EmbeddableApiContext,
|
||||
getInheritedViewMode,
|
||||
getPanelTitle,
|
||||
PublishesPanelTitle,
|
||||
HasLegacyLibraryTransforms,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export const ACTION_LEGACY_UNLINK_FROM_LIBRARY = 'legacyUnlinkFromLibrary';
|
||||
|
||||
export type LegacyUnlinkPanelFromLibraryActionApi = CanAccessViewMode &
|
||||
HasLegacyLibraryTransforms &
|
||||
Partial<PublishesPanelTitle>;
|
||||
|
||||
export const legacyUnlinkActionIsCompatible = (
|
||||
api: unknown | null
|
||||
): api is LegacyUnlinkPanelFromLibraryActionApi =>
|
||||
Boolean(apiCanAccessViewMode(api) && apiHasLegacyLibraryTransforms(api));
|
||||
|
||||
export class LegacyUnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
|
||||
public readonly id = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
|
||||
public order = 15;
|
||||
|
||||
private toastsService;
|
||||
|
||||
constructor() {
|
||||
({
|
||||
notifications: { toasts: this.toastsService },
|
||||
} = pluginServices.getServices());
|
||||
}
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return dashboardUnlinkFromLibraryActionStrings.getDisplayName();
|
||||
}
|
||||
|
||||
public getIconType({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return 'folderExclamation';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) return false;
|
||||
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canUnlinkFromLibrary());
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
const title = getPanelTitle(embeddable);
|
||||
try {
|
||||
await embeddable.unlinkFromLibrary();
|
||||
this.toastsService.addSuccess({
|
||||
title: dashboardUnlinkFromLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
|
||||
'data-test-subj': 'unlinkPanelSuccess',
|
||||
});
|
||||
} catch (e) {
|
||||
this.toastsService.addDanger({
|
||||
title: dashboardUnlinkFromLibraryActionStrings.getFailureMessage(title ? `'${title}'` : ''),
|
||||
'data-test-subj': 'unlinkPanelFailure',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@ import {
|
|||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { LibraryNotificationPopover } from './library_notification_popover';
|
||||
import { unlinkActionIsCompatible, UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||
import { dashboardLibraryNotificationStrings } from './_dashboard_actions_strings';
|
||||
import { isApiCompatible, UnlinkFromLibraryAction } from './unlink_from_library_action';
|
||||
|
||||
export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION';
|
||||
|
||||
|
@ -29,19 +29,19 @@ export class LibraryNotificationAction implements Action<EmbeddableApiContext> {
|
|||
|
||||
public readonly MenuItem = ({ context }: { context: EmbeddableApiContext }) => {
|
||||
const { embeddable } = context;
|
||||
if (!unlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return <LibraryNotificationPopover unlinkAction={this.unlinkAction} api={embeddable} />;
|
||||
};
|
||||
|
||||
public couldBecomeCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
return unlinkActionIsCompatible(embeddable);
|
||||
return isApiCompatible(embeddable);
|
||||
}
|
||||
|
||||
public subscribeToCompatibilityChanges(
|
||||
{ embeddable }: EmbeddableApiContext,
|
||||
onChange: (isCompatible: boolean, action: LibraryNotificationAction) => void
|
||||
) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) return;
|
||||
if (!isApiCompatible(embeddable)) return;
|
||||
|
||||
/**
|
||||
* TODO: Upgrade this action by subscribing to changes in the existance of a saved object id. Currently,
|
||||
|
@ -55,17 +55,17 @@ export class LibraryNotificationAction implements Action<EmbeddableApiContext> {
|
|||
}
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return dashboardLibraryNotificationStrings.getDisplayName();
|
||||
}
|
||||
|
||||
public getIconType({ embeddable }: EmbeddableApiContext) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return 'folderCheck';
|
||||
}
|
||||
|
||||
public isCompatible = async ({ embeddable }: EmbeddableApiContext) => {
|
||||
if (!unlinkActionIsCompatible(embeddable)) return false;
|
||||
if (!isApiCompatible(embeddable)) return false;
|
||||
return getInheritedViewMode(embeddable) === 'edit' && embeddable.canUnlinkFromLibrary();
|
||||
};
|
||||
|
||||
|
|
|
@ -15,24 +15,26 @@ import React from 'react';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { LibraryNotificationPopover } from './library_notification_popover';
|
||||
import {
|
||||
UnlinkFromLibraryAction,
|
||||
UnlinkPanelFromLibraryActionApi,
|
||||
} from './unlink_from_library_action';
|
||||
LegacyUnlinkFromLibraryAction,
|
||||
LegacyUnlinkPanelFromLibraryActionApi,
|
||||
} from './legacy_unlink_from_library_action';
|
||||
|
||||
const mockUnlinkFromLibraryAction = {
|
||||
execute: jest.fn(),
|
||||
isCompatible: jest.fn().mockResolvedValue(true),
|
||||
getDisplayName: jest.fn().mockReturnValue('Test Unlink'),
|
||||
} as unknown as UnlinkFromLibraryAction;
|
||||
} as unknown as LegacyUnlinkFromLibraryAction;
|
||||
|
||||
describe('library notification popover', () => {
|
||||
let api: UnlinkPanelFromLibraryActionApi;
|
||||
let api: LegacyUnlinkPanelFromLibraryActionApi;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = {
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
canUnlinkFromLibrary: jest.fn().mockResolvedValue(true),
|
||||
unlinkFromLibrary: jest.fn(),
|
||||
canLinkToLibrary: jest.fn().mockResolvedValue(true),
|
||||
linkToLibrary: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -18,15 +18,19 @@ import {
|
|||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
LegacyUnlinkFromLibraryAction,
|
||||
LegacyUnlinkPanelFromLibraryActionApi,
|
||||
} from './legacy_unlink_from_library_action';
|
||||
import { dashboardLibraryNotificationStrings } from './_dashboard_actions_strings';
|
||||
import {
|
||||
UnlinkFromLibraryAction,
|
||||
UnlinkPanelFromLibraryActionApi,
|
||||
} from './unlink_from_library_action';
|
||||
import { dashboardLibraryNotificationStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export interface LibraryNotificationProps {
|
||||
api: UnlinkPanelFromLibraryActionApi;
|
||||
unlinkAction: UnlinkFromLibraryAction;
|
||||
api: UnlinkPanelFromLibraryActionApi | LegacyUnlinkPanelFromLibraryActionApi;
|
||||
unlinkAction: UnlinkFromLibraryAction | LegacyUnlinkFromLibraryAction;
|
||||
}
|
||||
|
||||
export function LibraryNotificationPopover({ unlinkAction, api }: LibraryNotificationProps) {
|
||||
|
|
|
@ -6,30 +6,46 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { apiCanUnlinkFromLibrary, CanUnlinkFromLibrary } from '@kbn/presentation-library';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import {
|
||||
apiCanAccessViewMode,
|
||||
apiHasLibraryTransforms,
|
||||
CanAccessViewMode,
|
||||
EmbeddableApiContext,
|
||||
getInheritedViewMode,
|
||||
getPanelTitle,
|
||||
PublishesPanelTitle,
|
||||
HasLibraryTransforms,
|
||||
HasParentApi,
|
||||
apiHasParentApi,
|
||||
HasUniqueId,
|
||||
apiHasUniqueId,
|
||||
HasType,
|
||||
apiHasType,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
|
||||
|
||||
export type UnlinkPanelFromLibraryActionApi = CanAccessViewMode &
|
||||
CanUnlinkFromLibrary &
|
||||
HasLibraryTransforms &
|
||||
HasType &
|
||||
HasUniqueId &
|
||||
HasParentApi<Pick<PresentationContainer, 'replacePanel'>> &
|
||||
Partial<PublishesPanelTitle>;
|
||||
|
||||
export const unlinkActionIsCompatible = (
|
||||
api: unknown | null
|
||||
): api is UnlinkPanelFromLibraryActionApi =>
|
||||
Boolean(apiCanAccessViewMode(api) && apiCanUnlinkFromLibrary(api));
|
||||
export const isApiCompatible = (api: unknown | null): api is UnlinkPanelFromLibraryActionApi =>
|
||||
Boolean(
|
||||
apiCanAccessViewMode(api) &&
|
||||
apiHasLibraryTransforms(api) &&
|
||||
apiHasUniqueId(api) &&
|
||||
apiHasType(api) &&
|
||||
apiHasParentApi(api) &&
|
||||
typeof (api.parentApi as PresentationContainer)?.replacePanel === 'function'
|
||||
);
|
||||
|
||||
export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_UNLINK_FROM_LIBRARY;
|
||||
|
@ -45,25 +61,28 @@ export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
|
|||
}
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return dashboardUnlinkFromLibraryActionStrings.getDisplayName();
|
||||
}
|
||||
|
||||
public getIconType({ embeddable }: EmbeddableApiContext) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
return 'folderExclamation';
|
||||
}
|
||||
|
||||
public async isCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) return false;
|
||||
if (!isApiCompatible(embeddable)) return false;
|
||||
return getInheritedViewMode(embeddable) === 'edit' && (await embeddable.canUnlinkFromLibrary());
|
||||
}
|
||||
|
||||
public async execute({ embeddable }: EmbeddableApiContext) {
|
||||
if (!unlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
const title = getPanelTitle(embeddable);
|
||||
try {
|
||||
await embeddable.unlinkFromLibrary();
|
||||
await embeddable.parentApi.replacePanel(embeddable.uuid, {
|
||||
panelType: embeddable.type,
|
||||
initialState: { ...embeddable.savedObjectAttributesToState(), title },
|
||||
});
|
||||
this.toastsService.addSuccess({
|
||||
title: dashboardUnlinkFromLibraryActionStrings.getSuccessMessage(title ? `'${title}'` : ''),
|
||||
'data-test-subj': 'unlinkPanelSuccess',
|
||||
|
|
|
@ -69,7 +69,6 @@
|
|||
"@kbn/react-kibana-mount",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/logging",
|
||||
"@kbn/presentation-library",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/presentation-panel-plugin",
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import { ErrorLike } from '@kbn/expressions-plugin/common';
|
||||
import { CanLinkToLibrary, CanUnlinkFromLibrary } from '@kbn/presentation-library';
|
||||
import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types';
|
||||
import {
|
||||
HasEditCapabilities,
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
PublishesWritablePanelTitle,
|
||||
PublishesPhaseEvents,
|
||||
PublishesSavedObjectId,
|
||||
HasLegacyLibraryTransforms,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EmbeddableInput } from '../../../common/types';
|
||||
|
@ -53,7 +53,7 @@ export type LegacyEmbeddableAPI = HasType &
|
|||
PublishesDisabledActionIds &
|
||||
PublishesWritablePanelTitle &
|
||||
PublishesWritablePanelDescription &
|
||||
Partial<CanLinkToLibrary & CanUnlinkFromLibrary> &
|
||||
Partial<HasLegacyLibraryTransforms> &
|
||||
HasParentApi<DefaultPresentationPanelApi['parentApi']> &
|
||||
EmbeddableHasTimeRange &
|
||||
PublishesSavedObjectId;
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
"@kbn/presentation-panel-plugin",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/presentation-library",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardAddPanel.closeAddPanel();
|
||||
|
||||
const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap');
|
||||
await panelActions.unlinkFromLibary(originalPanel);
|
||||
await panelActions.legacyUnlinkFromLibary(originalPanel);
|
||||
await testSubjects.existOrFail('unlinkPanelSuccess');
|
||||
|
||||
const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap');
|
||||
|
@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('save visualize panel to embeddable library', async () => {
|
||||
const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap');
|
||||
await panelActions.saveToLibrary('Rendering Test: heatmap - copy', originalPanel);
|
||||
await panelActions.legacySaveToLibrary('Rendering Test: heatmap - copy', originalPanel);
|
||||
await testSubjects.existOrFail('addPanelToLibrarySuccess');
|
||||
|
||||
const updatedPanel = await testSubjects.find(
|
||||
|
|
|
@ -98,7 +98,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboard.clickUnsavedChangesContinueEditing(DASHBOARD_NAME);
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.saveToLibrary('Some more links');
|
||||
await dashboardPanelActions.legacySaveToLibrary('Some more links');
|
||||
await testSubjects.existOrFail('addPanelToLibrarySuccess');
|
||||
});
|
||||
|
||||
|
|
|
@ -20,7 +20,9 @@ const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_P
|
|||
const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon';
|
||||
const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector';
|
||||
const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard';
|
||||
const LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-legacySaveToLibrary';
|
||||
const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary';
|
||||
const LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-legacyUnlinkFromLibrary';
|
||||
const UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-unlinkFromLibrary';
|
||||
const CONVERT_TO_LENS_TEST_SUBJ = 'embeddablePanelAction-ACTION_EDIT_IN_LENS';
|
||||
|
||||
|
@ -257,6 +259,19 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
await this.testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async legacyUnlinkFromLibary(parent?: WebElementWrapper) {
|
||||
this.log.debug('legacyUnlinkFromLibrary');
|
||||
await this.openContextMenu(parent);
|
||||
const exists = await this.testSubjects.exists(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ);
|
||||
if (!exists) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
await this.testSubjects.click(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ);
|
||||
await this.testSubjects.waitForDeleted(
|
||||
'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'
|
||||
);
|
||||
}
|
||||
|
||||
async unlinkFromLibary(parent?: WebElementWrapper) {
|
||||
this.log.debug('unlinkFromLibrary');
|
||||
await this.openContextMenu(parent);
|
||||
|
@ -270,6 +285,25 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
);
|
||||
}
|
||||
|
||||
async legacySaveToLibrary(newTitle: string, parent?: WebElementWrapper) {
|
||||
this.log.debug('legacySaveToLibrary');
|
||||
await this.openContextMenu(parent);
|
||||
const exists = await this.testSubjects.exists(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ);
|
||||
if (!exists) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
await this.testSubjects.click(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ);
|
||||
await this.testSubjects.setValue('savedObjectTitle', newTitle, {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
await this.testSubjects.click('confirmSaveSavedObjectButton');
|
||||
await this.retry.try(async () => {
|
||||
await this.testSubjects.existOrFail(
|
||||
'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async saveToLibrary(newTitle: string, parent?: WebElementWrapper) {
|
||||
this.log.debug('saveToLibrary');
|
||||
await this.openContextMenu(parent);
|
||||
|
|
|
@ -1226,8 +1226,6 @@
|
|||
"@kbn/preboot-example-plugin/*": ["examples/preboot_example/*"],
|
||||
"@kbn/presentation-containers": ["packages/presentation/presentation_containers"],
|
||||
"@kbn/presentation-containers/*": ["packages/presentation/presentation_containers/*"],
|
||||
"@kbn/presentation-library": ["packages/presentation/presentation_library"],
|
||||
"@kbn/presentation-library/*": ["packages/presentation/presentation_library/*"],
|
||||
"@kbn/presentation-panel-plugin": ["src/plugins/presentation_panel"],
|
||||
"@kbn/presentation-panel-plugin/*": ["src/plugins/presentation_panel/*"],
|
||||
"@kbn/presentation-publishing": ["packages/presentation/presentation_publishing"],
|
||||
|
|
|
@ -77,6 +77,7 @@ import {
|
|||
APP_ID,
|
||||
getEditPath,
|
||||
getFullPath,
|
||||
MAP_EMBEDDABLE_NAME,
|
||||
MAP_SAVED_OBJECT_TYPE,
|
||||
RawValue,
|
||||
RENDER_TIMEOUT,
|
||||
|
@ -85,6 +86,7 @@ import { RenderToolTipContent } from '../classes/tooltips/tooltip_property';
|
|||
import {
|
||||
getCharts,
|
||||
getCoreI18n,
|
||||
getCoreOverlays,
|
||||
getExecutionContextService,
|
||||
getHttp,
|
||||
getSearchService,
|
||||
|
@ -93,6 +95,7 @@ import {
|
|||
getUiActions,
|
||||
} from '../kibana_services';
|
||||
import { LayerDescriptor, MapExtent } from '../../common/descriptor_types';
|
||||
import { extractReferences } from '../../common/migrations/references';
|
||||
import { MapContainer } from '../connected_components/map_container';
|
||||
import { SavedMap } from '../routes/map_page';
|
||||
import { getIndexPatternsFromIds } from '../index_pattern_util';
|
||||
|
@ -101,6 +104,7 @@ import { isUrlDrilldown, toValueClickDataFormat } from '../trigger_actions/trigg
|
|||
import { waitUntilTimeLayersLoad$ } from '../routes/map_page/map_app/wait_until_time_layers_load';
|
||||
import { mapEmbeddablesSingleton } from './map_embeddables_singleton';
|
||||
import { getGeoFieldsLabel } from './get_geo_fields_label';
|
||||
import { checkForDuplicateTitle, getMapClient } from '../content_management';
|
||||
|
||||
import {
|
||||
MapByValueInput,
|
||||
|
@ -665,6 +669,58 @@ export class MapEmbeddable
|
|||
} as ActionExecutionContext;
|
||||
};
|
||||
|
||||
// remove legacy library tranform methods
|
||||
linkToLibrary = undefined;
|
||||
unlinkFromLibrary = undefined;
|
||||
// add implemenation for library transform methods
|
||||
saveStateToSavedObject = async (title: string) => {
|
||||
const { attributes, references } = extractReferences({
|
||||
attributes: this._savedMap.getAttributes(),
|
||||
});
|
||||
|
||||
const {
|
||||
item: { id: savedObjectId },
|
||||
} = await getMapClient().create({
|
||||
data: {
|
||||
...attributes,
|
||||
title,
|
||||
},
|
||||
options: { references },
|
||||
});
|
||||
return {
|
||||
state: {
|
||||
..._.omit(this.getExplicitInput(), 'attributes'),
|
||||
savedObjectId,
|
||||
},
|
||||
savedObjectId,
|
||||
};
|
||||
};
|
||||
savedObjectAttributesToState = () => {
|
||||
return {
|
||||
..._.omit(this.getExplicitInput(), 'savedObjectId'),
|
||||
attributes: this._savedMap.getAttributes(),
|
||||
};
|
||||
};
|
||||
checkForDuplicateTitle = async (
|
||||
newTitle: string,
|
||||
isTitleDuplicateConfirmed: boolean,
|
||||
onTitleDuplicate: () => void
|
||||
) => {
|
||||
return checkForDuplicateTitle(
|
||||
{
|
||||
title: newTitle,
|
||||
copyOnSave: false,
|
||||
lastSavedTitle: '',
|
||||
isTitleDuplicateConfirmed,
|
||||
getDisplayName: () => MAP_EMBEDDABLE_NAME,
|
||||
onTitleDuplicate,
|
||||
},
|
||||
{
|
||||
overlays: getCoreOverlays(),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Timing bug for dashboard with multiple maps with synchronized movement and filter by map extent enabled
|
||||
// When moving map with filterByMapExtent:false, previous map extent filter(s) does not get removed
|
||||
// Cuased by syncDashboardContainerInput applyContainerChangesToState.
|
||||
|
|
|
@ -148,7 +148,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('dashboard panel - save to library', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await testSubjects.click('embeddablePanelAction-saveToLibrary');
|
||||
await testSubjects.click('embeddablePanelAction-legacySaveToLibrary');
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('saveCancelButton');
|
||||
});
|
||||
|
|
|
@ -94,7 +94,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
panels[0]
|
||||
)
|
||||
).to.be(true);
|
||||
await dashboardPanelActions.unlinkFromLibary(panels[0]);
|
||||
await dashboardPanelActions.legacyUnlinkFromLibary(panels[0]);
|
||||
await testSubjects.existOrFail('unlinkPanelSuccess');
|
||||
panels = await testSubjects.findAll('embeddablePanel');
|
||||
expect(panels.length).to.be(1);
|
||||
|
|
|
@ -65,7 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
describe('by reference', () => {
|
||||
it('can add a custom time range to panel', async () => {
|
||||
await dashboardPanelActions.saveToLibrary('My by reference visualization');
|
||||
await dashboardPanelActions.legacySaveToLibrary('My by reference visualization');
|
||||
await dashboardPanelActions.customizePanel();
|
||||
await dashboardCustomizePanel.enableCustomTimeRange();
|
||||
await dashboardCustomizePanel.openDatePickerQuickMenu();
|
||||
|
|
|
@ -85,7 +85,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardPanelActions.customizePanel();
|
||||
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE);
|
||||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await dashboardPanelActions.saveToLibrary(LIBRARY_TITLE_FOR_CUSTOM_TESTS);
|
||||
await dashboardPanelActions.legacySaveToLibrary(LIBRARY_TITLE_FOR_CUSTOM_TESTS);
|
||||
await retry.try(async () => {
|
||||
// need to surround in 'retry' due to delays in HTML updates causing the title read to be behind
|
||||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
|
||||
|
@ -109,7 +109,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardPanelActions.customizePanel();
|
||||
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE);
|
||||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await dashboardPanelActions.unlinkFromLibary();
|
||||
await dashboardPanelActions.legacyUnlinkFromLibary();
|
||||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
|
||||
expect(newPanelTitle).to.equal(CUSTOM_TITLE);
|
||||
});
|
||||
|
@ -118,7 +118,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardPanelActions.customizePanel();
|
||||
await dashboardCustomizePanel.setCustomPanelTitle('');
|
||||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await dashboardPanelActions.saveToLibrary(LIBRARY_TITLE_FOR_EMPTY_TESTS);
|
||||
await dashboardPanelActions.legacySaveToLibrary(LIBRARY_TITLE_FOR_EMPTY_TESTS);
|
||||
await retry.try(async () => {
|
||||
// need to surround in 'retry' due to delays in HTML updates causing the title read to be behind
|
||||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
|
||||
|
@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('unlinking a by reference panel without a custom title will keep the library title', async () => {
|
||||
await dashboardPanelActions.unlinkFromLibary();
|
||||
await dashboardPanelActions.legacyUnlinkFromLibary();
|
||||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
|
||||
expect(newPanelTitle).to.equal(LIBRARY_TITLE_FOR_EMPTY_TESTS);
|
||||
});
|
||||
|
|
|
@ -90,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
|
||||
await dashboardPanelActions.saveToLibrary('My by reference visualization');
|
||||
await dashboardPanelActions.legacySaveToLibrary('My by reference visualization');
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
|
|
@ -242,7 +242,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardAddPanel.closeAddPanel();
|
||||
|
||||
const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis');
|
||||
await panelActions.unlinkFromLibary(originalPanel);
|
||||
await panelActions.legacyUnlinkFromLibary(originalPanel);
|
||||
await testSubjects.existOrFail('unlinkPanelSuccess');
|
||||
|
||||
const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis');
|
||||
|
@ -255,7 +255,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('save lens panel to embeddable library', async () => {
|
||||
const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis');
|
||||
await panelActions.saveToLibrary('lnsPieVis - copy', originalPanel);
|
||||
await panelActions.legacySaveToLibrary('lnsPieVis - copy', originalPanel);
|
||||
|
||||
const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis-copy');
|
||||
const libraryActionExists = await testSubjects.descendantExists(
|
||||
|
|
|
@ -85,7 +85,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await testSubjects.click('visualizesaveAndReturnButton');
|
||||
// save it to library
|
||||
const originalPanel = await testSubjects.find('embeddablePanelHeading-');
|
||||
await panelActions.saveToLibrary('My TSVB to Lens viz 2', originalPanel);
|
||||
await panelActions.legacySaveToLibrary('My TSVB to Lens viz 2', originalPanel);
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
const originalEmbeddableCount = await canvas.getEmbeddableCount();
|
||||
|
|
|
@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// Convert to by-value
|
||||
const byRefPanel = await testSubjects.find('embeddablePanelHeading-' + lensTitle);
|
||||
await dashboardPanelActions.unlinkFromLibary(byRefPanel);
|
||||
await dashboardPanelActions.legacyUnlinkFromLibary(byRefPanel);
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
const byValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle);
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
// save it to library
|
||||
const originalPanel = await testSubjects.find('embeddablePanelHeading-');
|
||||
await panelActions.saveToLibrary('My TSVB to Lens viz 2', originalPanel);
|
||||
await panelActions.legacySaveToLibrary('My TSVB to Lens viz 2', originalPanel);
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
const originalEmbeddableCount = await canvas.getEmbeddableCount();
|
||||
|
|
|
@ -5498,10 +5498,6 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/presentation-library@link:packages/presentation/presentation_library":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/presentation-panel-plugin@link:src/plugins/presentation_panel":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue