{viewMode !== ViewMode.PRINT ? (
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts
index 3eab6c641ee87..2510f2e015dfb 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts
@@ -110,6 +110,7 @@ export const createDashboard = async (
},
lastSavedId: savedObjectId,
managed: savedObjectResult.managed ?? false,
+ fullScreenMode: creationOptions?.fullScreenMode ?? false,
};
const dashboardContainer = new DashboardContainer(
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
index 4805c890f5e95..167ee26055166 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.test.tsx
@@ -178,6 +178,7 @@ test('searchSessionId propagates to children', async () => {
lastSavedInput: sampleInput,
lastSavedId: undefined,
managed: false,
+ fullScreenMode: false,
}
);
container?.setControlGroupApi(mockControlGroupApi);
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
index e508e511d41cb..a6765732c064c 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -314,6 +314,7 @@ export class DashboardContainer
isEmbeddedExternally: false,
lastSavedInput: initialInput,
lastSavedId: undefined,
+ fullScreenMode: false,
managed: false,
},
(id: string) => this.untilEmbeddableLoaded(id)
diff --git a/src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss b/src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
index 6b0141a50861d..0d3f80ae79fec 100644
--- a/src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
+++ b/src/plugins/dashboard/public/dashboard_top_nav/_dashboard_top_nav.scss
@@ -7,7 +7,7 @@
.dashboardTopNav {
width: 100%;
position: sticky;
- z-index: $euiZLevel2;
+ z-index: $euiZLevel3;
top: var(--euiFixedHeadersOffset, 0);
background: $euiPageBackgroundColor;
}
diff --git a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
index 6ca2298272c08..bf2be799dcc1f 100644
--- a/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
+++ b/src/plugins/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx
@@ -123,13 +123,6 @@ export function InternalDashboardTopNav({
dashboardTitleRef.current?.focus();
}, [title, viewMode]);
- /**
- * Manage chrome visibility when dashboard is embedded.
- */
- useEffect(() => {
- if (!embedSettings) coreServices.chrome.setIsVisible(viewMode !== 'print');
- }, [embedSettings, viewMode]);
-
/**
* populate recently accessed, and set is chrome visible.
*/
diff --git a/src/plugins/dashboard/public/mocks.tsx b/src/plugins/dashboard/public/mocks.tsx
index 17081084d5dec..2374788e60ea8 100644
--- a/src/plugins/dashboard/public/mocks.tsx
+++ b/src/plugins/dashboard/public/mocks.tsx
@@ -99,6 +99,7 @@ export function buildMockDashboard({
lastSavedInput: initialInput,
lastSavedId: savedObjectId,
managed: false,
+ fullScreenMode: false,
}
);
dashboardContainer?.setControlGroupApi(mockControlGroupApi);
diff --git a/src/plugins/discover/public/embeddable/actions/view_saved_search_action.ts b/src/plugins/discover/public/embeddable/actions/view_saved_search_action.ts
index 4d77e9dbd4400..d1092a28d9f55 100644
--- a/src/plugins/discover/public/embeddable/actions/view_saved_search_action.ts
+++ b/src/plugins/discover/public/embeddable/actions/view_saved_search_action.ts
@@ -20,6 +20,7 @@ export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH';
export class ViewSavedSearchAction implements Action
{
public id = ACTION_VIEW_SAVED_SEARCH;
public readonly type = ACTION_VIEW_SAVED_SEARCH;
+ public readonly order = 20; // Same order as ACTION_OPEN_IN_DISCOVER
constructor(
private readonly application: ApplicationStart,
@@ -43,7 +44,7 @@ export class ViewSavedSearchAction implements Action {
}
getIconType(): string | undefined {
- return 'inspect';
+ return 'discoverApp';
}
async isCompatible({ embeddable }: EmbeddableApiContext) {
diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.ts
index d866e4d05b4d7..47140911b0980 100644
--- a/src/plugins/discover/public/embeddable/utils/update_search_source.ts
+++ b/src/plugins/discover/public/embeddable/utils/update_search_source.ts
@@ -52,6 +52,7 @@ export const updateSearchSource = (
) => {
const { sortDir } = defaults;
searchSource.setField('size', sampleSize);
+ searchSource.setField('highlightAll', true);
searchSource.setField(
'sort',
getSortForSearchSource({
diff --git a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts
index dab0968af0056..daab774d7f35d 100644
--- a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts
@@ -245,6 +245,8 @@ export const legacyEmbeddableToApi = (
return !isInputControl && !isMarkdown && !isImage && !isLinks;
};
+ const hasLockedHoverActions$ = new BehaviorSubject(false);
+
return {
api: {
parentApi: parentApi as LegacyEmbeddableAPI['parentApi'],
@@ -270,6 +272,9 @@ export const legacyEmbeddableToApi = (
disabledActionIds,
setDisabledActionIds: (ids) => disabledActionIds.next(ids),
+ hasLockedHoverActions$,
+ lockHoverActions: (lock: boolean) => hasLockedHoverActions$.next(lock),
+
panelTitle,
setPanelTitle,
defaultPanelTitle,
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index 89df109be5ef1..9fc3598bcd5ad 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -148,6 +148,8 @@ export abstract class Embeddable<
canUnlinkFromLibrary: this.canUnlinkFromLibrary,
isCompatibleWithUnifiedSearch: this.isCompatibleWithUnifiedSearch,
savedObjectId: this.savedObjectId,
+ hasLockedHoverActions$: this.hasLockedHoverActions$,
+ lockHoverActions: this.lockHoverActions,
} = api);
setTimeout(() => {
@@ -191,6 +193,8 @@ export abstract class Embeddable<
public canUnlinkFromLibrary: LegacyEmbeddableAPI['canUnlinkFromLibrary'];
public isCompatibleWithUnifiedSearch: LegacyEmbeddableAPI['isCompatibleWithUnifiedSearch'];
public savedObjectId: LegacyEmbeddableAPI['savedObjectId'];
+ public hasLockedHoverActions$: LegacyEmbeddableAPI['hasLockedHoverActions$'];
+ public lockHoverActions: LegacyEmbeddableAPI['lockHoverActions'];
public async getEditHref(): Promise {
return this.getOutput().editUrl ?? undefined;
diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
index 779c1a235bc82..57cf7eec6eb95 100644
--- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts
@@ -27,6 +27,7 @@ import {
PublishesSavedObjectId,
HasLegacyLibraryTransforms,
EmbeddableAppContext,
+ CanLockHoverActions,
} from '@kbn/presentation-publishing';
import { Observable } from 'rxjs';
import { EmbeddableInput } from '../../../common/types';
@@ -58,7 +59,8 @@ export type LegacyEmbeddableAPI = HasType &
Partial &
HasParentApi &
EmbeddableHasTimeRange &
- PublishesSavedObjectId;
+ PublishesSavedObjectId &
+ CanLockHoverActions;
export interface EmbeddableOutput {
// Whether the embeddable is actively loading.
diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx
index 3722647526c79..63433d1d1319b 100644
--- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx
+++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx
@@ -194,6 +194,8 @@ describe('react embeddable renderer', () => {
resetUnsavedChanges: expect.any(Function),
snapshotRuntimeState: expect.any(Function),
phase$: expect.any(Object),
+ hasLockedHoverActions$: expect.any(Object),
+ lockHoverActions: expect.any(Function),
})
);
});
diff --git a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx
index 0f9ae361bbf93..c3dc06e198cd8 100644
--- a/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx
+++ b/src/plugins/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx
@@ -122,11 +122,16 @@ export const ReactEmbeddableRenderer = <
const setApi = (
apiRegistration: SetReactEmbeddableApiRegistration
) => {
+ const hasLockedHoverActions$ = new BehaviorSubject(false);
return {
...apiRegistration,
uuid,
phase$,
parentApi,
+ hasLockedHoverActions$,
+ lockHoverActions: (lock: boolean) => {
+ hasLockedHoverActions$.next(lock);
+ },
type: factory.type,
} as unknown as Api;
};
diff --git a/src/plugins/embeddable/public/react_embeddable_system/types.ts b/src/plugins/embeddable/public/react_embeddable_system/types.ts
index 1ab43d4bb1b7d..4ba8653310ff0 100644
--- a/src/plugins/embeddable/public/react_embeddable_system/types.ts
+++ b/src/plugins/embeddable/public/react_embeddable_system/types.ts
@@ -14,6 +14,7 @@ import {
} from '@kbn/presentation-containers';
import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types';
import {
+ CanLockHoverActions,
HasType,
PublishesPhaseEvents,
PublishesUnsavedChanges,
@@ -48,7 +49,7 @@ export type SetReactEmbeddableApiRegistration<
SerializedState,
RuntimeState
>
-> = Omit;
+> = Omit;
/**
* Defines the subset of the default embeddable API that the `buildApi` method uses, which allows implementors
diff --git a/src/plugins/links/public/embeddable/links_embeddable.tsx b/src/plugins/links/public/embeddable/links_embeddable.tsx
index 177f2f1c82118..685f0a6c46a3b 100644
--- a/src/plugins/links/public/embeddable/links_embeddable.tsx
+++ b/src/plugins/links/public/embeddable/links_embeddable.tsx
@@ -248,6 +248,7 @@ export const getLinksEmbeddableFactory = () => {
data-shared-item
data-rendering-count={1}
data-test-subj="links--component"
+ borderRadius="none"
>
{
export class InspectPanelAction implements Action {
public readonly type = ACTION_INSPECT_PANEL;
public readonly id = ACTION_INSPECT_PANEL;
- public order = 20;
+ public order = 19; // right after Explore in Discover which is 20
constructor() {}
diff --git a/src/plugins/presentation_panel/public/panel_actions/remove_panel_action/remove_panel_action.ts b/src/plugins/presentation_panel/public/panel_actions/remove_panel_action/remove_panel_action.ts
index b065ed5cedf59..335fda267a800 100644
--- a/src/plugins/presentation_panel/public/panel_actions/remove_panel_action/remove_panel_action.ts
+++ b/src/plugins/presentation_panel/public/panel_actions/remove_panel_action/remove_panel_action.ts
@@ -33,14 +33,8 @@ const isApiCompatible = (api: unknown | null): api is RemovePanelActionApi =>
export class RemovePanelAction implements Action {
public readonly type = ACTION_REMOVE_PANEL;
public readonly id = ACTION_REMOVE_PANEL;
- public order = 1;
-
- public grouping = [
- {
- id: 'delete_panel_action',
- order: 1,
- },
- ];
+ public order = 0;
+ public grouping = [{ id: 'remove_panel_group', order: 1 }];
constructor() {}
diff --git a/src/plugins/presentation_panel/public/panel_component/_presentation_panel.scss b/src/plugins/presentation_panel/public/panel_component/_presentation_panel.scss
index 434cca42e7c9f..5094cf6b02ba3 100644
--- a/src/plugins/presentation_panel/public/panel_component/_presentation_panel.scss
+++ b/src/plugins/presentation_panel/public/panel_component/_presentation_panel.scss
@@ -6,6 +6,8 @@
height: 100%;
min-height: $euiSizeL + 2px; // + 2px to account for border
position: relative;
+ border: none;
+ outline: $euiBorderThin;
&-isLoading {
// completely center the loading indicator
@@ -44,6 +46,13 @@
display: flex;
// ensure menu button is on the right even if the title doesn't exist
justify-content: flex-end;
+ height: $euiSizeL;
+}
+
+.embPanel__header + .embPanel__content {
+ border-radius: 0;
+ border-bottom-left-radius: $euiBorderRadius;
+ border-bottom-right-radius: $euiBorderRadius;
}
.embPanel__title {
@@ -112,7 +121,6 @@
&:focus {
background-color: transparentize($euiColorLightestShade, .5);
}
-
}
.embPanel__optionsMenuPopover-loading {
@@ -129,43 +137,20 @@
font-size: $euiSizeL;
}
-.embPanel .embPanel__optionsMenuButton {
- opacity: 0; /* 1 */
-
- &:focus {
- opacity: 1; /* 2 */
- }
-}
-
-.embPanel:hover {
- .embPanel__optionsMenuButton {
- opacity: 1;
- }
-}
-
// EDITING MODE
.embPanel--editing {
transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
+ outline: 1px dashed $euiColorMediumShade;
.embPanel--dragHandle {
transition: background-color $euiAnimSpeedFast $euiAnimSlightResistance;
- &:hover {
+ .embPanel--dragHandle:hover {
background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7));
cursor: move;
}
}
-
- .embPanel__content {
- border-radius: 0;
- border-bottom-left-radius: $euiBorderRadius;
- border-bottom-right-radius: $euiBorderRadius;
- }
-
- .embPanel__optionsMenuButton {
- opacity: 1; /* 3 */
- }
}
// LOADING and ERRORS
@@ -184,3 +169,57 @@
padding-left: $euiSizeS;
z-index: $euiZLevel1;
}
+
+.embPanel__hoverActionsAnchor {
+ position: relative;
+ height: 100%;
+
+ .embPanel__hoverActionsWrapper {
+ height: $euiSizeXL;
+ position: absolute;
+ top: 0;
+ display: flex;
+ justify-content: space-between;
+ padding: 0 $euiSize;
+ flex-wrap: nowrap;
+ min-width: 100%;
+ z-index: -1;
+ pointer-events: none; // Prevent hover actions wrapper from blocking interactions with other panels
+ }
+
+ .embPanel__hoverActions {
+ opacity: 0;
+ padding: calc($euiSizeXS - 1px);
+ display: flex;
+ flex-wrap: nowrap;
+ border: $euiBorderThin;
+
+ background-color: $euiColorEmptyShade;
+ height: $euiSizeXL;
+
+ pointer-events: all; // Re-enable pointer-events for hover actions
+ }
+
+ .embPanel--dragHandle {
+ cursor: move;
+
+ img {
+ pointer-events: all !important;
+ }
+ }
+
+ .embPanel__descriptionTooltipAnchor {
+ padding: $euiSizeXS;
+ }
+
+ &:hover .embPanel__hoverActionsWrapper,
+ &:focus-within .embPanel__hoverActionsWrapper,
+ .embPanel__hoverActionsWrapper--lockHoverActions {
+ z-index: $euiZLevel9;
+ top: -$euiSizeXL;
+
+ .embPanel__hoverActions {
+ opacity: 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx
deleted file mode 100644
index 2376c4b43edbb..0000000000000
--- a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx
+++ /dev/null
@@ -1,177 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
- * License v3.0 only", or the "Server Side Public License, v 1".
- */
-
-import { i18n } from '@kbn/i18n';
-import classNames from 'classnames';
-import React, { useEffect, useMemo, useState } from 'react';
-
-import {
- EuiButtonIcon,
- EuiContextMenu,
- EuiContextMenuItem,
- EuiContextMenuPanel,
- EuiContextMenuPanelDescriptor,
- EuiPopover,
- EuiSkeletonText,
-} from '@elastic/eui';
-import { Action, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
-
-import {
- getViewModeSubject,
- useBatchedOptionalPublishingSubjects,
-} from '@kbn/presentation-publishing';
-import { uiActions } from '../../kibana_services';
-import { contextMenuTrigger, CONTEXT_MENU_TRIGGER } from '../../panel_actions';
-import { getContextMenuAriaLabel } from '../presentation_panel_strings';
-import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
-
-export const PresentationPanelContextMenu = ({
- api,
- index,
- getActions,
- actionPredicate,
-}: {
- index?: number;
- api: DefaultPresentationPanelApi;
- getActions: PresentationPanelInternalProps['getActions'];
- actionPredicate?: (actionId: string) => boolean;
-}) => {
- const [menuPanelsLoading, setMenuPanelsLoading] = useState(false);
- const [contextMenuActions, setContextMenuActions] = useState>>([]);
- const [isContextMenuOpen, setIsContextMenuOpen] = useState(undefined);
- const [contextMenuPanels, setContextMenuPanels] = useState([]);
-
- const [title, parentViewMode] = useBatchedOptionalPublishingSubjects(
- api.panelTitle,
-
- /**
- * View mode changes often have the biggest influence over which actions will be compatible,
- * so we build and update all actions when the view mode changes. This is temporary, as these
- * actions should eventually all be Frequent Compatibility Change Actions which can track their
- * own dependencies.
- */
- getViewModeSubject(api)
- );
-
- useEffect(() => {
- /**
- * isContextMenuOpen starts as undefined which allows this use effect to run on mount. This
- * is required so that showNotification is calculated on mount.
- */
- if (isContextMenuOpen === false || !api) return;
-
- setMenuPanelsLoading(true);
- let canceled = false;
- (async () => {
- /**
- * Build and update all actions
- */
- let compatibleActions: Array> = await (async () => {
- if (getActions) return await getActions(CONTEXT_MENU_TRIGGER, { embeddable: api });
- return (
- (await uiActions.getTriggerCompatibleActions(CONTEXT_MENU_TRIGGER, {
- embeddable: api,
- })) ?? []
- );
- })();
- if (canceled) return;
-
- const disabledActions = api.disabledActionIds?.value;
- if (disabledActions) {
- compatibleActions = compatibleActions.filter(
- (action) => disabledActions.indexOf(action.id) === -1
- );
- }
-
- if (actionPredicate) {
- compatibleActions = compatibleActions.filter(({ id }) => actionPredicate(id));
- }
-
- compatibleActions.sort(
- ({ order: orderA }, { order: orderB }) => (orderB || 0) - (orderA || 0)
- );
-
- /**
- * Build context menu panel from actions
- */
- const panels = await buildContextMenuForActions({
- actions: compatibleActions.map((action) => ({
- action,
- context: { embeddable: api },
- trigger: contextMenuTrigger,
- })),
- closeMenu: () => setIsContextMenuOpen(false),
- });
- if (canceled) return;
-
- setMenuPanelsLoading(false);
- setContextMenuActions(compatibleActions);
- setContextMenuPanels(panels);
- })();
- return () => {
- canceled = true;
- };
- }, [actionPredicate, api, getActions, isContextMenuOpen, parentViewMode]);
-
- const showNotification = useMemo(
- () => contextMenuActions.some((action) => action.showNotification),
- [contextMenuActions]
- );
-
- const contextMenuClasses = classNames({
- // eslint-disable-next-line @typescript-eslint/naming-convention
- embPanel__optionsMenuPopover: true,
- 'embPanel__optionsMenuPopover-notification': showNotification,
- });
-
- const ContextMenuButton = (
- setIsContextMenuOpen((isOpen) => !isOpen)}
- iconType={'boxesHorizontal'}
- />
- );
-
- return (
- setIsContextMenuOpen(false)}
- data-test-subj={
- isContextMenuOpen ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'
- }
- >
- {menuPanelsLoading ? (
-
-
-
-
-
- ) : (
-
- )}
-
- );
-};
diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx
index 669f15cb2ba6b..0747e4a4f8229 100644
--- a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_header.tsx
@@ -13,7 +13,6 @@ import classNames from 'classnames';
import React from 'react';
import { getAriaLabelForTitle } from '../presentation_panel_strings';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
-import { PresentationPanelContextMenu } from './presentation_panel_context_menu';
import { PresentationPanelTitle } from './presentation_panel_title';
import { usePresentationPanelHeaderActions } from './use_presentation_panel_header_actions';
@@ -24,23 +23,18 @@ export type PresentationPanelHeaderProps;
+} & Pick;
export const PresentationPanelHeader = <
ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi
>({
api,
- index,
viewMode,
headerId,
getActions,
hideTitle,
panelTitle,
panelDescription,
- actionPredicate,
showBadges = true,
showNotifications = true,
}: PresentationPanelHeaderProps) => {
@@ -52,11 +46,9 @@ export const PresentationPanelHeader = <
);
const showPanelBar =
- !hideTitle ||
- panelDescription ||
- viewMode !== 'view' ||
- badgeElements.length > 0 ||
- notificationElements.length > 0;
+ (!hideTitle && panelTitle) || badgeElements.length > 0 || notificationElements.length > 0;
+
+ if (!showPanelBar) return null;
const ariaLabel = getAriaLabelForTitle(showPanelBar ? panelTitle : undefined);
const ariaLabelElement = (
@@ -66,6 +58,7 @@ export const PresentationPanelHeader = <
);
const headerClasses = classNames('embPanel__header', {
+ 'embPanel--dragHandle': viewMode === 'edit',
'embPanel__header--floater': !showPanelBar,
});
@@ -73,19 +66,6 @@ export const PresentationPanelHeader = <
'embPanel--dragHandle': viewMode === 'edit',
});
- const contextMenuElement = (
-
- );
-
- if (!showPanelBar) {
- return (
-
- {contextMenuElement}
- {ariaLabelElement}
-
- );
- }
-
return (
{showNotifications && notificationElements}
- {contextMenuElement}
);
};
diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_hover_actions.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_hover_actions.tsx
new file mode 100644
index 0000000000000..469a1f8c4f6e3
--- /dev/null
+++ b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_hover_actions.tsx
@@ -0,0 +1,563 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { i18n } from '@kbn/i18n';
+import classNames from 'classnames';
+import React, {
+ MouseEventHandler,
+ ReactElement,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+
+import {
+ EuiButtonIcon,
+ EuiContextMenu,
+ EuiContextMenuPanelDescriptor,
+ EuiIcon,
+ EuiIconTip,
+ EuiNotificationBadge,
+ EuiPopover,
+ EuiToolTip,
+ IconType,
+} from '@elastic/eui';
+import { ActionExecutionContext, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
+
+import {
+ apiCanLockHoverActions,
+ EmbeddableApiContext,
+ getViewModeSubject,
+ useBatchedOptionalPublishingSubjects,
+ ViewMode,
+} from '@kbn/presentation-publishing';
+import { Subscription } from 'rxjs';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { css } from '@emotion/react';
+import { ActionWithContext } from '@kbn/ui-actions-plugin/public/context_menu/build_eui_context_menu_panels';
+import { uiActions } from '../../kibana_services';
+import {
+ contextMenuTrigger,
+ CONTEXT_MENU_TRIGGER,
+ panelNotificationTrigger,
+ PANEL_NOTIFICATION_TRIGGER,
+} from '../../panel_actions';
+import { getContextMenuAriaLabel } from '../presentation_panel_strings';
+import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
+import { AnyApiAction } from '../../panel_actions/types';
+
+const QUICK_ACTION_IDS = {
+ edit: [
+ 'editPanel',
+ 'ACTION_CONFIGURE_IN_LENS',
+ 'ACTION_CUSTOMIZE_PANEL',
+ 'ACTION_OPEN_IN_DISCOVER',
+ 'ACTION_VIEW_SAVED_SEARCH',
+ ],
+ view: ['ACTION_OPEN_IN_DISCOVER', 'ACTION_VIEW_SAVED_SEARCH', 'openInspector', 'togglePanel'],
+} as const;
+
+const ALLOWED_NOTIFICATIONS = ['ACTION_FILTERS_NOTIFICATION'] as const;
+
+const ALL_ROUNDED_CORNERS = `border-radius: ${euiThemeVars.euiBorderRadius};
+`;
+const TOP_ROUNDED_CORNERS = `border-top-left-radius: ${euiThemeVars.euiBorderRadius};
+ border-top-right-radius: ${euiThemeVars.euiBorderRadius};
+ border-bottom: 0 !important;
+ `;
+
+const createClickHandler =
+ (action: AnyApiAction, context: ActionExecutionContext) =>
+ (event: React.MouseEvent) => {
+ if (event.currentTarget instanceof HTMLAnchorElement) {
+ // from react-router's
+ if (
+ !event.defaultPrevented && // onClick prevented default
+ event.button === 0 && // ignore everything but left clicks
+ (!event.currentTarget.target || event.currentTarget.target === '_self') && // let browser handle "target=_blank" etc.
+ !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) // ignore clicks with modifier keys
+ ) {
+ event.preventDefault();
+ }
+ }
+ (event.currentTarget as HTMLElement).blur();
+ action.execute(context);
+ };
+
+export const PresentationPanelHoverActions = ({
+ api,
+ index,
+ getActions,
+ actionPredicate,
+ children,
+ className,
+ viewMode,
+ showNotifications = true,
+}: {
+ index?: number;
+ api: DefaultPresentationPanelApi | null;
+ getActions: PresentationPanelInternalProps['getActions'];
+ actionPredicate?: (actionId: string) => boolean;
+ children: ReactElement;
+ className?: string;
+ viewMode?: ViewMode;
+ showNotifications?: boolean;
+}) => {
+ const [quickActions, setQuickActions] = useState([]);
+ const [contextMenuPanels, setContextMenuPanels] = useState([]);
+ const [showNotification, setShowNotification] = useState(false);
+ const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
+ const [notifications, setNotifications] = useState([]);
+ const hoverActionsRef = useRef(null);
+ const anchorRef = useRef(null);
+ const leftHoverActionsRef = useRef(null);
+ const rightHoverActionsRef = useRef(null);
+ const [combineHoverActions, setCombineHoverActions] = useState(false);
+ const [borderStyles, setBorderStyles] = useState(TOP_ROUNDED_CORNERS);
+
+ const updateCombineHoverActions = () => {
+ if (!hoverActionsRef.current || !anchorRef.current) return;
+ const anchorBox = anchorRef.current.getBoundingClientRect();
+ const anchorLeft = anchorBox.left;
+ const anchorTop = anchorBox.top;
+ const anchorWidth = anchorRef.current.offsetWidth;
+ const hoverActionsWidth =
+ (rightHoverActionsRef.current?.offsetWidth ?? 0) +
+ (leftHoverActionsRef.current?.offsetWidth ?? 0) +
+ parseInt(euiThemeVars.euiSize, 10) * 2;
+ const hoverActionsHeight = rightHoverActionsRef.current?.offsetHeight ?? 0;
+
+ // Left align hover actions when they would get cut off by the right edge of the window
+ if (anchorLeft - (hoverActionsWidth - anchorWidth) <= parseInt(euiThemeVars.euiSize, 10)) {
+ hoverActionsRef.current.style.removeProperty('right');
+ hoverActionsRef.current.style.setProperty('left', '0');
+ } else {
+ hoverActionsRef.current.style.removeProperty('left');
+ hoverActionsRef.current.style.setProperty('right', '0');
+ }
+
+ if (anchorRef.current && rightHoverActionsRef.current) {
+ const shouldCombine = anchorWidth < hoverActionsWidth;
+ const willGetCutOff = anchorTop < hoverActionsHeight;
+
+ if (shouldCombine !== combineHoverActions) {
+ setCombineHoverActions(shouldCombine);
+ }
+
+ if (willGetCutOff) {
+ hoverActionsRef.current.style.setProperty('position', 'absolute');
+ hoverActionsRef.current.style.setProperty('top', `-${euiThemeVars.euiSizeS}`);
+ } else if (shouldCombine) {
+ hoverActionsRef.current.style.setProperty('top', `-${euiThemeVars.euiSizeL}`);
+ } else {
+ hoverActionsRef.current.style.removeProperty('position');
+ hoverActionsRef.current.style.removeProperty('top');
+ }
+
+ if (shouldCombine || willGetCutOff) {
+ setBorderStyles(ALL_ROUNDED_CORNERS);
+ } else {
+ setBorderStyles(TOP_ROUNDED_CORNERS);
+ }
+ }
+ };
+
+ const [
+ defaultTitle,
+ title,
+ description,
+ hidePanelTitle,
+ hasLockedHoverActions,
+ parentHideTitle,
+ parentViewMode,
+ ] = useBatchedOptionalPublishingSubjects(
+ api?.defaultPanelTitle,
+ api?.panelTitle,
+ api?.panelDescription,
+ api?.hidePanelTitle,
+ api?.hasLockedHoverActions$,
+ api?.parentApi?.hidePanelTitle,
+ /**
+ * View mode changes often have the biggest influence over which actions will be compatible,
+ * so we build and update all actions when the view mode changes. This is temporary, as these
+ * actions should eventually all be Frequent Compatibility Change Actions which can track their
+ * own dependencies.
+ */
+ getViewModeSubject(api ?? undefined)
+ );
+
+ const hideTitle = hidePanelTitle || parentHideTitle;
+
+ const showDescription = description && (!title || hideTitle);
+
+ const quickActionIds = useMemo(
+ () => QUICK_ACTION_IDS[parentViewMode === 'edit' ? 'edit' : 'view'],
+ [parentViewMode]
+ );
+
+ const onClose = useCallback(() => {
+ setIsContextMenuOpen(false);
+ if (apiCanLockHoverActions(api)) {
+ api?.lockHoverActions(false);
+ }
+ }, [api]);
+
+ useEffect(() => {
+ if (!api) return;
+ let canceled = false;
+
+ const apiContext = { embeddable: api };
+ const subscriptions = new Subscription();
+ const handleActionCompatibilityChange = (
+ type: 'quickActions' | 'notifications',
+ isCompatible: boolean,
+ action: AnyApiAction
+ ) => {
+ if (canceled) return;
+ (type === 'quickActions' ? setQuickActions : setNotifications)((currentActions) => {
+ const newActions = currentActions?.filter((current) => current.id !== action.id);
+ if (isCompatible) return [...newActions, action];
+ return newActions;
+ });
+ };
+
+ (async () => {
+ // subscribe to any frequently changing context menu actions
+ const frequentlyChangingActions = uiActions.getFrequentlyChangingActionsForTrigger(
+ CONTEXT_MENU_TRIGGER,
+ apiContext
+ );
+
+ for (const frequentlyChangingAction of frequentlyChangingActions) {
+ if ((quickActionIds as readonly string[]).includes(frequentlyChangingAction.id)) {
+ subscriptions.add(
+ frequentlyChangingAction.subscribeToCompatibilityChanges(
+ apiContext,
+ (isCompatible, action) =>
+ handleActionCompatibilityChange(
+ 'quickActions',
+ isCompatible,
+ action as AnyApiAction
+ )
+ )
+ );
+ }
+ }
+
+ // subscribe to any frequently changing notification actions
+ const frequentlyChangingNotifications = uiActions.getFrequentlyChangingActionsForTrigger(
+ PANEL_NOTIFICATION_TRIGGER,
+ apiContext
+ );
+
+ for (const frequentlyChangingNotification of frequentlyChangingNotifications) {
+ if (
+ (ALLOWED_NOTIFICATIONS as readonly string[]).includes(frequentlyChangingNotification.id)
+ ) {
+ subscriptions.add(
+ frequentlyChangingNotification.subscribeToCompatibilityChanges(
+ apiContext,
+ (isCompatible, action) =>
+ handleActionCompatibilityChange(
+ 'notifications',
+ isCompatible,
+ action as AnyApiAction
+ )
+ )
+ );
+ }
+ }
+ })();
+
+ return () => {
+ canceled = true;
+ subscriptions.unsubscribe();
+ };
+ }, [api, quickActionIds]);
+
+ useEffect(() => {
+ if (!api) return;
+
+ let canceled = false;
+ const apiContext = { embeddable: api };
+
+ (async () => {
+ let compatibleActions = (await (async () => {
+ if (getActions) return await getActions(CONTEXT_MENU_TRIGGER, apiContext);
+ return (
+ (await uiActions.getTriggerCompatibleActions(CONTEXT_MENU_TRIGGER, {
+ embeddable: api,
+ })) ?? []
+ );
+ })()) as AnyApiAction[];
+ if (canceled) return;
+
+ const disabledActions = api.disabledActionIds?.value;
+ if (disabledActions) {
+ compatibleActions = compatibleActions.filter(
+ (action) => disabledActions.indexOf(action.id) === -1
+ );
+ }
+
+ if (actionPredicate) {
+ compatibleActions = compatibleActions.filter(({ id }) => actionPredicate(id));
+ }
+
+ compatibleActions.sort(
+ ({ order: orderA }, { order: orderB }) => (orderB || 0) - (orderA || 0)
+ );
+
+ const contextMenuActions = compatibleActions.filter(
+ ({ id }) => !(quickActionIds as readonly string[]).includes(id)
+ );
+
+ const menuPanels = await buildContextMenuForActions({
+ actions: contextMenuActions.map((action) => ({
+ action,
+ context: apiContext,
+ trigger: contextMenuTrigger,
+ })) as ActionWithContext[],
+ closeMenu: onClose,
+ });
+ setContextMenuPanels(menuPanels);
+ setShowNotification(contextMenuActions.some((action) => action.showNotification));
+ setQuickActions(
+ compatibleActions.filter(({ id }) => (quickActionIds as readonly string[]).includes(id))
+ );
+ })();
+
+ return () => {
+ canceled = true;
+ };
+ }, [
+ actionPredicate,
+ api,
+ getActions,
+ isContextMenuOpen,
+ onClose,
+ parentViewMode,
+ quickActionIds,
+ ]);
+
+ const quickActionElements = useMemo(() => {
+ if (!api || quickActions.length < 1) return [];
+
+ const apiContext = { embeddable: api, trigger: contextMenuTrigger };
+
+ return quickActions
+ .sort(({ order: orderA }, { order: orderB }) => {
+ const orderComparison = (orderB || 0) - (orderA || 0);
+ return orderComparison;
+ })
+ .map((action) => {
+ const name = action.getDisplayName(apiContext);
+ const iconType = action.getIconType(apiContext) as IconType;
+ const id = action.id;
+
+ return {
+ iconType,
+ 'data-test-subj': `embeddablePanelAction-${action.id}`,
+ onClick: createClickHandler(action, apiContext),
+ name,
+ id,
+ };
+ });
+ }, [api, quickActions]);
+
+ const notificationElements = useMemo(() => {
+ if (!showNotifications || !api) return [];
+ return notifications?.map((notification) => {
+ let notificationComponent = notification.MenuItem ? (
+ React.createElement(notification.MenuItem, {
+ key: notification.id,
+ context: {
+ embeddable: api,
+ trigger: panelNotificationTrigger,
+ },
+ })
+ ) : (
+
+ notification.execute({ embeddable: api, trigger: panelNotificationTrigger })
+ }
+ >
+ {notification.getDisplayName({ embeddable: api, trigger: panelNotificationTrigger })}
+
+ );
+
+ if (notification.getDisplayNameTooltip) {
+ const tooltip = notification.getDisplayNameTooltip({
+ embeddable: api,
+ trigger: panelNotificationTrigger,
+ });
+
+ if (tooltip) {
+ notificationComponent = (
+
+ {notificationComponent}
+
+ );
+ }
+ }
+
+ return notificationComponent;
+ });
+ }, [api, notifications, showNotifications]);
+
+ const contextMenuClasses = classNames({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ embPanel__optionsMenuPopover: true,
+ 'embPanel__optionsMenuPopover-notification': showNotification,
+ });
+
+ const ContextMenuButton = (
+ {
+ setIsContextMenuOpen(!isContextMenuOpen);
+ if (apiCanLockHoverActions(api)) {
+ api?.lockHoverActions(!hasLockedHoverActions);
+ }
+ }}
+ iconType="boxesVertical"
+ />
+ );
+
+ const dragHandle = (
+
+ );
+
+ return (
+
+ {children}
+ {api ? (
+
+ {viewMode === 'edit' && !combineHoverActions ? (
+
+ {dragHandle}
+
+ ) : (
+
// necessary for the right hover actions to align correctly when left hover actions are not present
+ )}
+
+ {viewMode === 'edit' && combineHoverActions && dragHandle}
+ {showNotifications && notificationElements}
+ {showDescription && (
+
+ )}
+ {quickActionElements.map(
+ ({ iconType, 'data-test-subj': dataTestSubj, onClick, name }, i) => (
+
+
+
+ )
+ )}
+ {contextMenuPanels.length ? (
+
+
+
+ ) : null}
+
+
+ ) : null}
+
+ );
+};
diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx
index 4189250e394d3..ef819c427c765 100644
--- a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_title.tsx
@@ -131,8 +131,8 @@ export const PresentationPanelTitle = ({
}, [api, onClick]);
const describedPanelTitleElement = useMemo(() => {
+ if (hideTitle) return null;
if (!panelDescription) {
- if (hideTitle) return null;
return (
{panelTitleElement}
diff --git a/src/plugins/presentation_panel/public/panel_component/panel_header/use_presentation_panel_header_actions.tsx b/src/plugins/presentation_panel/public/panel_component/panel_header/use_presentation_panel_header_actions.tsx
index 570fdfd91e229..b48a4eca7ae1f 100644
--- a/src/plugins/presentation_panel/public/panel_component/panel_header/use_presentation_panel_header_actions.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/panel_header/use_presentation_panel_header_actions.tsx
@@ -22,6 +22,8 @@ import {
import { AnyApiAction } from '../../panel_actions/types';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
+const disabledNotifications = ['ACTION_FILTERS_NOTIFICATION'];
+
export const usePresentationPanelHeaderActions = <
ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi
>(
@@ -47,10 +49,8 @@ export const usePresentationPanelHeaderActions = <
embeddable: api,
})) as AnyApiAction[]) ?? [];
- const disabledActions = api.disabledActionIds?.value;
- if (disabledActions) {
- nextActions = nextActions.filter((badge) => disabledActions.indexOf(badge.id) === -1);
- }
+ const disabledActions = (api.disabledActionIds?.value ?? []).concat(disabledNotifications);
+ nextActions = nextActions.filter((badge) => disabledActions.indexOf(badge.id) === -1);
return nextActions;
};
@@ -85,8 +85,8 @@ export const usePresentationPanelHeaderActions = <
);
for (const badge of frequentlyChangingBadges) {
subscriptions.add(
- badge.subscribeToCompatibilityChanges(apiContext, (isComptaible, action) =>
- handleActionCompatibilityChange('badge', isComptaible, action as AnyApiAction)
+ badge.subscribeToCompatibilityChanges(apiContext, (isCompatible, action) =>
+ handleActionCompatibilityChange('badge', isCompatible, action as AnyApiAction)
)
);
}
@@ -97,11 +97,12 @@ export const usePresentationPanelHeaderActions = <
apiContext
);
for (const notification of frequentlyChangingNotifications) {
- subscriptions.add(
- notification.subscribeToCompatibilityChanges(apiContext, (isComptaible, action) =>
- handleActionCompatibilityChange('notification', isComptaible, action as AnyApiAction)
- )
- );
+ if (!disabledNotifications.includes(notification.id))
+ subscriptions.add(
+ notification.subscribeToCompatibilityChanges(apiContext, (isCompatible, action) =>
+ handleActionCompatibilityChange('notification', isCompatible, action as AnyApiAction)
+ )
+ );
}
})();
diff --git a/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx b/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx
index 550c76a14aee1..fa86060859098 100644
--- a/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.test.tsx
@@ -37,7 +37,7 @@ describe('Presentation panel', () => {
);
await waitFor(() => {
- expect(screen.getByTestId('embeddablePanelToggleMenuIcon')).toBeInTheDocument();
+ expect(screen.getByTestId('embeddablePanel')).toBeInTheDocument();
});
};
@@ -223,12 +223,10 @@ describe('Presentation panel', () => {
viewMode: new BehaviorSubject('view'),
};
await renderPresentationPanel({ api });
- const header = await screen.findByTestId('embeddablePanelHeading');
- const titleComponent = screen.queryByTestId('dashboardPanelTitle');
- expect(header).not.toContainElement(titleComponent);
+ expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
});
- it('renders a placeholder title when in edit mode and the provided title is blank', async () => {
+ it('does not render a title when in edit mode and the provided title is blank', async () => {
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject(''),
@@ -236,9 +234,7 @@ describe('Presentation panel', () => {
dataViews: new BehaviorSubject([]),
};
await renderPresentationPanel({ api });
- await waitFor(() => {
- expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('[No Title]');
- });
+ expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
});
it('opens customize panel flyout on title click when in edit mode', async () => {
@@ -274,7 +270,7 @@ describe('Presentation panel', () => {
expect(screen.queryByTestId('embeddablePanelTitleLink')).not.toBeInTheDocument();
});
- it('hides title when API hide title option is true', async () => {
+ it('hides title in view mode when API hide title option is true', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject('SUPER TITLE'),
@@ -285,7 +281,18 @@ describe('Presentation panel', () => {
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
});
- it('hides title when parent hide title option is true', async () => {
+ it('hides title in edit mode when API hide title option is true', async () => {
+ const api: DefaultPresentationPanelApi & PublishesViewMode = {
+ uuid: 'test',
+ panelTitle: new BehaviorSubject('SUPER TITLE'),
+ hidePanelTitle: new BehaviorSubject(true),
+ viewMode: new BehaviorSubject('edit'),
+ };
+ await renderPresentationPanel({ api });
+ expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
+ });
+
+ it('hides title in view mode when parent hide title option is true', async () => {
const api: DefaultPresentationPanelApi & PublishesViewMode = {
uuid: 'test',
panelTitle: new BehaviorSubject('SUPER TITLE'),
@@ -298,5 +305,19 @@ describe('Presentation panel', () => {
await renderPresentationPanel({ api });
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
});
+
+ it('hides title in edit mode when parent hide title option is true', async () => {
+ const api: DefaultPresentationPanelApi & PublishesViewMode = {
+ uuid: 'test',
+ panelTitle: new BehaviorSubject('SUPER TITLE'),
+ viewMode: new BehaviorSubject('edit'),
+ parentApi: {
+ viewMode: new BehaviorSubject('edit'),
+ ...getMockPresentationContainer(),
+ },
+ };
+ await renderPresentationPanel({ api });
+ expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
+ });
});
});
diff --git a/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx b/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx
index 6890ea2f76109..ccf2e694d1b7a 100644
--- a/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx
+++ b/src/plugins/presentation_panel/public/panel_component/presentation_panel_internal.tsx
@@ -16,6 +16,7 @@ import {
} from '@kbn/presentation-publishing';
import classNames from 'classnames';
import React, { useMemo, useState } from 'react';
+import { PresentationPanelHoverActions } from './panel_header/presentation_panel_hover_actions';
import { PresentationPanelHeader } from './panel_header/presentation_panel_header';
import { PresentationPanelError } from './presentation_panel_error';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from './types';
@@ -76,7 +77,7 @@ export const PresentationPanelInternal = <
const hideTitle =
Boolean(hidePanelTitle) ||
Boolean(parentHidePanelTitle) ||
- (viewMode === 'view' && !Boolean(panelTitle ?? defaultPanelTitle));
+ !Boolean(panelTitle ?? defaultPanelTitle);
const contentAttrs = useMemo(() => {
const attrs: { [key: string]: boolean } = {};
@@ -90,55 +91,56 @@ export const PresentationPanelInternal = <
}, [dataLoading, blockingError]);
return (
-
- {!hideHeader && api && (
-
- )}
- {blockingError && api && (
-
-
-
- )}
- {!initialLoadComplete && }
-
-
- )}
- ref={(newApi) => {
- if (newApi && !api) setApi(newApi);
- }}
+
+ {!hideHeader && api && (
+
-
-
-
+ )}
+ {blockingError && api && (
+
+
+
+ )}
+ {!initialLoadComplete && }
+
+
+ )}
+ ref={(newApi) => {
+ if (newApi && !api) setApi(newApi);
+ }}
+ />
+
+
+
+
);
};
diff --git a/src/plugins/presentation_panel/public/panel_component/types.ts b/src/plugins/presentation_panel/public/panel_component/types.ts
index a05fbc6d92a75..fa60f134321ac 100644
--- a/src/plugins/presentation_panel/public/panel_component/types.ts
+++ b/src/plugins/presentation_panel/public/panel_component/types.ts
@@ -9,6 +9,7 @@
import { PresentationContainer } from '@kbn/presentation-containers';
import {
+ CanLockHoverActions,
HasParentApi,
HasUniqueId,
PublishesBlockingError,
@@ -74,7 +75,8 @@ export interface DefaultPresentationPanelApi
HasParentApi<
PresentationContainer &
Partial & PublishesViewMode>
- >
+ > &
+ CanLockHoverActions
> {}
export type PresentationPanelProps<
diff --git a/src/plugins/saved_objects_tagging_oss/common/index.ts b/src/plugins/saved_objects_tagging_oss/common/index.ts
index 7d0a78571cb3c..c07d4e612aa49 100644
--- a/src/plugins/saved_objects_tagging_oss/common/index.ts
+++ b/src/plugins/saved_objects_tagging_oss/common/index.ts
@@ -10,6 +10,7 @@
export type {
Tag,
TagAttributes,
+ CreateTagOptions,
GetAllTagsOptions,
ITagsClient,
TagWithOptionalId,
diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx
index 7f02a934a4370..d62551efce297 100644
--- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx
+++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx
@@ -21,7 +21,7 @@ export const txtMore = i18n.translate('uiActions.actionPanel.more', {
defaultMessage: 'More',
});
-interface ActionWithContext {
+export interface ActionWithContext {
action: Action | ActionInternal;
context: Context;
@@ -37,6 +37,7 @@ type ItemDescriptor = EuiContextMenuPanelItemDescriptor & {
};
type PanelDescriptor = EuiContextMenuPanelDescriptor & {
+ _order?: number;
_level?: number;
_icon?: string;
items: ItemDescriptor[];
@@ -101,7 +102,7 @@ const removeItemMetaFields = (items: ItemDescriptor[]): EuiContextMenuPanelItemD
const removePanelMetaFields = (panels: PanelDescriptor[]): EuiContextMenuPanelDescriptor[] => {
const euiPanels: EuiContextMenuPanelDescriptor[] = [];
for (const panel of panels) {
- const { _level: omit, _icon: omit2, ...rest } = panel;
+ const { _level: omit, _icon: omit2, _order: omit3, ...rest } = panel;
euiPanels.push({ ...rest, items: removeItemMetaFields(rest.items) });
}
return euiPanels;
@@ -124,15 +125,18 @@ export async function buildContextMenuForActions({
const panels: Record = {
mainMenu: {
id: 'mainMenu',
- title,
items: [],
},
};
const promises = actions.map(async (item) => {
const { action } = item;
- const context: ActionExecutionContext