diff --git a/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts b/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts index 110251e4d759f4..423fab660695b5 100644 --- a/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts +++ b/packages/kbn-ftr-common-functional-ui-services/services/web_element_wrapper/web_element_wrapper.ts @@ -427,16 +427,16 @@ export class WebElementWrapper { /** * Moves the remote environment’s mouse cursor to the current element with optional offset * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move - * @param { xOffset: 0, yOffset: 0 } options + * @param { xOffset: 0, yOffset: 0, topOffset: number } options Optional * @return {Promise} */ - public async moveMouseTo(options = { xOffset: 0, yOffset: 0 }) { + public async moveMouseTo({ xOffset = 0, yOffset = 0, topOffset = 0 } = {}) { await this.retryCall(async function moveMouseTo(wrapper) { - await wrapper.scrollIntoViewIfNecessary(); + await wrapper.scrollIntoViewIfNecessary(topOffset); await wrapper.getActions().move({ x: 0, y: 0 }).perform(); await wrapper .getActions() - .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) + .move({ x: xOffset, y: yOffset, origin: wrapper._webElement }) .perform(); }); } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index bd0234eaa87ec7..7936e52ccbf184 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -23,7 +23,7 @@ pageLoadAssetSize: core: 564663 crossClusterReplication: 65408 customIntegrations: 22034 - dashboard: 52967 + dashboard: 68015 dashboardEnhanced: 65646 data: 454087 dataQuality: 19384 diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index ab5e396e657271..2b96c6d353eeee 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -30,6 +30,10 @@ export { useInheritedViewMode, type CanAccessViewMode, } from './interfaces/can_access_view_mode'; +export { + apiCanLockHoverActions, + type CanLockHoverActions, +} from './interfaces/can_lock_hover_actions'; export { fetch$, useFetchContext, type FetchContext } from './interfaces/fetch/fetch'; export { initializeTimeRange, diff --git a/packages/presentation/presentation_publishing/interfaces/can_lock_hover_actions.ts b/packages/presentation/presentation_publishing/interfaces/can_lock_hover_actions.ts new file mode 100644 index 00000000000000..db7a0c5cc8a3b3 --- /dev/null +++ b/packages/presentation/presentation_publishing/interfaces/can_lock_hover_actions.ts @@ -0,0 +1,27 @@ +/* + * 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 { PublishingSubject } from '../publishing_subject'; + +/** + * This API can lock hover actions + */ +export interface CanLockHoverActions { + hasLockedHoverActions$: PublishingSubject; + lockHoverActions: (lock: boolean) => void; +} + +export const apiCanLockHoverActions = (api: unknown): api is CanLockHoverActions => { + return Boolean( + api && + (api as CanLockHoverActions).hasLockedHoverActions$ && + (api as CanLockHoverActions).lockHoverActions && + typeof (api as CanLockHoverActions).lockHoverActions === 'function' + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_actions/add_to_library_action.tsx b/src/plugins/dashboard/public/dashboard_actions/add_to_library_action.tsx index acd46f2763bbcc..90da6c3297cbd0 100644 --- a/src/plugins/dashboard/public/dashboard_actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/add_to_library_action.tsx @@ -39,6 +39,7 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { coreServices } from '../services/kibana_services'; import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings'; +import { DASHBOARD_ACTION_GROUP } from '.'; export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; @@ -63,6 +64,7 @@ export class AddToLibraryAction implements Action { public readonly type = ACTION_ADD_TO_LIBRARY; public readonly id = ACTION_ADD_TO_LIBRARY; public order = 8; + public grouping = [DASHBOARD_ACTION_GROUP]; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx index 5eec25f1f052ba..4eae444dfecb7d 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -20,6 +20,7 @@ import { HasUniqueId, } from '@kbn/presentation-publishing'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { DASHBOARD_ACTION_GROUP } from '.'; import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings'; export const ACTION_CLONE_PANEL = 'clonePanel'; @@ -41,6 +42,7 @@ export class ClonePanelAction implements Action { public readonly type = ACTION_CLONE_PANEL; public readonly id = ACTION_CLONE_PANEL; public order = 45; + public grouping = [DASHBOARD_ACTION_GROUP]; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); diff --git a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx index fb318869197735..10b21fc36edccc 100644 --- a/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/copy_to_dashboard_action.tsx @@ -28,6 +28,7 @@ import { DASHBOARD_CONTAINER_TYPE } from '../dashboard_container'; import { coreServices } from '../services/kibana_services'; import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_strings'; +import { DASHBOARD_ACTION_GROUP } from '.'; import { CopyToDashboardModal } from './copy_to_dashboard_modal'; export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard'; @@ -59,6 +60,7 @@ export class CopyToDashboardAction implements Action { public readonly type = ACTION_COPY_TO_DASHBOARD; public readonly id = ACTION_COPY_TO_DASHBOARD; public order = 1; + public grouping = [DASHBOARD_ACTION_GROUP]; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!apiIsCompatible(embeddable)) throw new IncompatibleActionError(); diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx index 09bc56ea88586c..1ebf937e470e56 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.test.tsx @@ -13,15 +13,17 @@ import { ExpandPanelActionApi, ExpandPanelAction } from './expand_panel_action'; describe('Expand panel action', () => { let action: ExpandPanelAction; let context: { embeddable: ExpandPanelActionApi }; + let expandPanelIdSubject: BehaviorSubject; beforeEach(() => { + expandPanelIdSubject = new BehaviorSubject(undefined); action = new ExpandPanelAction(); context = { embeddable: { uuid: 'superId', parentApi: { expandPanel: jest.fn(), - expandedPanelId: new BehaviorSubject(undefined), + expandedPanelId: expandPanelIdSubject, }, }, }; @@ -38,19 +40,22 @@ describe('Expand panel action', () => { expect(await action.isCompatible(emptyContext)).toBe(false); }); + it('calls onChange when expandedPanelId changes', async () => { + const onChange = jest.fn(); + action.subscribeToCompatibilityChanges(context, onChange); + expandPanelIdSubject.next('superPanelId'); + expect(onChange).toHaveBeenCalledWith(true, action); + }); + it('returns the correct icon based on expanded panel id', async () => { expect(await action.getIconType(context)).toBe('expand'); - context.embeddable.parentApi.expandedPanelId = new BehaviorSubject( - 'superPanelId' - ); + expandPanelIdSubject.next('superPanelId'); expect(await action.getIconType(context)).toBe('minimize'); }); it('returns the correct display name based on expanded panel id', async () => { expect(await action.getDisplayName(context)).toBe('Maximize'); - context.embeddable.parentApi.expandedPanelId = new BehaviorSubject( - 'superPanelId' - ); + expandPanelIdSubject.next('superPanelId'); expect(await action.getDisplayName(context)).toBe('Minimize'); }); diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx index b4f2a06e6895a9..a207d181d26cca 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx @@ -16,6 +16,8 @@ import { HasUniqueId, } from '@kbn/presentation-publishing'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { skip } from 'rxjs'; +import { DASHBOARD_ACTION_GROUP } from '.'; import { dashboardExpandPanelActionStrings } from './_dashboard_actions_strings'; @@ -29,7 +31,8 @@ const isApiCompatible = (api: unknown | null): api is ExpandPanelActionApi => export class ExpandPanelAction implements Action { public readonly type = ACTION_EXPAND_PANEL; public readonly id = ACTION_EXPAND_PANEL; - public order = 7; + public order = 9; + public grouping = [DASHBOARD_ACTION_GROUP]; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); @@ -47,6 +50,20 @@ export class ExpandPanelAction implements Action { return isApiCompatible(embeddable); } + public couldBecomeCompatible({ embeddable }: EmbeddableApiContext) { + return apiHasParentApi(embeddable) && apiCanExpandPanels(embeddable.parentApi); + } + + public subscribeToCompatibilityChanges( + { embeddable }: EmbeddableApiContext, + onChange: (isCompatible: boolean, action: ExpandPanelAction) => void + ) { + if (!isApiCompatible(embeddable)) return; + return embeddable.parentApi.expandedPanelId.pipe(skip(1)).subscribe(() => { + onChange(isApiCompatible(embeddable), this); + }); + } + public async execute({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); embeddable.parentApi.expandPanel(embeddable.uuid); diff --git a/src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx index fd55816134ed1d..94dbf9e3087aa6 100644 --- a/src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/export_csv_action.tsx @@ -41,7 +41,7 @@ const isApiCompatible = (api: unknown | null): api is ExportCsvActionApi => export class ExportCSVAction implements Action { public readonly id = ACTION_EXPORT_CSV; public readonly type = ACTION_EXPORT_CSV; - public readonly order = 18; // right after Export in discover which is 19 + public readonly order = 18; public getIconType() { return 'exportAction'; diff --git a/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx index 29b03539790733..e639168b00c7f2 100644 --- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.test.tsx @@ -9,7 +9,6 @@ import { Filter, FilterStateStore, type AggregateQuery, type Query } from '@kbn/es-query'; -import { ViewMode } from '@kbn/presentation-publishing'; import { BehaviorSubject } from 'rxjs'; import { FiltersNotificationAction, @@ -42,7 +41,6 @@ describe('filters notification action', () => { let updateFilters: (filters: Filter[]) => void; let updateQuery: (query: Query | AggregateQuery | undefined) => void; - let updateViewMode: (viewMode: ViewMode) => void; beforeEach(() => { const filtersSubject = new BehaviorSubject(undefined); @@ -50,14 +48,10 @@ describe('filters notification action', () => { const querySubject = new BehaviorSubject(undefined); updateQuery = (query) => querySubject.next(query); - const viewModeSubject = new BehaviorSubject('edit'); - updateViewMode = (viewMode) => viewModeSubject.next(viewMode); - action = new FiltersNotificationAction(); context = { embeddable: { uuid: 'testId', - viewMode: viewModeSubject, filters$: filtersSubject, query$: querySubject, }, @@ -83,22 +77,6 @@ describe('filters notification action', () => { expect(await action.isCompatible(context)).toBe(true); }); - it('is incompatible when api is in view mode', async () => { - updateFilters([getMockPhraseFilter('SuperField', 'SuperValue')]); - updateQuery({ esql: 'FROM test_dataview' } as AggregateQuery); - updateViewMode('view'); - expect(await action.isCompatible(context)).toBe(false); - }); - - it('calls onChange when view mode changes', () => { - const onChange = jest.fn(); - updateFilters([getMockPhraseFilter('SuperField', 'SuperValue')]); - updateQuery({ esql: 'FROM test_dataview' } as AggregateQuery); - action.subscribeToCompatibilityChanges(context, onChange); - updateViewMode('view'); - expect(onChange).toHaveBeenCalledWith(false, action); - }); - it('calls onChange when filters change', async () => { const onChange = jest.fn(); action.subscribeToCompatibilityChanges(context, onChange); diff --git a/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx index 854ff5da948f4c..9662c8956dcc83 100644 --- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx @@ -13,17 +13,15 @@ import { merge } from 'rxjs'; import { isOfAggregateQueryType, isOfQueryType } from '@kbn/es-query'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { - CanAccessViewMode, + apiPublishesPartialUnifiedSearch, + apiHasUniqueId, EmbeddableApiContext, HasParentApi, HasUniqueId, PublishesDataViews, PublishesUnifiedSearch, - apiCanAccessViewMode, - apiHasUniqueId, - apiPublishesPartialUnifiedSearch, - getInheritedViewMode, - getViewModeSubject, + CanLockHoverActions, + CanAccessViewMode, } from '@kbn/presentation-publishing'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; @@ -34,17 +32,16 @@ import { FiltersNotificationPopover } from './filters_notification_popover'; export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION'; export type FiltersNotificationActionApi = HasUniqueId & - CanAccessViewMode & Partial & - Partial>>; + Partial>> & + Partial & + Partial; const isApiCompatible = (api: unknown | null): api is FiltersNotificationActionApi => - Boolean( - apiHasUniqueId(api) && apiCanAccessViewMode(api) && apiPublishesPartialUnifiedSearch(api) - ); + Boolean(apiHasUniqueId(api) && apiPublishesPartialUnifiedSearch(api)); const compatibilityCheck = (api: EmbeddableApiContext['embeddable']) => { - if (!isApiCompatible(api) || getInheritedViewMode(api) !== 'edit') return false; + if (!isApiCompatible(api)) return false; const query = api.query$?.value; return ( (api.filters$?.value ?? []).length > 0 || @@ -97,9 +94,7 @@ export class FiltersNotificationAction implements Action { ) { if (!isApiCompatible(embeddable)) return; return merge( - ...[embeddable.query$, embeddable.filters$, getViewModeSubject(embeddable)].filter((value) => - Boolean(value) - ) + ...[embeddable.query$, embeddable.filters$].filter((value) => Boolean(value)) ).subscribe(() => onChange(compatibilityCheck(embeddable), this)); } diff --git a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.test.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.test.tsx index b02443f01aaa8a..4488a96b52b683 100644 --- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.test.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.test.tsx @@ -10,13 +10,13 @@ import { AggregateQuery, Filter, FilterStateStore, Query } from '@kbn/es-query'; import { I18nProvider } from '@kbn/i18n-react'; import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl'; -import { ViewMode } from '@kbn/presentation-publishing'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { FiltersNotificationActionApi } from './filters_notification_action'; import { FiltersNotificationPopover } from './filters_notification_popover'; +import { ViewMode } from '@kbn/presentation-publishing'; const getMockPhraseFilter = (key: string, value: string): Filter => { return { @@ -50,18 +50,23 @@ describe('filters notification popover', () => { let api: FiltersNotificationActionApi; let updateFilters: (filters: Filter[]) => void; let updateQuery: (query: Query | AggregateQuery | undefined) => void; + let updateViewMode: (viewMode: ViewMode) => void; beforeEach(async () => { const filtersSubject = new BehaviorSubject(undefined); updateFilters = (filters) => filtersSubject.next(filters); const querySubject = new BehaviorSubject(undefined); updateQuery = (query) => querySubject.next(query); + const viewModeSubject = new BehaviorSubject('view'); + updateViewMode = (viewMode) => viewModeSubject.next(viewMode); api = { uuid: 'testId', - viewMode: new BehaviorSubject('edit'), filters$: filtersSubject, query$: querySubject, + parentApi: { + viewMode: viewModeSubject, + }, }; }); @@ -87,7 +92,15 @@ describe('filters notification popover', () => { expect(await screen.findByTestId('filtersNotificationModal__query')).toBeInTheDocument(); }); + it('does not render an edit button when not in edit mode', async () => { + await renderAndOpenPopover(); + expect( + await screen.queryByTestId('filtersNotificationModal__editButton') + ).not.toBeInTheDocument(); + }); + it('renders an edit button when the edit panel action is compatible', async () => { + updateViewMode('edit'); updateFilters([getMockPhraseFilter('ay', 'oh')]); await renderAndOpenPopover(); expect(await screen.findByTestId('filtersNotificationModal__editButton')).toBeInTheDocument(); @@ -104,6 +117,7 @@ describe('filters notification popover', () => { }); it('calls edit action execute when edit button is clicked', async () => { + updateViewMode('edit'); updateFilters([getMockPhraseFilter('ay', 'oh')]); await renderAndOpenPopover(); const editButton = await screen.findByTestId('filtersNotificationModal__editButton'); diff --git a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx index bafd06297fe7e2..5f23b21dc9155f 100644 --- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx @@ -26,8 +26,11 @@ import { css } from '@emotion/react'; import { AggregateQuery, getAggregateQueryMode, isOfQueryType } from '@kbn/es-query'; import { getEditPanelAction } from '@kbn/presentation-panel-plugin/public'; import { FilterItems } from '@kbn/unified-search-plugin/public'; -import { useStateFromPublishingSubject } from '@kbn/presentation-publishing'; -import { BehaviorSubject } from 'rxjs'; +import { + apiCanLockHoverActions, + getViewModeSubject, + useBatchedOptionalPublishingSubjects, +} from '@kbn/presentation-publishing'; import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings'; import { FiltersNotificationActionApi } from './filters_notification_action'; @@ -59,8 +62,10 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc } }, [api, setDisableEditButton]); - const dataViews = useStateFromPublishingSubject( - api.parentApi?.dataViews ? api.parentApi.dataViews : new BehaviorSubject(undefined) + const [hasLockedHoverActions, dataViews, parentViewMode] = useBatchedOptionalPublishingSubjects( + api.hasLockedHoverActions$, + api.parentApi?.dataViews, + getViewModeSubject(api ?? undefined) ); return ( @@ -69,13 +74,23 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc setIsPopoverOpen(!isPopoverOpen)} + onClick={() => { + setIsPopoverOpen(!isPopoverOpen); + if (apiCanLockHoverActions(api)) { + api?.lockHoverActions(!hasLockedHoverActions); + } + }} data-test-subj={`embeddablePanelNotification-${api.uuid}`} aria-label={displayName} /> } isOpen={isPopoverOpen} - closePopover={() => setIsPopoverOpen(false)} + closePopover={() => { + setIsPopoverOpen(false); + if (apiCanLockHoverActions(api)) { + api.lockHoverActions(false); + } + }} anchorPosition="upCenter" > {displayName} @@ -112,8 +127,8 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc )} - - {!disableEditbutton && ( + {!disableEditbutton && parentViewMode === 'edit' && ( + - )} - + + )} ); } diff --git a/src/plugins/dashboard/public/dashboard_actions/index.ts b/src/plugins/dashboard/public/dashboard_actions/index.ts index 55a371719d953e..1b9f2091fbce6c 100644 --- a/src/plugins/dashboard/public/dashboard_actions/index.ts +++ b/src/plugins/dashboard/public/dashboard_actions/index.ts @@ -24,6 +24,8 @@ interface BuildAllDashboardActionsProps { plugins: DashboardStartDependencies; } +export const DASHBOARD_ACTION_GROUP = { id: 'dashboard_actions', order: 10 } as const; + export const buildAllDashboardActions = async ({ plugins, allowByValueEmbeddables, diff --git a/src/plugins/dashboard/public/dashboard_actions/legacy_add_to_library_action.tsx b/src/plugins/dashboard/public/dashboard_actions/legacy_add_to_library_action.tsx index dee049dc2874e5..6cc46b6af51e34 100644 --- a/src/plugins/dashboard/public/dashboard_actions/legacy_add_to_library_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/legacy_add_to_library_action.tsx @@ -18,6 +18,7 @@ import { HasLegacyLibraryTransforms, } from '@kbn/presentation-publishing'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { DASHBOARD_ACTION_GROUP } from '.'; import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings'; import { coreServices } from '../services/kibana_services'; @@ -35,6 +36,7 @@ export class LegacyAddToLibraryAction implements Action { public readonly type = ACTION_LEGACY_ADD_TO_LIBRARY; public readonly id = ACTION_LEGACY_ADD_TO_LIBRARY; public order = 15; + public grouping = [DASHBOARD_ACTION_GROUP]; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); diff --git a/src/plugins/dashboard/public/dashboard_actions/legacy_unlink_from_library_action.tsx b/src/plugins/dashboard/public/dashboard_actions/legacy_unlink_from_library_action.tsx index 96daab215dec63..668f02dee3159f 100644 --- a/src/plugins/dashboard/public/dashboard_actions/legacy_unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/legacy_unlink_from_library_action.tsx @@ -20,6 +20,7 @@ import { HasLegacyLibraryTransforms, } from '@kbn/presentation-publishing'; import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings'; +import { DASHBOARD_ACTION_GROUP } from '.'; import { coreServices } from '../services/kibana_services'; export const ACTION_LEGACY_UNLINK_FROM_LIBRARY = 'legacyUnlinkFromLibrary'; @@ -37,6 +38,7 @@ export class LegacyUnlinkFromLibraryAction implements Action { public readonly type = ACTION_UNLINK_FROM_LIBRARY; public readonly id = ACTION_UNLINK_FROM_LIBRARY; public order = 15; + public grouping = [DASHBOARD_ACTION_GROUP]; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); diff --git a/src/plugins/dashboard/public/dashboard_api/track_panel.ts b/src/plugins/dashboard/public/dashboard_api/track_panel.ts index 42345f38d614f9..b9f9b3218488b4 100644 --- a/src/plugins/dashboard/public/dashboard_api/track_panel.ts +++ b/src/plugins/dashboard/public/dashboard_api/track_panel.ts @@ -73,7 +73,7 @@ export function initializeTrackPanel(untilEmbeddableLoaded: (id: string) => Prom }; return; } - panelRef.scrollIntoView({ block: 'nearest' }); + panelRef.scrollIntoView({ block: 'start' }); }); }, scrollToTop: () => { diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss index f6e7918fb1b0ba..49a6b01049da74 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss @@ -19,7 +19,7 @@ .dshLayout--editing { .react-resizable-handle { @include size($euiSizeL); - z-index: $euiZLevel1; /* 1 */ + z-index: $euiZLevel2; /* 1 */ right: 0; bottom: 0; padding-right: $euiSizeS; @@ -33,6 +33,10 @@ */ .dshLayout-isMaximizedPanel { height: 100% !important; /* 1. */ + + .embPanel__hoverActionsLeft { + visibility: hidden; + } } /** @@ -40,8 +44,7 @@ * Shifting the rendered panels offscreen prevents a quick flash when redrawing the panels on minimize */ .dshDashboardGrid__item--hidden { - top: -9999px; - left: -9999px; + transform: translate(-9999px, -9999px); } /** @@ -98,13 +101,26 @@ */ &.resizing, &.react-draggable-dragging { - z-index: $euiZLevel2 !important; + z-index: $euiZLevel3 !important; } &.react-draggable-dragging { transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance; @include euiBottomShadowLarge; border-radius: $euiBorderRadius; // keeps shadow within bounds + + .embPanel__hoverActionsWrapper { + z-index: $euiZLevel9; + top: -$euiSizeXL; + + .embPanel__hoverActions:has(.embPanel--dragHandle) { + opacity: 1; + } + + .embPanel__hoverActions:not(:has(.embPanel--dragHandle)) { + opacity: 0; + } + } } /** diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss index d54f513a207a43..93a95e1ef37e51 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss @@ -4,24 +4,42 @@ * .embPanel--editing doesn't get updating without a hard refresh */ -.dshDashboardGrid__item { - scroll-margin-top: calc((var(--euiFixedHeadersOffset, 100) * 2) + $euiSizeS); - scroll-margin-bottom: $euiSizeS; +.dshLayout--editing { + // change the style of the hover actions border to a dashed line in edit mode + .embPanel__hoverActionsAnchor { + .embPanel__hoverActionsWrapper { + .embPanel__hoverActions { + border-color: $euiColorMediumShade; + border-style: dashed; + } + } + } } // LAYOUT MODES // Adjust borders/etc... for non-spaced out and expanded panels .dshLayout-withoutMargins { - .embPanel { + .embPanel, + .embPanel__hoverActionsAnchor { box-shadow: none; + outline: none; border-radius: 0; } - .embPanel__content { - border-radius: 0; + &.dshLayout--editing { + .embPanel__hoverActionsAnchor:hover { + outline: 1px dashed $euiColorMediumShade; + } } - .dshDashboardGrid__item--highlighted { + .embPanel__hoverActionsAnchor:hover { + outline: $euiBorderThin; + z-index: $euiZLevel2; + } + + .embPanel__content, + .dshDashboardGrid__item--highlighted, + .lnsExpressionRenderer { border-radius: 0; } } @@ -35,6 +53,20 @@ background-color: unset; cursor: default; } + + .embPanel__hoverActions { + .embPanel--dragHandle { + visibility: hidden; + } + } +} + +// Hide hover actions when dashboard has an overlay +.dshDashboardGrid__item--blurred, +.dshDashboardGrid__item--focused { + .embPanel__hoverActions { + visibility: hidden; + } } @keyframes highlightOutline { @@ -52,10 +84,11 @@ } .dshDashboardGrid__item--highlighted { - border-radius: $euiSizeXS; - animation-name: highlightOutline; - animation-duration: 4s; - animation-timing-function: ease-out; - // keeps outline from getting cut off by other panels without margins - z-index: 999 !important; + .embPanel { + border-radius: $euiSizeXS; + animation-name: highlightOutline; + animation-duration: 4s; + animation-timing-function: ease-out; + z-index: $euiZLevel2; + } } diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index 577661b393c67b..0ef976af51eb6e 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -133,6 +133,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { rowHeight={DASHBOARD_GRID_HEIGHT} margin={useMargins ? [DASHBOARD_MARGIN_SIZE, DASHBOARD_MARGIN_SIZE] : [0, 0]} draggableHandle={'.embPanel--dragHandle'} + useCSSTransforms={false} > {panelComponents} diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx index 7b21db4ea3f84c..9b5a00c6286080 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx @@ -15,6 +15,7 @@ import { css } from '@emotion/react'; import { EmbeddablePanel, ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; +import { DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants'; import { DashboardPanelState } from '../../../../common'; import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api'; import { embeddableService, presentationUtilService } from '../../../services/kibana_services'; @@ -91,12 +92,21 @@ export const Item = React.forwardRef( } }, [id, dashboardApi, scrollToPanelId, highlightPanelId, ref, blurPanel]); + const dashboardContainerTopOffset = + (document.querySelector('.dashboardContainer') as HTMLDivElement)?.offsetTop || 0; + const globalNavTopOffset = + (document.querySelector('#app-fixed-viewport') as HTMLDivElement)?.offsetTop || 0; + const focusStyles = blurPanel ? css` pointer-events: none; opacity: 0.25; ` - : undefined; + : css` + scroll-margin-top: ${dashboardContainerTopOffset + + globalNavTopOffset + + DASHBOARD_MARGIN_SIZE}px; + `; const renderedEmbeddable = useMemo(() => { const panelProps = { diff --git a/src/plugins/dashboard/public/dashboard_container/component/viewport/_dashboard_viewport.scss b/src/plugins/dashboard/public/dashboard_container/component/viewport/_dashboard_viewport.scss index f0c51724b551ba..79e7c16bfe4a7f 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/viewport/_dashboard_viewport.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/viewport/_dashboard_viewport.scss @@ -23,3 +23,9 @@ .dashboardViewport--screenshotMode .controlsWrapper--empty { display:none } + +.dshDashboardViewportWrapper--isFullscreen { + .dshDashboardGrid__item--expanded { + padding: $euiSizeS; + } +} \ No newline at end of file diff --git a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx index ac39b3747b1bdc..664a3c43a8d9de 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/viewport/dashboard_viewport.tsx @@ -57,6 +57,7 @@ export const DashboardViewportComponent = () => { viewMode, useMargins, uuid, + fullScreenMode, ] = useBatchedPublishingSubjects( dashboardApi.controlGroupApi$, dashboardApi.panelTitle, @@ -66,7 +67,8 @@ export const DashboardViewportComponent = () => { dashboardApi.panels$, dashboardApi.viewMode, dashboardApi.useMargins$, - dashboardApi.uuid$ + dashboardApi.uuid$, + dashboardApi.fullScreenMode$ ); const panelCount = useMemo(() => { @@ -114,6 +116,7 @@ export const DashboardViewportComponent = () => {
{viewMode !== ViewMode.PRINT ? ( 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 6b0141a50861d7..0d3f80ae79fec3 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/discover/public/embeddable/actions/view_saved_search_action.ts b/src/plugins/discover/public/embeddable/actions/view_saved_search_action.ts index 4d77e9dbd44003..d1092a28d9f553 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/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts index dab0968af00561..daab774d7f35dc 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 89df109be5ef11..9fc3598bcd5ad2 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 779c1a235bc82d..57cf7eec6eb955 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 3722647526c792..63433d1d1319b8 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 0f9ae361bbf939..c3dc06e198cd8f 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 1ab43d4bb1b7d7..4ba8653310ff08 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 177f2f1c82118b..685f0a6c46a3b6 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 b065ed5cedf594..335fda267a8002 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 434cca42e7c9ff..5094cf6b02ba3e 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 2376c4b43edbb0..00000000000000 --- 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 669f15cb2ba6bc..0747e4a4f82299 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 00000000000000..469a1f8c4f6e38 --- /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 4189250e394d3f..ef819c427c7652 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 570fdfd91e2298..b48a4eca7ae1ff 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 550c76a14aee1f..fa86060859098a 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 6890ea2f76109b..ccf2e694d1b7a0 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 a05fbc6d92a752..fa60f134321ac0 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/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 7f02a934a4370b..d62551efce297a 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 = { ...item.context, trigger: item.trigger }; + const context: ActionExecutionContext = { + ...item.context, + trigger: item.trigger, + }; const isCompatible = await item.action.isCompatible(context); if (!isCompatible) return; + let parentPanel = ''; let currentPanel = ''; if (action.grouping) { @@ -146,6 +150,7 @@ export async function buildContextMenuForActions({ title: name, items: [], _level: i, + _order: group.order || 0, _icon: group.getIconType ? group.getIconType(context) : 'empty', }; if (parentPanel) { @@ -190,7 +195,11 @@ export async function buildContextMenuForActions({ wrapMainPanelItemsIntoSubmenu(panels, 'mainMenu'); - for (const panel of Object.values(panels)) { + const sortedPanels = Object.values(panels).sort((a, b) => { + return (b._order || 0) - (a._order || 0); + }); + + for (const panel of sortedPanels) { if (panel._level === 0) { if (panels.mainMenu.items.length > 0) { panels.mainMenu.items.push({ @@ -198,7 +207,7 @@ export async function buildContextMenuForActions({ key: panel.id + '__separator', }); } - if (panel.items.length > 3) { + if (panel.items.length > 4) { panels.mainMenu.items.push({ name: panel.title || panel.id, icon: panel._icon || 'empty', diff --git a/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts index f1c64555e048e7..640a3bbb703912 100644 --- a/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts +++ b/src/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_grouping.ts @@ -21,6 +21,6 @@ export const dynamicActionGrouping: PresentableGrouping<{ defaultMessage: 'Custom actions', }), getIconType: () => 'symlink', - order: 26, + order: 0, }, ]; diff --git a/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts index 2f40111ce1ed09..6546f5091a0db9 100644 --- a/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts +++ b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts @@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const newTitle = 'wowee, my title just got cooler'; await header.waitUntilLoadingHasFinished(); const originalPanelCount = await dashboard.getPanelCount(); - await dashboardPanelActions.clickEdit(); + await dashboardPanelActions.editPanelByTitle('wowee, looks like I have a new title'); await visualize.saveVisualizationExpectSuccess(newTitle, { saveAsNew: true, redirectToOrigin: true, @@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('loses originatingApp connection after save as when redirectToOrigin is false', async () => { const newTitle = 'wowee, my title just got cooler again'; await header.waitUntilLoadingHasFinished(); - await dashboardPanelActions.editPanelByTitle('wowee, my title just got cooler'); + await dashboardPanelActions.clickEdit(); await visualize.linkedToOriginatingApp(); await visualize.saveVisualizationExpectSuccess(newTitle, { saveAsNew: true, diff --git a/test/functional/apps/dashboard/group3/panel_context_menu.ts b/test/functional/apps/dashboard/group3/panel_context_menu.ts index 0bf31cf58616c2..367dae942af92e 100644 --- a/test/functional/apps/dashboard/group3/panel_context_menu.ts +++ b/test/functional/apps/dashboard/group3/panel_context_menu.ts @@ -48,9 +48,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('are shown in edit mode', async function () { await dashboard.switchToEditMode(); - const isContextMenuIconVisible = await dashboardPanelActions.isContextMenuIconVisible(); - expect(isContextMenuIconVisible).to.equal(true); - await dashboardPanelActions.expectExistsEditPanelAction(); await dashboardPanelActions.expectExistsClonePanelAction(); await dashboardPanelActions.expectExistsRemovePanelAction(); diff --git a/test/functional/apps/dashboard/group5/saved_search_embeddable.ts b/test/functional/apps/dashboard/group5/saved_search_embeddable.ts index 4b488fdb25d8a9..25e525747edc6f 100644 --- a/test/functional/apps/dashboard/group5/saved_search_embeddable.ts +++ b/test/functional/apps/dashboard/group5/saved_search_embeddable.ts @@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await header.waitUntilLoadingHasFinished(); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.clickContextMenuItem( + await dashboardPanelActions.clickPanelAction( 'embeddablePanelAction-ACTION_VIEW_SAVED_SEARCH' ); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index f6a3aec2eacd56..2a263e9aa8ca7b 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -296,11 +296,13 @@ export class DashboardPageObject extends FtrService { // if the dashboard is not already in edit mode await this.testSubjects.click('dashboardEditMode'); } - // wait until the count of dashboard panels equals the count of toggle menu icons + // wait until the count of dashboard panels equals the count of drag handles await this.retry.waitFor('in edit mode', async () => { - const panels = await this.testSubjects.findAll('embeddablePanel', 2500); - const menuIcons = await this.testSubjects.findAll('embeddablePanelToggleMenuIcon', 2500); - return panels.length === menuIcons.length; + const panels = await this.find.allByCssSelector('.embPanel__hoverActionsWrapper'); + const dragHandles = await this.find.allByCssSelector( + '[data-test-subj="embeddablePanelDragHandle"]' + ); + return panels.length === dragHandles.length; }); } diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index 07004fbb366551..dc6ba3498298e9 100644 Binary files a/test/functional/screenshots/baseline/area_chart.png and b/test/functional/screenshots/baseline/area_chart.png differ diff --git a/test/functional/screenshots/baseline/dashboard_embed_mode.png b/test/functional/screenshots/baseline/dashboard_embed_mode.png index 53a928bc1514ab..fad76455a2a240 100644 Binary files a/test/functional/screenshots/baseline/dashboard_embed_mode.png and b/test/functional/screenshots/baseline/dashboard_embed_mode.png differ diff --git a/test/functional/screenshots/baseline/tsvb_dashboard.png b/test/functional/screenshots/baseline/tsvb_dashboard.png index 8b33a0077efa23..5d29b891e6fd0d 100644 Binary files a/test/functional/screenshots/baseline/tsvb_dashboard.png and b/test/functional/screenshots/baseline/tsvb_dashboard.png differ diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 8cb8a9b635c2ca..75474fef41655a 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -26,106 +26,109 @@ const LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-legacyUnlink const UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-unlinkFromLibrary'; const CONVERT_TO_LENS_TEST_SUBJ = 'embeddablePanelAction-ACTION_EDIT_IN_LENS'; -const DASHBOARD_TOP_OFFSET = 96 + 105; // 96 for Kibana navigation bar + 105 for dashboard top nav bar (in edit mode) +const DASHBOARD_MARGIN_SIZE = 8; export class DashboardPanelActionsService extends FtrService { private readonly log = this.ctx.getService('log'); private readonly retry = this.ctx.getService('retry'); - private readonly browser = this.ctx.getService('browser'); private readonly find = this.ctx.getService('find'); private readonly inspector = this.ctx.getService('inspector'); private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly browser = this.ctx.getService('browser'); private readonly header = this.ctx.getPageObject('header'); private readonly common = this.ctx.getPageObject('common'); private readonly dashboard = this.ctx.getPageObject('dashboard'); - async findContextMenu(parent?: WebElementWrapper) { + async getContainerTopOffset() { + const containerSelector = (await this.find.existsByCssSelector('.dashboardContainer')) + ? '.dashboardContainer' + : '.canvasContainer'; + return ( + (await (await this.find.byCssSelector(containerSelector)).getPosition()).y + + DASHBOARD_MARGIN_SIZE + ); + } + + async findContextMenu(wrapper?: WebElementWrapper) { this.log.debug('findContextMenu'); - return parent - ? await this.testSubjects.findDescendant(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ, parent) + return wrapper + ? await wrapper.findByTestSubject(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ) : await this.testSubjects.find(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); } - async isContextMenuIconVisible() { - this.log.debug('isContextMenuIconVisible'); - return await this.testSubjects.exists(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); + async scrollPanelIntoView(wrapper?: WebElementWrapper) { + this.log.debug(`scrollPanelIntoView`); + wrapper = wrapper || (await this.getPanelWrapper()); + const yOffset = (await wrapper.getPosition()).y; + await this.browser.execute(` + const scrollY = window.scrollY; + window.scrollBy(0, scrollY - ${yOffset}); + `); + + const containerTop = await this.getContainerTopOffset(); + + await wrapper.moveMouseTo({ + topOffset: containerTop, + }); } - async toggleContextMenu(parent?: WebElementWrapper) { - this.log.debug(`toggleContextMenu(${parent})`); - if (parent) { - await parent.scrollIntoViewIfNecessary(DASHBOARD_TOP_OFFSET); - await this.browser.getActions().move({ x: 0, y: 0, origin: parent._webElement }).perform(); - } else { - await this.testSubjects.moveMouseTo('dashboardPanelTitle'); - } - const toggleMenuItem = await this.findContextMenu(parent); - await toggleMenuItem.click(DASHBOARD_TOP_OFFSET); + async toggleContextMenu(wrapper?: WebElementWrapper) { + this.log.debug(`toggleContextMenu`); + await this.scrollPanelIntoView(wrapper); + const toggleMenuItem = await this.findContextMenu(wrapper); + await toggleMenuItem.click(await this.getContainerTopOffset()); } async toggleContextMenuByTitle(title = '') { this.log.debug(`toggleContextMenu(${title})`); - const header = await this.getPanelHeading(title); - await this.toggleContextMenu(header); + const wrapper = await this.getPanelWrapper(title); + await this.toggleContextMenu(wrapper); } async expectContextMenuToBeOpen() { this.log.debug('expectContextMenuToBeOpen'); - await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen'); + await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen', { allowHidden: true }); } - async openContextMenu(parent?: WebElementWrapper) { - this.log.debug(`openContextMenu`); + async openContextMenu(wrapper?: WebElementWrapper) { + this.log.debug(`openContextMenu(${wrapper}`); const open = await this.testSubjects.exists('embeddablePanelContextMenuOpen'); - if (!open) await this.toggleContextMenu(parent); + if (!open) await this.toggleContextMenu(wrapper); await this.expectContextMenuToBeOpen(); } async openContextMenuByTitle(title = '') { this.log.debug(`openContextMenuByTitle(${title})`); - const header = await this.getPanelHeading(title); - await this.openContextMenu(header); - } - - async hasContextMenuMoreItem() { - this.log.debug('hasContextMenuMoreItem'); - return await this.testSubjects.exists('embeddablePanelMore-mainMenu', { timeout: 500 }); + const wrapper = await this.getPanelWrapper(title); + await this.openContextMenu(wrapper); } - async clickContextMenuMoreItem() { - this.log.debug('clickContextMenuMoreItem'); - await this.expectContextMenuToBeOpen(); - if (await this.hasContextMenuMoreItem()) { - await this.testSubjects.clickWhenNotDisabledWithoutRetry('embeddablePanelMore-mainMenu'); - } - } - - async openContextMenuMorePanel(parent?: WebElementWrapper) { - this.log.debug('openContextMenuMorePanel'); - await this.openContextMenu(parent); - await this.clickContextMenuMoreItem(); - } - - async clickContextMenuItem(testSubject: string, parent?: WebElementWrapper) { - this.log.debug(`clickContextMenuItem(${testSubject})`); - await this.openContextMenu(parent); - const exists = await this.testSubjects.exists(testSubject, { timeout: 500 }); + async clickPanelAction(testSubject: string, wrapper?: WebElementWrapper) { + this.log.debug(`clickPanelAction(${testSubject})`); + wrapper = wrapper || (await this.getPanelWrapper()); + await this.scrollPanelIntoView(wrapper); + const exists = await this.testSubjects.descendantExists(testSubject, wrapper); + let action; if (!exists) { - await this.clickContextMenuMoreItem(); + await this.openContextMenu(wrapper); + action = await this.testSubjects.find(testSubject); + } else { + action = await this.testSubjects.findDescendant(testSubject, wrapper); } - await this.testSubjects.clickWhenNotDisabledWithoutRetry(testSubject, { timeout: 500 }); + + await action.click(await this.getContainerTopOffset()); } - async clickContextMenuItemByTitle(testSubject: string, title = '') { - this.log.debug(`openContextMenuByTitle(${title})`); - const header = await this.getPanelHeading(title); - await this.clickContextMenuItem(testSubject, header); + async clickPanelActionByTitle(testSubject: string, title = '') { + this.log.debug(`clickPanelActionByTitle(${testSubject},${title})`); + const wrapper = await this.getPanelWrapper(title); + await this.clickPanelAction(testSubject, wrapper); } - async navigateToEditorFromFlyout() { + async navigateToEditorFromFlyout(wrapper?: WebElementWrapper) { this.log.debug('navigateToEditorFromFlyout'); - await this.clickContextMenuItem(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); + await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ, wrapper); await this.header.waitUntilLoadingHasFinished(); await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ); const isConfirmModalVisible = await this.testSubjects.exists('confirmModalConfirmButton'); @@ -138,7 +141,7 @@ export class DashboardPanelActionsService extends FtrService { async clickInlineEdit() { this.log.debug('clickInlineEditAction'); - await this.clickContextMenuItem(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); + await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); await this.header.waitUntilLoadingHasFinished(); await this.common.waitForTopNavToBeVisible(); } @@ -147,20 +150,16 @@ export class DashboardPanelActionsService extends FtrService { * The dashboard/canvas panels can be either edited on their editor or inline. * The inline editing panels allow the navigation to the editor after the flyout opens */ - async clickEdit(parent?: WebElementWrapper) { - this.log.debug('clickEdit'); - await this.openContextMenu(parent); - const isActionVisible = await this.testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ); - const isInlineEditingActionVisible = await this.testSubjects.exists( - INLINE_EDIT_PANEL_DATA_TEST_SUBJ - ); - if (!isActionVisible && !isInlineEditingActionVisible) await this.clickContextMenuMoreItem(); - // navigate to the editor - if (await this.testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ)) { - await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_PANEL_DATA_TEST_SUBJ); - // open the flyout and then navigate to the editor + async clickEdit(wrapper?: WebElementWrapper) { + this.log.debug(`clickEdit`); + wrapper = wrapper || (await this.getPanelWrapper()); + await this.scrollPanelIntoView(wrapper); + if (await this.testSubjects.descendantExists(EDIT_PANEL_DATA_TEST_SUBJ, wrapper)) { + // navigate to the editor + await this.clickPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, wrapper); } else { - await this.navigateToEditorFromFlyout(); + // open the flyout and then navigate to the editor + await this.navigateToEditorFromFlyout(wrapper); } await this.header.waitUntilLoadingHasFinished(); await this.common.waitForTopNavToBeVisible(); @@ -172,55 +171,55 @@ export class DashboardPanelActionsService extends FtrService { */ async editPanelByTitle(title = '') { this.log.debug(`editPanelByTitle(${title})`); - const header = await this.getPanelHeading(title); - await this.clickEdit(header); + const wrapper = await this.getPanelWrapper(title); + await this.clickEdit(wrapper); } async clickExpandPanelToggle() { this.log.debug(`clickExpandPanelToggle`); await this.openContextMenu(); - await this.clickContextMenuItem(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + await this.clickPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); } - async removePanel(parent?: WebElementWrapper) { + async removePanel(wrapper?: WebElementWrapper) { this.log.debug('removePanel'); - await this.openContextMenu(parent); - await this.clickContextMenuItem(REMOVE_PANEL_DATA_TEST_SUBJ, parent); + await this.clickPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ, wrapper); } async removePanelByTitle(title = '') { this.log.debug(`removePanel(${title})`); - const header = await this.getPanelHeading(title); - this.log.debug('found header? ', Boolean(header)); - await this.removePanel(header); + const wrapper = await this.getPanelWrapper(title); + await this.removePanel(wrapper); } async customizePanel(title = '') { this.log.debug(`customizePanel(${title})`); - const header = await this.getPanelHeading(title); - await this.clickContextMenuItem(CUSTOMIZE_PANEL_DATA_TEST_SUBJ, header); + await this.clickPanelActionByTitle(CUSTOMIZE_PANEL_DATA_TEST_SUBJ, title); } async clonePanel(title = '') { this.log.debug(`clonePanel(${title})`); - const header = await this.getPanelHeading(title); - await this.clickContextMenuItem(CLONE_PANEL_DATA_TEST_SUBJ, header); + await this.clickPanelActionByTitle(CLONE_PANEL_DATA_TEST_SUBJ, title); await this.dashboard.waitForRenderComplete(); } async openCopyToModalByTitle(title = '') { this.log.debug(`copyPanelTo(${title})`); - const header = await this.getPanelHeading(title); - await this.clickContextMenuItem(COPY_PANEL_TO_DATA_TEST_SUBJ, header); + await this.clickPanelActionByTitle(COPY_PANEL_TO_DATA_TEST_SUBJ, title); + } + + async openInspector(wrapper?: WebElementWrapper) { + this.log.debug(`openInspector`); + await this.clickPanelAction(OPEN_INSPECTOR_TEST_SUBJ, wrapper); } - async openInspectorByTitle(title: string) { + async openInspectorByTitle(title = '') { this.log.debug(`openInspector(${title})`); - const header = await this.getPanelHeading(title); - await this.openInspector(header); + const wrapper = await this.getPanelWrapper(title); + await this.openInspector(wrapper); } - async getSearchSessionIdByTitle(title: string) { + async getSearchSessionIdByTitle(title = '') { this.log.debug(`getSearchSessionId(${title})`); await this.openInspectorByTitle(title); await this.inspector.openInspectorRequestsView(); @@ -231,7 +230,7 @@ export class DashboardPanelActionsService extends FtrService { return searchSessionId; } - async getSearchResponseByTitle(title: string) { + async getSearchResponseByTitle(title = '') { this.log.debug(`setSearchResponse(${title})`); await this.openInspectorByTitle(title); await this.inspector.openInspectorRequestsView(); @@ -240,31 +239,23 @@ export class DashboardPanelActionsService extends FtrService { return response; } - async openInspector(parent?: WebElementWrapper) { - this.log.debug(`openInspector`); - await this.clickContextMenuItem(OPEN_INSPECTOR_TEST_SUBJ, parent); - } - async legacyUnlinkFromLibrary(title = '') { this.log.debug(`legacyUnlinkFromLibrary(${title}`); - const header = await this.getPanelHeading(title); - await this.clickContextMenuItem(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, header); + await this.clickPanelActionByTitle(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title); await this.testSubjects.existOrFail('unlinkPanelSuccess'); await this.expectNotLinkedToLibrary(title, true); } async unlinkFromLibrary(title = '') { this.log.debug(`unlinkFromLibrary(${title})`); - const header = await this.getPanelHeading(title); - await this.clickContextMenuItem(UNLINK_FROM_LIBRARY_TEST_SUBJ, header); + await this.clickPanelActionByTitle(UNLINK_FROM_LIBRARY_TEST_SUBJ, title); await this.testSubjects.existOrFail('unlinkPanelSuccess'); await this.expectNotLinkedToLibrary(title); } async legacySaveToLibrary(newTitle = '', oldTitle = '') { this.log.debug(`legacySaveToLibrary(${newTitle},${oldTitle})`); - const header = await this.getPanelHeading(oldTitle); - await this.clickContextMenuItem(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, header); + await this.clickPanelActionByTitle(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle); await this.testSubjects.setValue('savedObjectTitle', newTitle, { clearWithKeyboard: true, }); @@ -275,8 +266,7 @@ export class DashboardPanelActionsService extends FtrService { async saveToLibrary(newTitle = '', oldTitle = '') { this.log.debug(`saveToLibraryByTitle(${newTitle},${oldTitle})`); - const header = await this.getPanelHeading(oldTitle); - await this.clickContextMenuItem(SAVE_TO_LIBRARY_TEST_SUBJ, header); + await this.clickPanelActionByTitle(SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle); await this.testSubjects.setValue('savedObjectTitle', newTitle, { clearWithKeyboard: true, }); @@ -285,18 +275,31 @@ export class DashboardPanelActionsService extends FtrService { await this.expectLinkedToLibrary(newTitle); } + async panelActionExists(testSubject: string, wrapper?: WebElementWrapper) { + this.log.debug(`panelActionExists(${testSubject})`); + return wrapper + ? await this.testSubjects.descendantExists(testSubject, wrapper) + : await this.testSubjects.exists(testSubject, { allowHidden: true }); + } + + async panelActionExistsByTitle(testSubject: string, title = '') { + this.log.debug(`panelActionExists(${testSubject}) on "${title}"`); + const wrapper = await this.getPanelWrapper(title); + return await this.panelActionExists(testSubject, wrapper); + } + async expectExistsPanelAction(testSubject: string, title = '') { this.log.debug('expectExistsPanelAction', testSubject, title); - const panelWrapper = await this.getPanelHeading(title); - await this.openContextMenu(panelWrapper); - if (!(await this.testSubjects.exists(testSubject, { timeout: 1000 }))) { - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); - } - await this.testSubjects.existOrFail(testSubject, { timeout: 1000 }); + const wrapper = await this.getPanelWrapper(title); + + const exists = await this.panelActionExists(testSubject, wrapper); + + if (!exists) { + await this.openContextMenu(wrapper); + await this.testSubjects.existOrFail(testSubject, { allowHidden: true }); + await this.toggleContextMenu(wrapper); } - await this.toggleContextMenu(panelWrapper); } async expectExistsRemovePanelAction(title = '') { @@ -324,15 +327,16 @@ export class DashboardPanelActionsService extends FtrService { } async expectMissingPanelAction(testSubject: string, title = '') { - this.log.debug(`expectMissingPanelAction(${title})`, testSubject); - const panelWrapper = await this.getPanelHeading(title); - await this.openContextMenu(panelWrapper); - await this.testSubjects.missingOrFail(testSubject); - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); + this.log.debug('expectMissingPanelAction', testSubject, title); + const wrapper = await this.getPanelWrapper(title); + + const exists = await this.panelActionExists(testSubject, wrapper); + + if (!exists) { + await this.openContextMenu(wrapper); await this.testSubjects.missingOrFail(testSubject); + await this.toggleContextMenu(wrapper); } - await this.toggleContextMenu(panelWrapper); } async expectMissingEditPanelAction(title = '') { @@ -352,10 +356,21 @@ export class DashboardPanelActionsService extends FtrService { async getPanelHeading(title = '') { this.log.debug(`getPanelHeading(${title})`); - if (!title) return await this.find.byClassName('embPanel__header'); + if (!title) return await this.find.byClassName('embPanel__wrapper'); return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); } + async getPanelWrapper(title = '') { + this.log.debug(`getPanelWrapper(${title})`); + if (!title) return await this.find.byClassName('embPanel__hoverActionsAnchor'); + return await this.testSubjects.find(`embeddablePanelHoverActions-${title.replace(/\s/g, '')}`); + } + + async getPanelWrapperById(embeddableId: string) { + this.log.debug(`getPanelWrapperById(${embeddableId})`); + return await this.find.byCssSelector(`[data-test-embeddable-id="${embeddableId}"]`); + } + async getActionWebElementByText(text: string): Promise { this.log.debug(`getActionWebElement: "${text}"`); const menu = await this.testSubjects.find('multipleActionsContextMenu'); @@ -370,28 +385,23 @@ export class DashboardPanelActionsService extends FtrService { throw new Error(`No action matching text "${text}"`); } - async canConvertToLens(parent?: WebElementWrapper) { + async canConvertToLens(wrapper?: WebElementWrapper) { this.log.debug('canConvertToLens'); - await this.openContextMenu(parent); - const isActionVisible = await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ); - if (!isActionVisible) await this.clickContextMenuMoreItem(); - return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 1000 }); + await this.openContextMenu(wrapper); + return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 500 }); } async canConvertToLensByTitle(title = '') { this.log.debug(`canConvertToLens(${title})`); - const header = await this.getPanelHeading(title); - await this.openContextMenu(header); - const isActionVisible = await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ); - if (!isActionVisible) await this.clickContextMenuMoreItem(); - return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 1000 }); + const wrapper = await this.getPanelWrapper(title); + return await this.canConvertToLens(wrapper); } - async convertToLens(parent?: WebElementWrapper) { + async convertToLens(wrapper?: WebElementWrapper) { this.log.debug('convertToLens'); await this.retry.try(async () => { - if (!(await this.canConvertToLens(parent))) { + if (!(await this.canConvertToLens(wrapper))) { throw new Error('Convert to Lens option not found'); } @@ -401,29 +411,31 @@ export class DashboardPanelActionsService extends FtrService { async convertToLensByTitle(title = '') { this.log.debug(`convertToLens(${title})`); - const header = await this.getPanelHeading(title); - return await this.convertToLens(header); + const wrapper = await this.getPanelWrapper(title); + return await this.convertToLens(wrapper); } - public async expectLinkedToLibrary(title = '', legacy?: boolean) { + async expectLinkedToLibrary(title = '', legacy?: boolean) { this.log.debug(`expectLinkedToLibrary(${title})`); + const isViewMode = await this.dashboard.getIsInViewMode(); + if (isViewMode) await this.dashboard.switchToEditMode(); if (legacy) { await this.expectExistsPanelAction(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title); } else { await this.expectExistsPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title); } - await this.expectMissingPanelAction(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, title); - await this.expectMissingPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title); + if (isViewMode) await this.dashboard.clickCancelOutOfEditMode(); } - public async expectNotLinkedToLibrary(title = '', legacy?: boolean) { + async expectNotLinkedToLibrary(title = '', legacy?: boolean) { this.log.debug(`expectNotLinkedToLibrary(${title})`); + const isViewMode = await this.dashboard.getIsInViewMode(); + if (isViewMode) await this.dashboard.switchToEditMode(); if (legacy) { await this.expectExistsPanelAction(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, title); } else { await this.expectExistsPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title); } - await this.expectMissingPanelAction(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title); - await this.expectMissingPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title); + if (isViewMode) await this.dashboard.clickCancelOutOfEditMode(); } } diff --git a/test/functional/services/dashboard/panel_drilldown_actions.ts b/test/functional/services/dashboard/panel_drilldown_actions.ts index 7c2e0278bc8e9e..8dad803a114a20 100644 --- a/test/functional/services/dashboard/panel_drilldown_actions.ts +++ b/test/functional/services/dashboard/panel_drilldown_actions.ts @@ -37,7 +37,7 @@ export function DashboardDrilldownPanelActionsProvider({ async clickCreateDrilldown() { log.debug('clickCreateDrilldown'); await this.expectExistsCreateDrilldownAction(); - await dashboardPanelActions.clickContextMenuItem(CREATE_DRILLDOWN_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(CREATE_DRILLDOWN_DATA_TEST_SUBJ); } async expectExistsManageDrilldownsAction() { @@ -52,7 +52,7 @@ export function DashboardDrilldownPanelActionsProvider({ async clickManageDrilldowns() { log.debug('clickManageDrilldowns'); - await dashboardPanelActions.clickContextMenuItem(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); } async expectMultipleActionsMenuOpened() { @@ -93,14 +93,13 @@ export function DashboardDrilldownPanelActionsProvider({ async getPanelDrilldownCount(panelIndex = 0): Promise { log.debug('getPanelDrilldownCount'); const panel = (await dashboard.getDashboardPanels())[panelIndex]; - await dashboardPanelActions.openContextMenu(panel); try { const exists = await testSubjects.exists(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ, { timeout: 500, }); if (!exists) { - await dashboardPanelActions.clickContextMenuMoreItem(); + await dashboardPanelActions.openContextMenu(panel); if (!(await testSubjects.exists(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ, { timeout: 500 }))) { return 0; } diff --git a/test/plugin_functional/test_suites/panel_actions/panel_actions.ts b/test/plugin_functional/test_suites/panel_actions/panel_actions.ts index 8db65c13b57b6d..186f91ba26944c 100644 --- a/test/plugin_functional/test_suites/panel_actions/panel_actions.ts +++ b/test/plugin_functional/test_suites/panel_actions/panel_actions.ts @@ -23,10 +23,6 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide it('allows to register links into the context menu', async () => { await dashboardPanelActions.openContextMenu(); - const actionExists = await testSubjects.exists('embeddablePanelAction-samplePanelLink'); - if (!actionExists) { - await dashboardPanelActions.clickContextMenuMoreItem(); - } const actionElement = await testSubjects.find('embeddablePanelAction-samplePanelLink'); const actionElementTag = await actionElement.getTagName(); expect(actionElementTag).to.be('a'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts index e54cfd503e1973..92376abbc72467 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts @@ -66,7 +66,7 @@ export function containerStyle(): ExpressionFunctionDefinition< types: ['string'], help: argHelp.overflow, options: Object.values(Overflow), - default: 'hidden', + default: 'visible', }, padding: { types: ['string'], diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js index 223b8532d8a56c..7b1884215a20a7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/container_style.test.js @@ -140,9 +140,9 @@ describe('containerStyle', () => { result = fn(null, { overflow: 'hidden' }); expect(result).toHaveProperty('overflow', 'hidden'); }); - it(`defaults to 'hidden'`, () => { + it(`defaults to 'visible'`, () => { const result = fn(null); - expect(result).toHaveProperty('overflow', 'hidden'); + expect(result).toHaveProperty('overflow', 'visible'); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss index 29888d862db7ce..793cc423d79042 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.scss @@ -3,6 +3,7 @@ outline: none !important; background: none; border-radius: 0 !important; + box-shadow: none; .embPanel__title { margin-bottom: $euiSizeXS; @@ -24,6 +25,15 @@ } } + .embPanel__hoverActionsLeft, .embPanel__hoverActions > .embPanel--dragHandle { + visibility: hidden; + } + + .embPanel--dragHandle:hover { + background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7)); + cursor: move; + } + .euiTable { background: none; } diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.scss b/x-pack/plugins/canvas/public/components/element_content/element_content.scss index d27e759c63ea17..a0bc78749c5197 100644 --- a/x-pack/plugins/canvas/public/components/element_content/element_content.scss +++ b/x-pack/plugins/canvas/public/components/element_content/element_content.scss @@ -1,10 +1,32 @@ .canvasElement { height: 100%; width: 100%; - overflow: hidden; + + .embPanel { + .embPanel__content { + overflow: visible; + } + + .embPanel__hoverActionsLeft, .embPanel__dragHandle { + visibility: hidden; + } + } } .canvasElement__content { height: 100%; width: 100%; } + +.canvas__element--selected { + .embPanel__hoverActionsAnchor { + .embPanel__hoverActionsWrapper { + z-index: $euiZLevel9; + top: -$euiSizeXL; + + .embPanel__hoverActions { + opacity: 1; + } + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.tsx b/x-pack/plugins/canvas/public/components/element_content/element_content.tsx index 1c65c8f9cfa4e2..a6fb2e1af58f1a 100644 --- a/x-pack/plugins/canvas/public/components/element_content/element_content.tsx +++ b/x-pack/plugins/canvas/public/components/element_content/element_content.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { omitBy, isNil } from 'lodash'; +import classNames from 'classnames'; import { css } from '@emotion/react'; import { ExpressionRenderer } from '@kbn/expressions-plugin/common'; @@ -29,6 +30,8 @@ export interface Props { backgroundColor: string; selectElement: () => void; state: string; + selectedElementId: string | null; + id: string; } export const ElementContent = (props: Props) => { @@ -59,7 +62,9 @@ export const ElementContent = (props: Props) => {
diff --git a/x-pack/plugins/canvas/public/components/element_content/index.tsx b/x-pack/plugins/canvas/public/components/element_content/index.tsx index 72ff04cbf20557..e753be8cbc5277 100644 --- a/x-pack/plugins/canvas/public/components/element_content/index.tsx +++ b/x-pack/plugins/canvas/public/components/element_content/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { getSelectedPage, getPageById } from '../../state/selectors/workpad'; +import { getSelectedPage, getPageById, getSelectedElementId } from '../../state/selectors/workpad'; import { ElementContent as Component, Props as ComponentProps } from './element_content'; import { State } from '../../../types'; import { getCanvasExpressionService } from '../../services/canvas_expressions_service'; @@ -16,6 +16,7 @@ export type Props = Omit; export const ElementContent = (props: Props) => { const selectedPageId = useSelector(getSelectedPage); + const selectedElementId = useSelector(getSelectedElementId); const backgroundColor = useSelector((state: State) => getPageById(state, selectedPageId)?.style.background) || ''; const { renderable } = props; @@ -24,5 +25,5 @@ export const ElementContent = (props: Props) => { return renderable ? getCanvasExpressionService().getRenderer(renderable.as) : null; }, [renderable]); - return ; + return ; }; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js index 80b2f0497e89e9..c1a40839530fd0 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js @@ -11,11 +11,12 @@ import { Positionable } from '../positionable'; import { ElementContent } from '../element_content'; export const ElementWrapper = (props) => { - const { renderable, transformMatrix, width, height, state, handlers } = props; + const { renderable, transformMatrix, width, height, state, handlers, id } = props; return ( ({ id: ACTION_ID, type: 'actionButton', + order: 10, + grouping: [{ id: 'cases', order: 6 }], getIconType: () => 'casesApp', getDisplayName: () => ADD_TO_EXISTING_CASE_DISPLAYNAME, isCompatible: async ({ embeddable }) => { diff --git a/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts b/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts index 5db66cce872b1d..dea0c1ace09a79 100644 --- a/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts +++ b/x-pack/plugins/cases/public/components/visualizations/actions/mocks.ts @@ -42,13 +42,13 @@ export const getMockLensApi = ( ({ type: 'lens', getSavedVis: () => {}, - canViewUnderlyingData: () => {}, + canViewUnderlyingData$: new BehaviorSubject(true), getViewUnderlyingDataArgs: () => {}, getFullAttributes: () => { return mockLensAttributes; }, panelTitle: new BehaviorSubject('myPanel'), - hidePanelTitle: new BehaviorSubject('false'), + hidePanelTitle: new BehaviorSubject(false), timeslice$: new BehaviorSubject<[number, number] | undefined>(undefined), timeRange$: new BehaviorSubject({ from, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts index 3fa6586dbb83ab..7d4458d02b556e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts @@ -77,3 +77,5 @@ export const createDrilldownTemplatesFromSiblings = ( }; export const DRILLDOWN_MAX_WIDTH = 500; + +export const DRILLDOWN_ACTION_GROUP = { id: 'drilldown', order: 3 } as const; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index b4d1455290c1ad..36c157470a2f21 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -36,6 +36,7 @@ import React from 'react'; import { StartDependencies } from '../../../../plugin'; import { createDrilldownTemplatesFromSiblings, + DRILLDOWN_ACTION_GROUP, DRILLDOWN_MAX_WIDTH, ensureNestedTriggers, } from '../drilldown_shared'; @@ -62,6 +63,7 @@ export class FlyoutCreateDrilldownAction implements Action public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; public order = 12; + public grouping = [DRILLDOWN_ACTION_GROUP]; constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index ca184c23c9603e..26f5311d5b3253 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -34,6 +34,7 @@ import { MenuItem } from './menu_item'; import { StartDependencies } from '../../../../plugin'; import { createDrilldownTemplatesFromSiblings, + DRILLDOWN_ACTION_GROUP, DRILLDOWN_MAX_WIDTH, ensureNestedTriggers, } from '../drilldown_shared'; @@ -57,6 +58,7 @@ export class FlyoutEditDrilldownAction implements Action { public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; public order = 10; + public grouping = [DRILLDOWN_ACTION_GROUP]; constructor(protected readonly params: FlyoutEditDrilldownParams) {} diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 5ef2a8d2029842..ce86b896d5fa04 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -7,7 +7,7 @@ import { partition, uniqBy } from 'lodash'; import React from 'react'; -import type { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; @@ -1034,6 +1034,8 @@ export class Embeddable this.activeData = newActiveData; this.renderUserMessages(); + + this.loadViewUnderlyingDataArgs(); }; private onRender: ExpressionWrapperProps['onRender$'] = () => { @@ -1480,7 +1482,7 @@ export class Embeddable } } - private async loadViewUnderlyingDataArgs(): Promise { + private async loadViewUnderlyingDataArgs(): Promise { if ( !this.savedVis || !this.activeData || @@ -1489,13 +1491,15 @@ export class Embeddable !this.activeVisualization || !this.activeVisualizationState ) { - return false; + this.canViewUnderlyingData$.next(false); + return; } const mergedSearchContext = this.getMergedSearchContext(); if (!mergedSearchContext.timeRange) { - return false; + this.canViewUnderlyingData$.next(false); + return; } const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({ @@ -1517,7 +1521,8 @@ export class Embeddable if (loaded) { this.viewUnderlyingDataArgs = viewUnderlyingDataArgs; } - return loaded; + + this.canViewUnderlyingData$.next(loaded); } /** @@ -1529,9 +1534,7 @@ export class Embeddable return this.viewUnderlyingDataArgs; } - public canViewUnderlyingData() { - return this.loadViewUnderlyingDataArgs(); - } + public canViewUnderlyingData$ = new BehaviorSubject(false); async initializeOutput() { if (!this.savedVis) { diff --git a/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts b/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts index 3a03e63ded311b..11b70cd6e7763d 100644 --- a/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts +++ b/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts @@ -10,6 +10,7 @@ import type { HasType, PublishesUnifiedSearch, PublishesPanelTitle, + PublishingSubject, } from '@kbn/presentation-publishing'; import { apiIsOfType, @@ -20,7 +21,7 @@ import { LensSavedObjectAttributes, ViewUnderlyingDataArgs } from '../embeddable export type HasLensConfig = HasType<'lens'> & { getSavedVis: () => Readonly; - canViewUnderlyingData: () => Promise; + canViewUnderlyingData$: PublishingSubject; getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs; getFullAttributes: () => LensSavedObjectAttributes | undefined; }; @@ -35,7 +36,7 @@ export const isLensApi = (api: unknown): api is LensApi => { api && apiIsOfType(api, 'lens') && typeof (api as HasLensConfig).getSavedVis === 'function' && - typeof (api as HasLensConfig).canViewUnderlyingData === 'function' && + (api as HasLensConfig).canViewUnderlyingData$ && typeof (api as HasLensConfig).getViewUnderlyingDataArgs === 'function' && typeof (api as HasLensConfig).getFullAttributes === 'function' && apiPublishesPanelTitle(api) && diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts index 45dc8cbe32898f..fd1ef4f746c41a 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.test.ts @@ -22,7 +22,7 @@ describe('open in discover action', () => { query$: new BehaviorSubject({ query: 'test', language: 'kuery' }), timeRange$: new BehaviorSubject({ from: 'now-15m', to: 'now' }), getSavedVis: jest.fn(() => undefined), - canViewUnderlyingData: () => Promise.resolve(true), + canViewUnderlyingData$: new BehaviorSubject(true), getFullAttributes: jest.fn(() => undefined), getViewUnderlyingDataArgs: jest.fn(() => ({ dataViewSpec: { id: 'index-pattern-id' }, @@ -78,8 +78,7 @@ describe('open in discover action', () => { // setup const embeddable = { ...compatibleEmbeddableApi, - canViewUnderlyingData: jest.fn(() => Promise.resolve(false)), - getViewUnderlyingDataArgs: jest.fn(() => undefined), + canViewUnderlyingData$: { getValue: jest.fn(() => false) }, }; // test false @@ -93,10 +92,11 @@ describe('open in discover action', () => { } as ActionExecutionContext) ).toBeFalsy(); - expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1); + expect(embeddable.canViewUnderlyingData$.getValue).toHaveBeenCalledTimes(1); // test true - embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(true)); + embeddable.canViewUnderlyingData$.getValue = jest.fn(() => true); + expect( await createOpenInDiscoverAction( {} as DiscoverAppLocator, @@ -107,7 +107,7 @@ describe('open in discover action', () => { } as ActionExecutionContext) ).toBeTruthy(); - expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1); + expect(embeddable.canViewUnderlyingData$.getValue).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts index 9b48c41e418562..d9dccab616d5bf 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_action.ts @@ -6,10 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { createAction } from '@kbn/ui-actions-plugin/public'; +import { Action, createAction, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; import type { DataViewsService } from '@kbn/data-views-plugin/public'; import type { DiscoverAppLocator } from './open_in_discover_helpers'; +import { LensApi } from '../embeddable'; const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER'; @@ -19,12 +20,12 @@ export const createOpenInDiscoverAction = ( locator: DiscoverAppLocator, dataViews: Pick, hasDiscoverAccess: boolean -) => - createAction({ +) => { + const actionDefinition = { type: ACTION_OPEN_IN_DISCOVER, id: ACTION_OPEN_IN_DISCOVER, - order: 19, // right after Inspect which is 20 - getIconType: () => 'popout', + order: 20, // right before Inspect which is 19 + getIconType: () => 'discoverApp', getDisplayName: () => i18n.translate('xpack.lens.action.exploreInDiscover', { defaultMessage: 'Explore in Discover', @@ -47,8 +48,26 @@ export const createOpenInDiscoverAction = ( embeddable: context.embeddable, }); }, + couldBecomeCompatible: ({ embeddable }: EmbeddableApiContext) => { + if (!typeof (embeddable as LensApi).canViewUnderlyingData$) + throw new IncompatibleActionError(); + return hasDiscoverAccess && Boolean((embeddable as LensApi).canViewUnderlyingData$); + }, + subscribeToCompatibilityChanges: ( + { embeddable }: EmbeddableApiContext, + onChange: (isCompatible: boolean, action: Action) => void + ) => { + if (!typeof (embeddable as LensApi).canViewUnderlyingData$) + throw new IncompatibleActionError(); + return (embeddable as LensApi).canViewUnderlyingData$.subscribe((canViewUnderlyingData) => { + onChange(canViewUnderlyingData, actionDefinition); + }); + }, execute: async (context: EmbeddableApiContext) => { const { execute } = await getDiscoverHelpersAsync(); return execute({ ...context, locator, dataViews, hasDiscoverAccess }); }, - }); + }; + + return createAction(actionDefinition); +}; diff --git a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts index 0276674767120a..0a52ea6b4711fb 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_in_discover_helpers.ts @@ -31,10 +31,10 @@ type Context = EmbeddableApiContext & { timeFieldName?: string; }; -export async function isCompatible({ hasDiscoverAccess, embeddable }: Context) { +export function isCompatible({ hasDiscoverAccess, embeddable }: Context) { if (!hasDiscoverAccess) return false; try { - return isLensApi(embeddable) && (await embeddable.canViewUnderlyingData()); + return isLensApi(embeddable) && embeddable.canViewUnderlyingData$.getValue(); } catch (e) { // Fetching underlying data failed, log the error and behave as if the action is not compatible // eslint-disable-next-line no-console diff --git a/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx b/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx index 84f053aafcaf57..6091eccbe28ad6 100644 --- a/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/open_vis_in_ml_action.tsx @@ -23,6 +23,8 @@ export function createVisToADJobAction( return { id: 'create-ml-ad-job-action', type: CREATE_LENS_VIS_TO_ML_AD_JOB_ACTION, + order: 8, + grouping: [{ id: 'ml', order: 3 }], getIconType(context): string { return 'machineLearningApp'; }, diff --git a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/use_actions.ts b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/use_actions.ts index cf24d50473467d..21feb23a9ca9aa 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/use_actions.ts +++ b/x-pack/plugins/observability_solution/exploratory_view/public/components/shared/exploratory_view/embeddable/use_actions.ts @@ -190,5 +190,6 @@ const getAddToCaseAction = ({ callback }: { callback: () => void }): Action => { return; }, order: 48, + grouping: [{ id: 'observability', order: 5 }], }; }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 2c76d6c9573191..ee89bfc78b6428 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5983,7 +5983,6 @@ "presentationPanel.contextMenu.ariaLabel": "Options de panneau", "presentationPanel.contextMenu.ariaLabelWithIndex": "Options pour le panneau {index}", "presentationPanel.contextMenu.ariaLabelWithTitle": "Options de panneau pour {title}", - "presentationPanel.contextMenu.loadingTitle": "Options", "presentationPanel.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau", "presentationPanel.contextMenuTrigger.title": "Menu contextuel", "presentationPanel.emptyErrorMessage": "Erreur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b7b13e022bb150..5d18d2518d4e83 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5737,7 +5737,6 @@ "presentationPanel.contextMenu.ariaLabel": "パネルオプション", "presentationPanel.contextMenu.ariaLabelWithIndex": "パネル{index}のオプション", "presentationPanel.contextMenu.ariaLabelWithTitle": "{title} のパネルオプション", - "presentationPanel.contextMenu.loadingTitle": "オプション", "presentationPanel.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます", "presentationPanel.contextMenuTrigger.title": "コンテキストメニュー", "presentationPanel.emptyErrorMessage": "エラー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 47bbff06074b2b..7d1682a99f5fc2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5750,7 +5750,6 @@ "presentationPanel.contextMenu.ariaLabel": "面板选项", "presentationPanel.contextMenu.ariaLabelWithIndex": "面板 {index} 的选项", "presentationPanel.contextMenu.ariaLabelWithTitle": "{title} 的面板选项", - "presentationPanel.contextMenu.loadingTitle": "选项", "presentationPanel.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单", "presentationPanel.contextMenuTrigger.title": "上下文菜单", "presentationPanel.emptyErrorMessage": "错误", diff --git a/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts index 92c558f17dadc4..9ef299ca1cf2dd 100644 --- a/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -18,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Failing: See https://github.com/elastic/kibana/issues/147667 describe.skip('Dashboard panel options a11y tests', () => { - let header: WebElementWrapper; + const title = '[Flights] Flight count'; before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, @@ -28,7 +27,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); - header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count'); }); after(async () => { @@ -40,13 +38,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // dashboard panel options in view mode it('dashboard panel - open menu', async () => { - await dashboardPanelActions.toggleContextMenu(header); + await dashboardPanelActions.toggleContextMenuByTitle(title); await a11y.testAppSnapshot(); - await dashboardPanelActions.toggleContextMenu(header); + await dashboardPanelActions.toggleContextMenuByTitle(title); }); it('dashboard panel - customize time range', async () => { - await dashboardPanelActions.toggleContextMenu(header); + await dashboardPanelActions.toggleContextMenuByTitle(title); await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); await a11y.testAppSnapshot(); await testSubjects.click('cancelPerPanelTimeRangeButton'); @@ -79,21 +77,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await inspector.close(); }); - it('dashboard panel- more options in view mode', async () => { - await dashboardPanelActions.openContextMenuMorePanel(header); - await a11y.testAppSnapshot(); - }); - it('dashboard panel - maximize', async () => { - await dashboardPanelActions.openContextMenuMorePanel(header); await dashboardPanelActions.clickExpandPanelToggle(); await a11y.testAppSnapshot(); - await dashboardPanelActions.openContextMenuMorePanel(header); await dashboardPanelActions.clickExpandPanelToggle(); }); it('dashboard panel - copy to dashboard', async () => { - await dashboardPanelActions.openContextMenuMorePanel(header); + await dashboardPanelActions.openContextMenuByTitle(title); await testSubjects.click('embeddablePanelAction-copyToDashboard'); await a11y.testAppSnapshot(); await testSubjects.click('cancelCopyToButton'); @@ -103,14 +94,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dashboard panel - clone panel', async () => { await testSubjects.click('dashboardEditMode'); - await dashboardPanelActions.toggleContextMenu(header); + await dashboardPanelActions.openContextMenuByTitle(title); await testSubjects.click('embeddablePanelAction-clonePanel'); await toasts.dismissAll(); await a11y.testAppSnapshot(); }); it('dashboard panel - edit panel title', async () => { - await dashboardPanelActions.toggleContextMenu(header); await dashboardPanelActions.customizePanel(); await a11y.testAppSnapshot(); await testSubjects.click('customEmbeddablePanelHideTitleSwitch'); @@ -120,8 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel - Create drilldown panel', async () => { - await dashboardPanelActions.toggleContextMenu(header); - await testSubjects.click('embeddablePanelMore-mainMenu'); + await dashboardPanelActions.openContextMenuByTitle(title); await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); await a11y.testAppSnapshot(); await testSubjects.click('actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN'); @@ -136,30 +125,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel - manage drilldown', async () => { - await dashboardPanelActions.toggleContextMenu(header); - await testSubjects.click('embeddablePanelMore-mainMenu'); + await dashboardPanelActions.openContextMenuByTitle(title); await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_EDIT_DRILLDOWN'); await a11y.testAppSnapshot(); await testSubjects.click('euiFlyoutCloseButton'); }); - it('dashboard panel - more options in edit view', async () => { - await dashboardPanelActions.openContextMenuMorePanel(header); - await a11y.testAppSnapshot(); - }); - it('dashboard panel - save to library', async () => { - await dashboardPanelActions.openContextMenuMorePanel(header); - await testSubjects.click('embeddablePanelAction-saveToLibrary'); + await dashboardPanelActions.legacySaveToLibrary('', title); await a11y.testAppSnapshot(); await testSubjects.click('saveCancelButton'); }); - - it('dashboard panel - replace panel', async () => { - await dashboardPanelActions.openContextMenuMorePanel(header); - await testSubjects.click('embeddablePanelAction-replacePanel'); - await a11y.testAppSnapshot(); - await testSubjects.click('euiFlyoutCloseButton'); - }); }); } diff --git a/x-pack/test/functional/apps/canvas/embeddables/lens.ts b/x-pack/test/functional/apps/canvas/embeddables/lens.ts index 2bd2ec820b6f3c..ebd85a0ab27202 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/lens.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/lens.ts @@ -34,32 +34,8 @@ export default function canvasLensTest({ getService, getPageObjects }: FtrProvid await kibanaServer.savedObjects.cleanStandardList(); }); - describe('by-reference', () => { - it('adds existing lens embeddable from the visualize library', async () => { - await canvas.clickAddFromLibrary(); - await dashboardAddPanel.addEmbeddable('Artistpreviouslyknownaslens', 'lens'); - await testSubjects.existOrFail('embeddablePanelHeading-Artistpreviouslyknownaslens'); - }); - - it('edits lens by-reference embeddable', async () => { - await dashboardPanelActions.editPanelByTitle('Artistpreviouslyknownaslens'); - await lens.save('Artistpreviouslyknownaslens v2', false, true); - await testSubjects.existOrFail('embeddablePanelHeading-Artistpreviouslyknownaslensv2'); - }); - - it('renders lens visualization using savedLens expression', async () => { - // load test workpad - await canvas.goToListingPage(); - await canvas.loadFirstWorkpad('Test Workpad'); - await header.waitUntilLoadingHasFinished(); - - await lens.assertLegacyMetric('Maximum of bytes', '16,788'); - }); - }); - describe('by-value', () => { it('creates new lens embeddable', async () => { - await canvas.addNewPage(); await canvas.createNewVis('lens'); await lens.goToTimeRange(); await lens.configureDimension({ @@ -79,8 +55,6 @@ export default function canvasLensTest({ getService, getPageObjects }: FtrProvid it('edits lens by-value embeddable', async () => { await header.waitUntilLoadingHasFinished(); - const panelHeader = await testSubjects.find('embeddablePanelHeading-'); - await dashboardPanelActions.openContextMenu(panelHeader); await dashboardPanelActions.clickEdit(); await lens.saveAndReturn(); await header.waitUntilLoadingHasFinished(); @@ -88,8 +62,34 @@ export default function canvasLensTest({ getService, getPageObjects }: FtrProvid }); }); + describe('by-reference', () => { + it('adds existing lens embeddable from the visualize library', async () => { + await canvas.goToListingPageViaBreadcrumbs(); + await canvas.createNewWorkpad(); + await canvas.clickAddFromLibrary(); + await dashboardAddPanel.addEmbeddable('Artistpreviouslyknownaslens', 'lens'); + await testSubjects.existOrFail('embeddablePanelHeading-Artistpreviouslyknownaslens'); + }); + + it('edits lens by-reference embeddable', async () => { + await dashboardPanelActions.editPanelByTitle('Artistpreviouslyknownaslens'); + await lens.save('Artistpreviouslyknownaslens v2', false, true); + await testSubjects.existOrFail('embeddablePanelHeading-Artistpreviouslyknownaslensv2'); + }); + + it('renders lens visualization using savedLens expression', async () => { + // load test workpad + await canvas.goToListingPage(); + await canvas.loadFirstWorkpad('Test Workpad'); + await header.waitUntilLoadingHasFinished(); + + await lens.assertLegacyMetric('Maximum of bytes', '16,788'); + }); + }); + describe('switch page smoke test', () => { it('loads embeddables on page change', async () => { + await canvas.addNewPage(); await canvas.goToPreviousPage(); await header.waitUntilLoadingHasFinished(); await lens.assertLegacyMetric('Maximum of bytes', '16,788'); diff --git a/x-pack/test/functional/apps/canvas/embeddables/maps.ts b/x-pack/test/functional/apps/canvas/embeddables/maps.ts index 2a63c4f64b57a7..ac6a861e9796ea 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/maps.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/maps.ts @@ -36,7 +36,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('edits map by-value embeddable', async () => { const originalEmbeddableCount = await canvas.getEmbeddableCount(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await maps.saveMap('canvas test map'); const embeddableCount = await canvas.getEmbeddableCount(); diff --git a/x-pack/test/functional/apps/canvas/embeddables/visualization.ts b/x-pack/test/functional/apps/canvas/embeddables/visualization.ts index 9cb0d55371f725..7de1ef28a43a17 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/visualization.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/visualization.ts @@ -71,7 +71,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('edits tsvb by-value embeddable', async () => { const originalEmbeddableCount = await canvas.getEmbeddableCount(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await visualize.saveVisualizationAndReturn(); await retry.try(async () => { @@ -93,7 +92,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('edits vega by-value embeddable', async () => { const originalEmbeddableCount = await canvas.getEmbeddableCount(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await visualize.saveVisualizationAndReturn(); await retry.try(async () => { diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts index 3e648f5000945a..a974eb8c1284b1 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_lens_by_value.ts @@ -47,7 +47,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('edits to a by value lens panel are properly applied', async () => { await dashboard.waitForRenderComplete(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await lens.switchToVisualization('pie'); await lens.saveAndReturn(); @@ -60,7 +59,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('editing and saving a lens by value panel retains number of panels', async () => { const originalPanelCount = await dashboard.getPanelCount(); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await lens.switchToVisualization('treemap'); await lens.saveAndReturn(); @@ -73,7 +71,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const newTitle = 'look out library, here I come!'; const originalPanelCount = await dashboard.getPanelCount(); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await lens.save(newTitle, false, true); await dashboard.waitForRenderComplete(); diff --git a/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts index a55c3c3c0433c6..4c890b41e0612b 100644 --- a/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts +++ b/x-pack/test/functional/apps/dashboard/group2/dashboard_maps_by_value.ts @@ -42,7 +42,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await dashboard.switchToEditMode(); } - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await maps.clickAddLayer(); await maps.selectLayerGroupCard(); diff --git a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts index c3d5bfce6e6210..7d8456a9e81a8d 100644 --- a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const { dashboard, lens } = getPageObjects(['dashboard', 'lens']); - const EMPTY_TITLE = '[No Title]'; + const EMPTY_TITLE = undefined; describe('panel titles', () => { before(async () => { @@ -112,7 +112,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('resetting description on a by reference panel sets it to the library title', async () => { - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.navigateToEditorFromFlyout(); // legacySaveToLibrary UI cannot set description await lens.save( diff --git a/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts index ca6f23d09e3756..050483c98ac7bb 100644 --- a/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts @@ -50,11 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('action exists in panel context menu', async () => { await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); - await panelActions.openContextMenu(); - await testSubjects.existOrFail(ACTION_TEST_SUBJ); + await panelActions.expectExistsPanelAction(ACTION_TEST_SUBJ); }); it('is a link element', async () => { + await panelActions.openContextMenuByTitle('Visualization PieChart'); const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); const tag = await actionElement.getTagName(); @@ -87,8 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { exitFromEditMode: true, }); - await panelActions.openContextMenu(); - await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ); + await panelActions.clickPanelAction(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const text = await timePicker.getShowDatesButtonText(); diff --git a/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts index 2cf3f91c6a38e6..ea2a66028ada50 100644 --- a/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; import { FtrProviderContext } from '../../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -17,7 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const reportingService = getService('reporting'); const dashboardAddPanel = getService('dashboardAddPanel'); const filterBar = getService('filterBar'); - const find = getService('find'); const retry = getService('retry'); const toasts = getService('toasts'); const { reporting, common, dashboard, timePicker } = getPageObjects([ @@ -45,14 +45,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return res.text; }; - const clickActionsMenu = async (headingTestSubj: string) => { - const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-' + headingTestSubj); - await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + const clickDownloadCsv = async (wrapper?: WebElementWrapper) => { + log.debug('click "Generate CSV"'); + await dashboardPanelActions.clickPanelAction( + 'embeddablePanelAction-generateCsvReport', + wrapper + ); + await testSubjects.existOrFail('csvReportStarted'); // validate toast panel }; - const clickDownloadCsv = async () => { - log.debug('click "Generate CSV"'); - await dashboardPanelActions.clickContextMenuItem('embeddablePanelAction-generateCsvReport'); + const clickDownloadCsvByTitle = async (title?: string) => { + log.debug(`click "Generate CSV" on "${title}"`); + await dashboardPanelActions.clickPanelActionByTitle( + 'embeddablePanelAction-generateCsvReport', + title + ); await testSubjects.existOrFail('csvReportStarted'); // validate toast panel }; @@ -82,8 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Generate CSV export of a saved search panel', async function () { await dashboard.loadSavedDashboard('Ecom Dashboard - 3 Day Period'); - await clickActionsMenu('EcommerceData'); - await clickDownloadCsv(); + await clickDownloadCsvByTitle('EcommerceData'); const csvFile = await getCsvReportData(); expect(csvFile.length).to.be(76137); @@ -95,9 +101,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // add a filter await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); - - await clickActionsMenu('EcommerceData'); - await clickDownloadCsv(); + await clickDownloadCsvByTitle('EcommerceData'); const csvFile = await getCsvReportData(); expect(csvFile.length).to.be(17106); @@ -106,9 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Downloads a saved search panel with a custom time range that does not intersect with dashboard time range', async function () { await dashboard.loadSavedDashboard('Ecom Dashboard - 3 Day Period - custom time range'); - - await clickActionsMenu('EcommerceData'); - await clickDownloadCsv(); + await clickDownloadCsvByTitle('EcommerceData'); const csvFile = await getCsvReportData(); expect(csvFile.length).to.be(23277); @@ -117,12 +119,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Gets the correct filename if panel titles are hidden', async () => { await dashboard.loadSavedDashboard('Ecom Dashboard Hidden Panel Titles'); - const savedSearchPanel = await find.byCssSelector( - '[data-test-embeddable-id="94eab06f-60ac-4a85-b771-3a8ed475c9bb"]' + const savedSearchPanel = await dashboardPanelActions.getPanelWrapperById( + '94eab06f-60ac-4a85-b771-3a8ed475c9bb' ); // panel title is hidden - await dashboardPanelActions.toggleContextMenu(savedSearchPanel); - await clickDownloadCsv(); + await clickDownloadCsv(savedSearchPanel); await testSubjects.existOrFail('csvReportStarted'); const csvFile = await getCsvReportData(); @@ -158,8 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Downloads filtered Discover saved search report', async () => { - await clickActionsMenu(TEST_SEARCH_TITLE.replace(/ /g, '')); - await clickDownloadCsv(); + await clickDownloadCsvByTitle(TEST_SEARCH_TITLE); const csvFile = await getCsvReportData(); expect(csvFile.length).to.be(2446); @@ -196,8 +196,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Generate CSV export of a saved search panel', async () => { - await clickActionsMenu('namessearch'); - await clickDownloadCsv(); + await clickDownloadCsvByTitle('namessearch'); const csvFile = await getCsvReportData(); expect(csvFile.length).to.be(166); diff --git a/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/sample_data_ecommerce_76.png b/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/sample_data_ecommerce_76.png index 1ce272bd4a86fa..e188f1c6f4c1cb 100644 Binary files a/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/sample_data_ecommerce_76.png and b/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/sample_data_ecommerce_76.png differ diff --git a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts index 38f66db31dc92b..b7c4cbdddd5fa2 100644 --- a/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts +++ b/x-pack/test/functional/apps/lens/group1/ad_hoc_data_view.ts @@ -54,9 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } const checkDiscoverNavigationResult = async () => { - await dashboardPanelActions.clickContextMenuItem( - 'embeddablePanelAction-ACTION_OPEN_IN_DISCOVER' - ); + await dashboardPanelActions.clickPanelAction('embeddablePanelAction-ACTION_OPEN_IN_DISCOVER'); const [, discoverHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverHandle); diff --git a/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts b/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts index 2d92d98dc36061..312bddba10eacd 100644 --- a/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts +++ b/x-pack/test/functional/apps/lens/group3/dashboard_inline_editing.ts @@ -49,7 +49,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await lens.save('New Lens from Modal', false, false, false, 'new'); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Adds a secondary dimension'); @@ -90,7 +89,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.legacySaveToLibrary('My by reference visualization'); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Removes breakdown dimension'); @@ -110,7 +108,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await lens.save('New Lens from Modal', false, false, false, 'new'); await dashboard.waitForRenderComplete(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Adds a secondary dimension'); @@ -150,7 +147,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await elasticChart.setNewChartUiDebugFlag(true); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Adds annotation'); @@ -177,7 +173,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await elasticChart.setNewChartUiDebugFlag(true); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Adds reference line'); diff --git a/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts index 70625ecafaa75a..de563366af3fb6 100644 --- a/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts @@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { exitFromEditMode: true, }); - await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); @@ -59,7 +59,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should show the open button for a compatible saved visualization with annotations and reference line', async () => { await dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await header.waitUntilLoadingHasFinished(); await lens.createLayer('annotations'); @@ -73,7 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { exitFromEditMode: true, }); - await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); @@ -90,7 +89,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should bring both dashboard context and visualization context to discover', async () => { await dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await savedQueryManagementComponent.openSavedQueryManagementComponent(); await queryBar.switchQueryLanguage('lucene'); @@ -119,7 +117,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.expectExistsPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); await dashboard.clickCancelOutOfEditMode(); - await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); diff --git a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts index 7c3c14150d2b2c..56f97c8751d77c 100644 --- a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts @@ -97,7 +97,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('retains its saved object tags after save and return', async () => { - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await lens.saveAndReturn(); await header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts index 593f42c14db4a8..bf799673c2491f 100644 --- a/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts +++ b/x-pack/test/functional/apps/lens/open_in_lens/tsvb/dashboard.ts @@ -53,14 +53,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await dashboardCustomizePanel.clickSaveButton(); await dashboard.waitForRenderComplete(); await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); - await panelActions.openContextMenu(); - const editInLensExists = await testSubjects.exists( - 'embeddablePanelAction-ACTION_EDIT_IN_LENS' - ); - if (!editInLensExists) { - await testSubjects.click('embeddablePanelMore-mainMenu'); - } - await testSubjects.click('embeddablePanelAction-ACTION_EDIT_IN_LENS'); + await panelActions.convertToLens(); await lens.waitForVisualization('xyVisChart'); await retry.try(async () => { diff --git a/x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.ts b/x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.ts index 60ef9b8799d3c0..1f5a09d1449345 100644 --- a/x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.ts +++ b/x-pack/test/functional/apps/maps/group2/embeddable/filter_by_map_extent.ts @@ -38,7 +38,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should filter dashboard by map extent when "filter by map extent" is enabled', async () => { - await dashboardPanelActions.clickContextMenuItemByTitle( + await dashboardPanelActions.clickPanelActionByTitle( FILTER_BY_MAP_EXTENT_DATA_TEST_SUBJ, 'document example' ); @@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should remove map extent filter dashboard when "filter by map extent" is disabled', async () => { - await dashboardPanelActions.clickContextMenuItemByTitle( + await dashboardPanelActions.clickPanelActionByTitle( FILTER_BY_MAP_EXTENT_DATA_TEST_SUBJ, 'document example' ); diff --git a/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts b/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts index 6c57295a0c353d..9c3b9898824697 100644 --- a/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts +++ b/x-pack/test/functional/apps/saved_query_management/feature_controls/security.ts @@ -77,7 +77,7 @@ export default function (ctx: FtrProviderContext) { break; case 'dashboard': await dashboard.navigateToApp(); - await dashboard.gotoDashboardEditMode('A Dashboard'); + await dashboard.loadSavedDashboard('A Dashboard'); break; case 'maps': await maps.openNewMap(); diff --git a/x-pack/test/functional/services/ml/lens_visualizations.ts b/x-pack/test/functional/services/ml/lens_visualizations.ts index 7bb49e7397e9d9..2b95bfc1bd2c81 100644 --- a/x-pack/test/functional/services/ml/lens_visualizations.ts +++ b/x-pack/test/functional/services/ml/lens_visualizations.ts @@ -18,7 +18,7 @@ export function MachineLearningLensVisualizationsProvider( return { async clickCreateMLJobMenuAction(title = '') { - await dashboardPanelActions.clickContextMenuItemByTitle( + await dashboardPanelActions.clickPanelActionByTitle( 'embeddablePanelAction-create-ml-ad-job-action', title ); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index 146e72ab3698e0..e887f6ee80e382 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -402,7 +402,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await common.navigateToApp('dashboard'); await dashboard.preserveCrossAppState(); await dashboard.loadSavedDashboard(myDashboardName); - await dashboardPanelActions.clickContextMenuItem(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ); await testSubjects.click('cases-table-add-case-filter-bar'); await cases.create.createCase({ @@ -434,7 +434,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await dashboard.preserveCrossAppState(); await dashboard.loadSavedDashboard(myDashboardName); - await dashboardPanelActions.clickContextMenuItem(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ); await testSubjects.click(`cases-table-row-select-${theCase.id}`); diff --git a/x-pack/test/reporting_functional/services/scenarios.ts b/x-pack/test/reporting_functional/services/scenarios.ts index 1b5c23a1f65684..aea50c207dc20d 100644 --- a/x-pack/test/reporting_functional/services/scenarios.ts +++ b/x-pack/test/reporting_functional/services/scenarios.ts @@ -77,7 +77,7 @@ export function createScenarios( }; const tryDashboardGenerateCsvFail = async (savedSearchTitle: string) => { - await dashboardPanelActions.clickContextMenuItemByTitle( + await dashboardPanelActions.clickPanelActionByTitle( GENERATE_CSV_DATA_TEST_SUBJ, savedSearchTitle ); @@ -94,7 +94,7 @@ export function createScenarios( GENERATE_CSV_DATA_TEST_SUBJ, savedSearchTitle ); - await dashboardPanelActions.clickContextMenuItemByTitle( + await dashboardPanelActions.clickPanelActionByTitle( GENERATE_CSV_DATA_TEST_SUBJ, savedSearchTitle ); diff --git a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts index 2942e31ee2f773..b32eafc8c68996 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/dashboard/session_sharing/lens.ts @@ -48,7 +48,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Navigating to lens and back should create a new session const byRefSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); @@ -62,7 +61,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const byValueSessionId = await dashboardPanelActions.getSearchSessionIdByTitle(lensTitle); // Navigating to lens and back should keep the session - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await lens.saveAndReturn(); await dashboard.waitForRenderComplete(); diff --git a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts index ab86b163ef6ee0..de95f3b2a243a0 100644 --- a/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts +++ b/x-pack/test_serverless/functional/test_suites/common/visualizations/group2/open_in_lens/agg_based/gauge.ts @@ -43,13 +43,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should show the "Convert to Lens" menu item', async () => { - const visPanel = await panelActions.getPanelHeading('Gauge - Basic'); - expect(await panelActions.canConvertToLens(visPanel)).to.eql(true); + expect(await panelActions.canConvertToLensByTitle('Gauge - Basic')).to.eql(true); }); it('should convert aggregation with params', async () => { - const visPanel = await panelActions.getPanelHeading('Gauge - Agg with params'); - await panelActions.convertToLens(visPanel); + await panelActions.convertToLensByTitle('Gauge - Agg with params'); await lens.waitForVisualization('gaugeChart'); expect(await lens.getLayerCount()).to.be(1); diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts index c429fef23fdd92..17caa3d6560f26 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/attachment_framework.ts @@ -58,7 +58,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { it('adds lens visualization to a new case', async () => { const caseTitle = 'case created in observability from my dashboard with lens visualization'; - await dashboardPanelActions.clickContextMenuItem(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); await retry.waitFor('wait for the modal to open', async () => { return ( @@ -109,7 +109,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' }); - await dashboardPanelActions.clickContextMenuItem(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); // verify that solution filter is not visible await testSubjects.missingOrFail('options-filter-popover-button-owner'); diff --git a/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts b/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts index 1f13f10d9e948c..8f97f53c6275f5 100644 --- a/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts +++ b/x-pack/test_serverless/functional/test_suites/search/dashboards/build_dashboard.ts @@ -56,7 +56,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('can edit a Lens panel by value and save changes', async () => { await PageObjects.dashboard.waitForRenderComplete(); - await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickEdit(); await PageObjects.lens.switchToVisualization('pie'); await PageObjects.lens.saveAndReturn(); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts index 648309b3d5cabc..d8b43ca1c43011 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/attachment_framework.ts @@ -51,7 +51,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { const caseTitle = 'case created in security solution from my dashboard with lens visualization'; - await dashboardPanelActions.clickContextMenuItem(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); await retry.waitFor('wait for the modal to open', async () => { return ( @@ -108,7 +108,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { await testSubjects.click('edit-unsaved-New-Dashboard'); } - await dashboardPanelActions.clickContextMenuItem(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); // verify that solution filter is not visible await testSubjects.missingOrFail('options-filter-popover-button-owner');