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 423fab660695b..110251e4d759f 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, topOffset: number } options Optional + * @param { xOffset: 0, yOffset: 0 } options * @return {Promise} */ - public async moveMouseTo({ xOffset = 0, yOffset = 0, topOffset = 0 } = {}) { + public async moveMouseTo(options = { xOffset: 0, yOffset: 0 }) { await this.retryCall(async function moveMouseTo(wrapper) { - await wrapper.scrollIntoViewIfNecessary(topOffset); + await wrapper.scrollIntoViewIfNecessary(); await wrapper.getActions().move({ x: 0, y: 0 }).perform(); await wrapper .getActions() - .move({ x: xOffset, y: yOffset, origin: wrapper._webElement }) + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) .perform(); }); } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index aca3a1a0c3c99..b990ce5e534de 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: 68015 + dashboard: 52967 dashboardEnhanced: 65646 data: 454087 dataQuality: 19384 diff --git a/packages/presentation/presentation_publishing/index.ts b/packages/presentation/presentation_publishing/index.ts index 2b96c6d353eee..ab5e396e65727 100644 --- a/packages/presentation/presentation_publishing/index.ts +++ b/packages/presentation/presentation_publishing/index.ts @@ -30,10 +30,6 @@ 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 deleted file mode 100644 index db7a0c5cc8a3b..0000000000000 --- a/packages/presentation/presentation_publishing/interfaces/can_lock_hover_actions.ts +++ /dev/null @@ -1,27 +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 { 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 90da6c3297cbd..acd46f2763bbc 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,7 +39,6 @@ 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'; @@ -64,7 +63,6 @@ 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 4eae444dfecb7..5eec25f1f052b 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -20,7 +20,6 @@ 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'; @@ -42,7 +41,6 @@ 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 10b21fc36edcc..fb31886919773 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,7 +28,6 @@ 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'; @@ -60,7 +59,6 @@ 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 1ebf937e470e5..09bc56ea88586 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,17 +13,15 @@ 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: expandPanelIdSubject, + expandedPanelId: new BehaviorSubject(undefined), }, }, }; @@ -40,22 +38,19 @@ 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'); - expandPanelIdSubject.next('superPanelId'); + context.embeddable.parentApi.expandedPanelId = new BehaviorSubject( + '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'); - expandPanelIdSubject.next('superPanelId'); + context.embeddable.parentApi.expandedPanelId = new BehaviorSubject( + '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 a207d181d26cc..b4f2a06e6895a 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx @@ -16,8 +16,6 @@ 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'; @@ -31,8 +29,7 @@ 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 = 9; - public grouping = [DASHBOARD_ACTION_GROUP]; + public order = 7; public getDisplayName({ embeddable }: EmbeddableApiContext) { if (!isApiCompatible(embeddable)) throw new IncompatibleActionError(); @@ -50,20 +47,6 @@ 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 94dbf9e3087aa..fd55816134ed1 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; + public readonly order = 18; // right after Export in discover which is 19 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 e639168b00c7f..29b0353979073 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,6 +9,7 @@ import { Filter, FilterStateStore, type AggregateQuery, type Query } from '@kbn/es-query'; +import { ViewMode } from '@kbn/presentation-publishing'; import { BehaviorSubject } from 'rxjs'; import { FiltersNotificationAction, @@ -41,6 +42,7 @@ 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); @@ -48,10 +50,14 @@ 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, }, @@ -77,6 +83,22 @@ 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 9662c8956dcc8..854ff5da948f4 100644 --- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_action.tsx @@ -13,15 +13,17 @@ import { merge } from 'rxjs'; import { isOfAggregateQueryType, isOfQueryType } from '@kbn/es-query'; import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; import { - apiPublishesPartialUnifiedSearch, - apiHasUniqueId, + CanAccessViewMode, EmbeddableApiContext, HasParentApi, HasUniqueId, PublishesDataViews, PublishesUnifiedSearch, - CanLockHoverActions, - CanAccessViewMode, + apiCanAccessViewMode, + apiHasUniqueId, + apiPublishesPartialUnifiedSearch, + getInheritedViewMode, + getViewModeSubject, } from '@kbn/presentation-publishing'; import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; @@ -32,16 +34,17 @@ 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) && apiPublishesPartialUnifiedSearch(api)); + Boolean( + apiHasUniqueId(api) && apiCanAccessViewMode(api) && apiPublishesPartialUnifiedSearch(api) + ); const compatibilityCheck = (api: EmbeddableApiContext['embeddable']) => { - if (!isApiCompatible(api)) return false; + if (!isApiCompatible(api) || getInheritedViewMode(api) !== 'edit') return false; const query = api.query$?.value; return ( (api.filters$?.value ?? []).length > 0 || @@ -94,7 +97,9 @@ export class FiltersNotificationAction implements Action { ) { if (!isApiCompatible(embeddable)) return; return merge( - ...[embeddable.query$, embeddable.filters$].filter((value) => Boolean(value)) + ...[embeddable.query$, embeddable.filters$, getViewModeSubject(embeddable)].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 4488a96b52b68..b02443f01aaa8 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,23 +50,18 @@ 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, - }, }; }); @@ -92,15 +87,7 @@ 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(); @@ -117,7 +104,6 @@ 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 5f23b21dc9155..bafd06297fe7e 100644 --- a/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/filters_notification_popover.tsx @@ -26,11 +26,8 @@ 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 { - apiCanLockHoverActions, - getViewModeSubject, - useBatchedOptionalPublishingSubjects, -} from '@kbn/presentation-publishing'; +import { useStateFromPublishingSubject } from '@kbn/presentation-publishing'; +import { BehaviorSubject } from 'rxjs'; import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings'; import { FiltersNotificationActionApi } from './filters_notification_action'; @@ -62,10 +59,8 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc } }, [api, setDisableEditButton]); - const [hasLockedHoverActions, dataViews, parentViewMode] = useBatchedOptionalPublishingSubjects( - api.hasLockedHoverActions$, - api.parentApi?.dataViews, - getViewModeSubject(api ?? undefined) + const dataViews = useStateFromPublishingSubject( + api.parentApi?.dataViews ? api.parentApi.dataViews : new BehaviorSubject(undefined) ); return ( @@ -74,23 +69,13 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc { - setIsPopoverOpen(!isPopoverOpen); - if (apiCanLockHoverActions(api)) { - api?.lockHoverActions(!hasLockedHoverActions); - } - }} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} data-test-subj={`embeddablePanelNotification-${api.uuid}`} aria-label={displayName} /> } isOpen={isPopoverOpen} - closePopover={() => { - setIsPopoverOpen(false); - if (apiCanLockHoverActions(api)) { - api.lockHoverActions(false); - } - }} + closePopover={() => setIsPopoverOpen(false)} anchorPosition="upCenter" > {displayName} @@ -127,8 +112,8 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc )} - {!disableEditbutton && parentViewMode === 'edit' && ( - + + {!disableEditbutton && ( - - )} + )} + ); } diff --git a/src/plugins/dashboard/public/dashboard_actions/index.ts b/src/plugins/dashboard/public/dashboard_actions/index.ts index 1b9f2091fbce6..55a371719d953 100644 --- a/src/plugins/dashboard/public/dashboard_actions/index.ts +++ b/src/plugins/dashboard/public/dashboard_actions/index.ts @@ -24,8 +24,6 @@ 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 6cc46b6af51e3..dee049dc2874e 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,7 +18,6 @@ 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'; @@ -36,7 +35,6 @@ 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 668f02dee3159..96daab215dec6 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,7 +20,6 @@ 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'; @@ -38,7 +37,6 @@ 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 b9f9b3218488b..42345f38d614f 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: 'start' }); + panelRef.scrollIntoView({ block: 'nearest' }); }); }, 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 49a6b01049da7..f6e7918fb1b0b 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: $euiZLevel2; /* 1 */ + z-index: $euiZLevel1; /* 1 */ right: 0; bottom: 0; padding-right: $euiSizeS; @@ -33,10 +33,6 @@ */ .dshLayout-isMaximizedPanel { height: 100% !important; /* 1. */ - - .embPanel__hoverActionsLeft { - visibility: hidden; - } } /** @@ -44,7 +40,8 @@ * Shifting the rendered panels offscreen prevents a quick flash when redrawing the panels on minimize */ .dshDashboardGrid__item--hidden { - transform: translate(-9999px, -9999px); + top: -9999px; + left: -9999px; } /** @@ -101,26 +98,13 @@ */ &.resizing, &.react-draggable-dragging { - z-index: $euiZLevel3 !important; + z-index: $euiZLevel2 !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 93a95e1ef37e5..d54f513a207a4 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,42 +4,24 @@ * .embPanel--editing doesn't get updating without a hard refresh */ -.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; - } - } - } +.dshDashboardGrid__item { + scroll-margin-top: calc((var(--euiFixedHeadersOffset, 100) * 2) + $euiSizeS); + scroll-margin-bottom: $euiSizeS; } // LAYOUT MODES // Adjust borders/etc... for non-spaced out and expanded panels .dshLayout-withoutMargins { - .embPanel, - .embPanel__hoverActionsAnchor { + .embPanel { box-shadow: none; - outline: none; border-radius: 0; } - &.dshLayout--editing { - .embPanel__hoverActionsAnchor:hover { - outline: 1px dashed $euiColorMediumShade; - } - } - - .embPanel__hoverActionsAnchor:hover { - outline: $euiBorderThin; - z-index: $euiZLevel2; + .embPanel__content { + border-radius: 0; } - .embPanel__content, - .dshDashboardGrid__item--highlighted, - .lnsExpressionRenderer { + .dshDashboardGrid__item--highlighted { border-radius: 0; } } @@ -53,20 +35,6 @@ 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 { @@ -84,11 +52,10 @@ } .dshDashboardGrid__item--highlighted { - .embPanel { - border-radius: $euiSizeXS; - animation-name: highlightOutline; - animation-duration: 4s; - animation-timing-function: ease-out; - z-index: $euiZLevel2; - } + 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; } 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 0ef976af51eb6..577661b393c67 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,7 +133,6 @@ 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 9b5a00c628608..7b21db4ea3f84 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,7 +15,6 @@ 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'; @@ -92,21 +91,12 @@ 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; ` - : css` - scroll-margin-top: ${dashboardContainerTopOffset + - globalNavTopOffset + - DASHBOARD_MARGIN_SIZE}px; - `; + : undefined; 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 79e7c16bfe4a7..f0c51724b551b 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,9 +23,3 @@ .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 027d2aee62b15..67c40487fcc93 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 @@ -54,7 +54,6 @@ export const DashboardViewport = () => { viewMode, useMargins, uuid, - fullScreenMode, ] = useBatchedPublishingSubjects( dashboardApi.controlGroupApi$, dashboardApi.panelTitle, @@ -64,8 +63,7 @@ export const DashboardViewport = () => { dashboardApi.panels$, dashboardApi.viewMode, dashboardApi.useMargins$, - dashboardApi.uuid$, - dashboardApi.fullScreenMode$ + dashboardApi.uuid$ ); const onExit = useCallback(() => { dashboardApi.setFullScreenMode(false); @@ -116,7 +114,6 @@ export const DashboardViewport = () => {
{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 0d3f80ae79fec..6b0141a50861d 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: $euiZLevel3; + z-index: $euiZLevel2; 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 d1092a28d9f55..4d77e9dbd4400 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,7 +20,6 @@ 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, @@ -44,7 +43,7 @@ export class ViewSavedSearchAction implements Action { } getIconType(): string | undefined { - return 'discoverApp'; + return 'inspect'; } 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 daab774d7f35d..dab0968af0056 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,8 +245,6 @@ export const legacyEmbeddableToApi = ( return !isInputControl && !isMarkdown && !isImage && !isLinks; }; - const hasLockedHoverActions$ = new BehaviorSubject(false); - return { api: { parentApi: parentApi as LegacyEmbeddableAPI['parentApi'], @@ -272,9 +270,6 @@ 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 9fc3598bcd5ad..89df109be5ef1 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -148,8 +148,6 @@ export abstract class Embeddable< canUnlinkFromLibrary: this.canUnlinkFromLibrary, isCompatibleWithUnifiedSearch: this.isCompatibleWithUnifiedSearch, savedObjectId: this.savedObjectId, - hasLockedHoverActions$: this.hasLockedHoverActions$, - lockHoverActions: this.lockHoverActions, } = api); setTimeout(() => { @@ -193,8 +191,6 @@ 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 57cf7eec6eb95..779c1a235bc82 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -27,7 +27,6 @@ import { PublishesSavedObjectId, HasLegacyLibraryTransforms, EmbeddableAppContext, - CanLockHoverActions, } from '@kbn/presentation-publishing'; import { Observable } from 'rxjs'; import { EmbeddableInput } from '../../../common/types'; @@ -59,8 +58,7 @@ export type LegacyEmbeddableAPI = HasType & Partial & HasParentApi & EmbeddableHasTimeRange & - PublishesSavedObjectId & - CanLockHoverActions; + PublishesSavedObjectId; 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 63433d1d1319b..3722647526c79 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,8 +194,6 @@ 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 c3dc06e198cd8..0f9ae361bbf93 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,16 +122,11 @@ 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 4ba8653310ff0..1ab43d4bb1b7d 100644 --- a/src/plugins/embeddable/public/react_embeddable_system/types.ts +++ b/src/plugins/embeddable/public/react_embeddable_system/types.ts @@ -14,7 +14,6 @@ import { } from '@kbn/presentation-containers'; import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types'; import { - CanLockHoverActions, HasType, PublishesPhaseEvents, PublishesUnsavedChanges, @@ -49,7 +48,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 685f0a6c46a3b..177f2f1c82118 100644 --- a/src/plugins/links/public/embeddable/links_embeddable.tsx +++ b/src/plugins/links/public/embeddable/links_embeddable.tsx @@ -248,7 +248,6 @@ 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 = 19; // right after Explore in Discover which is 20 + public order = 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 335fda267a800..b065ed5cedf59 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,8 +33,14 @@ 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 = 0; - public grouping = [{ id: 'remove_panel_group', order: 1 }]; + public order = 1; + + public grouping = [ + { + id: 'delete_panel_action', + 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 5094cf6b02ba3..434cca42e7c9f 100644 --- a/src/plugins/presentation_panel/public/panel_component/_presentation_panel.scss +++ b/src/plugins/presentation_panel/public/panel_component/_presentation_panel.scss @@ -6,8 +6,6 @@ height: 100%; min-height: $euiSizeL + 2px; // + 2px to account for border position: relative; - border: none; - outline: $euiBorderThin; &-isLoading { // completely center the loading indicator @@ -46,13 +44,6 @@ 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 { @@ -121,6 +112,7 @@ &:focus { background-color: transparentize($euiColorLightestShade, .5); } + } .embPanel__optionsMenuPopover-loading { @@ -137,20 +129,43 @@ 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; - .embPanel--dragHandle:hover { + &: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 @@ -169,57 +184,3 @@ 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 new file mode 100644 index 0000000000000..2376c4b43edbb --- /dev/null +++ b/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_context_menu.tsx @@ -0,0 +1,177 @@ +/* + * 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 0747e4a4f8229..669f15cb2ba6b 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,6 +13,7 @@ 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'; @@ -23,18 +24,23 @@ export type PresentationPanelHeaderProps; +} & Pick< + PresentationPanelInternalProps, + 'index' | 'showBadges' | 'getActions' | 'actionPredicate' | 'showNotifications' +>; export const PresentationPanelHeader = < ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi >({ api, + index, viewMode, headerId, getActions, hideTitle, panelTitle, panelDescription, + actionPredicate, showBadges = true, showNotifications = true, }: PresentationPanelHeaderProps) => { @@ -46,9 +52,11 @@ export const PresentationPanelHeader = < ); const showPanelBar = - (!hideTitle && panelTitle) || badgeElements.length > 0 || notificationElements.length > 0; - - if (!showPanelBar) return null; + !hideTitle || + panelDescription || + viewMode !== 'view' || + badgeElements.length > 0 || + notificationElements.length > 0; const ariaLabel = getAriaLabelForTitle(showPanelBar ? panelTitle : undefined); const ariaLabelElement = ( @@ -58,7 +66,6 @@ export const PresentationPanelHeader = < ); const headerClasses = classNames('embPanel__header', { - 'embPanel--dragHandle': viewMode === 'edit', 'embPanel__header--floater': !showPanelBar, }); @@ -66,6 +73,19 @@ 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 deleted file mode 100644 index 469a1f8c4f6e3..0000000000000 --- a/src/plugins/presentation_panel/public/panel_component/panel_header/presentation_panel_hover_actions.tsx +++ /dev/null @@ -1,563 +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, { - 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 ef819c427c765..4189250e394d3 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 b48a4eca7ae1f..570fdfd91e229 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,8 +22,6 @@ import { import { AnyApiAction } from '../../panel_actions/types'; import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types'; -const disabledNotifications = ['ACTION_FILTERS_NOTIFICATION']; - export const usePresentationPanelHeaderActions = < ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi >( @@ -49,8 +47,10 @@ export const usePresentationPanelHeaderActions = < embeddable: api, })) as AnyApiAction[]) ?? []; - const disabledActions = (api.disabledActionIds?.value ?? []).concat(disabledNotifications); - nextActions = nextActions.filter((badge) => disabledActions.indexOf(badge.id) === -1); + const disabledActions = api.disabledActionIds?.value; + if (disabledActions) { + 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, (isCompatible, action) => - handleActionCompatibilityChange('badge', isCompatible, action as AnyApiAction) + badge.subscribeToCompatibilityChanges(apiContext, (isComptaible, action) => + handleActionCompatibilityChange('badge', isComptaible, action as AnyApiAction) ) ); } @@ -97,12 +97,11 @@ export const usePresentationPanelHeaderActions = < apiContext ); for (const notification of frequentlyChangingNotifications) { - if (!disabledNotifications.includes(notification.id)) - subscriptions.add( - notification.subscribeToCompatibilityChanges(apiContext, (isCompatible, action) => - handleActionCompatibilityChange('notification', isCompatible, action as AnyApiAction) - ) - ); + subscriptions.add( + notification.subscribeToCompatibilityChanges(apiContext, (isComptaible, action) => + handleActionCompatibilityChange('notification', isComptaible, 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 fa86060859098..550c76a14aee1 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('embeddablePanel')).toBeInTheDocument(); + expect(screen.getByTestId('embeddablePanelToggleMenuIcon')).toBeInTheDocument(); }); }; @@ -223,10 +223,12 @@ describe('Presentation panel', () => { viewMode: new BehaviorSubject('view'), }; await renderPresentationPanel({ api }); - expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument(); + const header = await screen.findByTestId('embeddablePanelHeading'); + const titleComponent = screen.queryByTestId('dashboardPanelTitle'); + expect(header).not.toContainElement(titleComponent); }); - it('does not render a title when in edit mode and the provided title is blank', async () => { + it('renders a placeholder title when in edit mode and the provided title is blank', async () => { const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = { uuid: 'test', panelTitle: new BehaviorSubject(''), @@ -234,7 +236,9 @@ describe('Presentation panel', () => { dataViews: new BehaviorSubject([]), }; await renderPresentationPanel({ api }); - expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('[No Title]'); + }); }); it('opens customize panel flyout on title click when in edit mode', async () => { @@ -270,7 +274,7 @@ describe('Presentation panel', () => { expect(screen.queryByTestId('embeddablePanelTitleLink')).not.toBeInTheDocument(); }); - it('hides title in view mode when API hide title option is true', async () => { + it('hides title when API hide title option is true', async () => { const api: DefaultPresentationPanelApi & PublishesViewMode = { uuid: 'test', panelTitle: new BehaviorSubject('SUPER TITLE'), @@ -281,18 +285,7 @@ describe('Presentation panel', () => { expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument(); }); - 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 () => { + it('hides title when parent hide title option is true', async () => { const api: DefaultPresentationPanelApi & PublishesViewMode = { uuid: 'test', panelTitle: new BehaviorSubject('SUPER TITLE'), @@ -305,19 +298,5 @@ 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 ccf2e694d1b7a..6890ea2f76109 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,7 +16,6 @@ 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'; @@ -77,7 +76,7 @@ export const PresentationPanelInternal = < const hideTitle = Boolean(hidePanelTitle) || Boolean(parentHidePanelTitle) || - !Boolean(panelTitle ?? defaultPanelTitle); + (viewMode === 'view' && !Boolean(panelTitle ?? defaultPanelTitle)); const contentAttrs = useMemo(() => { const attrs: { [key: string]: boolean } = {}; @@ -91,56 +90,55 @@ export const PresentationPanelInternal = < }, [dataLoading, blockingError]); return ( - - - {!hideHeader && api && ( - + )} + {blockingError && api && ( + + + + )} + {!initialLoadComplete && } +
+ + )} + ref={(newApi) => { + if (newApi && !api) setApi(newApi); + }} /> - )} - {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 fa60f134321ac..a05fbc6d92a75 100644 --- a/src/plugins/presentation_panel/public/panel_component/types.ts +++ b/src/plugins/presentation_panel/public/panel_component/types.ts @@ -9,7 +9,6 @@ import { PresentationContainer } from '@kbn/presentation-containers'; import { - CanLockHoverActions, HasParentApi, HasUniqueId, PublishesBlockingError, @@ -75,8 +74,7 @@ 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 d62551efce297..7f02a934a4370 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', }); -export interface ActionWithContext { +interface ActionWithContext { action: Action | ActionInternal; context: Context; @@ -37,7 +37,6 @@ type ItemDescriptor = EuiContextMenuPanelItemDescriptor & { }; type PanelDescriptor = EuiContextMenuPanelDescriptor & { - _order?: number; _level?: number; _icon?: string; items: ItemDescriptor[]; @@ -102,7 +101,7 @@ const removeItemMetaFields = (items: ItemDescriptor[]): EuiContextMenuPanelItemD const removePanelMetaFields = (panels: PanelDescriptor[]): EuiContextMenuPanelDescriptor[] => { const euiPanels: EuiContextMenuPanelDescriptor[] = []; for (const panel of panels) { - const { _level: omit, _icon: omit2, _order: omit3, ...rest } = panel; + const { _level: omit, _icon: omit2, ...rest } = panel; euiPanels.push({ ...rest, items: removeItemMetaFields(rest.items) }); } return euiPanels; @@ -125,18 +124,15 @@ 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) { @@ -150,7 +146,6 @@ export async function buildContextMenuForActions({ title: name, items: [], _level: i, - _order: group.order || 0, _icon: group.getIconType ? group.getIconType(context) : 'empty', }; if (parentPanel) { @@ -195,11 +190,7 @@ export async function buildContextMenuForActions({ wrapMainPanelItemsIntoSubmenu(panels, 'mainMenu'); - const sortedPanels = Object.values(panels).sort((a, b) => { - return (b._order || 0) - (a._order || 0); - }); - - for (const panel of sortedPanels) { + for (const panel of Object.values(panels)) { if (panel._level === 0) { if (panels.mainMenu.items.length > 0) { panels.mainMenu.items.push({ @@ -207,7 +198,7 @@ export async function buildContextMenuForActions({ key: panel.id + '__separator', }); } - if (panel.items.length > 4) { + if (panel.items.length > 3) { 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 640a3bbb70391..f1c64555e048e 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: 0, + order: 26, }, ]; diff --git a/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts b/test/functional/apps/dashboard/group1/edit_embeddable_redirects.ts index 6546f5091a0db..2f40111ce1ed0 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.editPanelByTitle('wowee, looks like I have a new title'); + await dashboardPanelActions.clickEdit(); 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.clickEdit(); + await dashboardPanelActions.editPanelByTitle('wowee, my title just got cooler'); 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 367dae942af92..0bf31cf58616c 100644 --- a/test/functional/apps/dashboard/group3/panel_context_menu.ts +++ b/test/functional/apps/dashboard/group3/panel_context_menu.ts @@ -48,6 +48,9 @@ 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 25e525747edc6..4b488fdb25d8a 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.clickPanelAction( + await dashboardPanelActions.clickContextMenuItem( 'embeddablePanelAction-ACTION_VIEW_SAVED_SEARCH' ); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 2a263e9aa8ca7..f6a3aec2eacd5 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -296,13 +296,11 @@ 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 drag handles + // wait until the count of dashboard panels equals the count of toggle menu icons await this.retry.waitFor('in edit mode', async () => { - const panels = await this.find.allByCssSelector('.embPanel__hoverActionsWrapper'); - const dragHandles = await this.find.allByCssSelector( - '[data-test-subj="embeddablePanelDragHandle"]' - ); - return panels.length === dragHandles.length; + const panels = await this.testSubjects.findAll('embeddablePanel', 2500); + const menuIcons = await this.testSubjects.findAll('embeddablePanelToggleMenuIcon', 2500); + return panels.length === menuIcons.length; }); } diff --git a/test/functional/screenshots/baseline/area_chart.png b/test/functional/screenshots/baseline/area_chart.png index dc6ba3498298e..07004fbb36655 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 fad76455a2a24..53a928bc1514a 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 5d29b891e6fd0..8b33a0077efa2 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 75474fef41655..8cb8a9b635c2c 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -26,109 +26,106 @@ 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_MARGIN_SIZE = 8; +const DASHBOARD_TOP_OFFSET = 96 + 105; // 96 for Kibana navigation bar + 105 for dashboard top nav bar (in edit mode) 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 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) { + async findContextMenu(parent?: WebElementWrapper) { this.log.debug('findContextMenu'); - return wrapper - ? await wrapper.findByTestSubject(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ) + return parent + ? await this.testSubjects.findDescendant(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ, parent) : await this.testSubjects.find(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 isContextMenuIconVisible() { + this.log.debug('isContextMenuIconVisible'); + return await this.testSubjects.exists(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); } - 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 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 toggleContextMenuByTitle(title = '') { this.log.debug(`toggleContextMenu(${title})`); - const wrapper = await this.getPanelWrapper(title); - await this.toggleContextMenu(wrapper); + const header = await this.getPanelHeading(title); + await this.toggleContextMenu(header); } async expectContextMenuToBeOpen() { this.log.debug('expectContextMenuToBeOpen'); - await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen', { allowHidden: true }); + await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen'); } - async openContextMenu(wrapper?: WebElementWrapper) { - this.log.debug(`openContextMenu(${wrapper}`); + async openContextMenu(parent?: WebElementWrapper) { + this.log.debug(`openContextMenu`); const open = await this.testSubjects.exists('embeddablePanelContextMenuOpen'); - if (!open) await this.toggleContextMenu(wrapper); + if (!open) await this.toggleContextMenu(parent); await this.expectContextMenuToBeOpen(); } async openContextMenuByTitle(title = '') { this.log.debug(`openContextMenuByTitle(${title})`); - const wrapper = await this.getPanelWrapper(title); - await this.openContextMenu(wrapper); + const header = await this.getPanelHeading(title); + await this.openContextMenu(header); } - 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.openContextMenu(wrapper); - action = await this.testSubjects.find(testSubject); - } else { - action = await this.testSubjects.findDescendant(testSubject, wrapper); + async hasContextMenuMoreItem() { + this.log.debug('hasContextMenuMoreItem'); + return await this.testSubjects.exists('embeddablePanelMore-mainMenu', { timeout: 500 }); + } + + async clickContextMenuMoreItem() { + this.log.debug('clickContextMenuMoreItem'); + await this.expectContextMenuToBeOpen(); + if (await this.hasContextMenuMoreItem()) { + await this.testSubjects.clickWhenNotDisabledWithoutRetry('embeddablePanelMore-mainMenu'); } + } - await action.click(await this.getContainerTopOffset()); + async openContextMenuMorePanel(parent?: WebElementWrapper) { + this.log.debug('openContextMenuMorePanel'); + await this.openContextMenu(parent); + await this.clickContextMenuMoreItem(); } - async clickPanelActionByTitle(testSubject: string, title = '') { - this.log.debug(`clickPanelActionByTitle(${testSubject},${title})`); - const wrapper = await this.getPanelWrapper(title); - await this.clickPanelAction(testSubject, wrapper); + async clickContextMenuItem(testSubject: string, parent?: WebElementWrapper) { + this.log.debug(`clickContextMenuItem(${testSubject})`); + await this.openContextMenu(parent); + const exists = await this.testSubjects.exists(testSubject, { timeout: 500 }); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await this.testSubjects.clickWhenNotDisabledWithoutRetry(testSubject, { timeout: 500 }); } - async navigateToEditorFromFlyout(wrapper?: WebElementWrapper) { + async clickContextMenuItemByTitle(testSubject: string, title = '') { + this.log.debug(`openContextMenuByTitle(${title})`); + const header = await this.getPanelHeading(title); + await this.clickContextMenuItem(testSubject, header); + } + + async navigateToEditorFromFlyout() { this.log.debug('navigateToEditorFromFlyout'); - await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ, wrapper); + await this.clickContextMenuItem(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); await this.header.waitUntilLoadingHasFinished(); await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ); const isConfirmModalVisible = await this.testSubjects.exists('confirmModalConfirmButton'); @@ -141,7 +138,7 @@ export class DashboardPanelActionsService extends FtrService { async clickInlineEdit() { this.log.debug('clickInlineEditAction'); - await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); + await this.clickContextMenuItem(INLINE_EDIT_PANEL_DATA_TEST_SUBJ); await this.header.waitUntilLoadingHasFinished(); await this.common.waitForTopNavToBeVisible(); } @@ -150,16 +147,20 @@ 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(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 { + 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 - await this.navigateToEditorFromFlyout(wrapper); + } else { + await this.navigateToEditorFromFlyout(); } await this.header.waitUntilLoadingHasFinished(); await this.common.waitForTopNavToBeVisible(); @@ -171,55 +172,55 @@ export class DashboardPanelActionsService extends FtrService { */ async editPanelByTitle(title = '') { this.log.debug(`editPanelByTitle(${title})`); - const wrapper = await this.getPanelWrapper(title); - await this.clickEdit(wrapper); + const header = await this.getPanelHeading(title); + await this.clickEdit(header); } async clickExpandPanelToggle() { this.log.debug(`clickExpandPanelToggle`); await this.openContextMenu(); - await this.clickPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + await this.clickContextMenuItem(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); } - async removePanel(wrapper?: WebElementWrapper) { + async removePanel(parent?: WebElementWrapper) { this.log.debug('removePanel'); - await this.clickPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ, wrapper); + await this.openContextMenu(parent); + await this.clickContextMenuItem(REMOVE_PANEL_DATA_TEST_SUBJ, parent); } async removePanelByTitle(title = '') { this.log.debug(`removePanel(${title})`); - const wrapper = await this.getPanelWrapper(title); - await this.removePanel(wrapper); + const header = await this.getPanelHeading(title); + this.log.debug('found header? ', Boolean(header)); + await this.removePanel(header); } async customizePanel(title = '') { this.log.debug(`customizePanel(${title})`); - await this.clickPanelActionByTitle(CUSTOMIZE_PANEL_DATA_TEST_SUBJ, title); + const header = await this.getPanelHeading(title); + await this.clickContextMenuItem(CUSTOMIZE_PANEL_DATA_TEST_SUBJ, header); } async clonePanel(title = '') { this.log.debug(`clonePanel(${title})`); - await this.clickPanelActionByTitle(CLONE_PANEL_DATA_TEST_SUBJ, title); + const header = await this.getPanelHeading(title); + await this.clickContextMenuItem(CLONE_PANEL_DATA_TEST_SUBJ, header); await this.dashboard.waitForRenderComplete(); } async openCopyToModalByTitle(title = '') { this.log.debug(`copyPanelTo(${title})`); - 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); + const header = await this.getPanelHeading(title); + await this.clickContextMenuItem(COPY_PANEL_TO_DATA_TEST_SUBJ, header); } - async openInspectorByTitle(title = '') { + async openInspectorByTitle(title: string) { this.log.debug(`openInspector(${title})`); - const wrapper = await this.getPanelWrapper(title); - await this.openInspector(wrapper); + const header = await this.getPanelHeading(title); + await this.openInspector(header); } - async getSearchSessionIdByTitle(title = '') { + async getSearchSessionIdByTitle(title: string) { this.log.debug(`getSearchSessionId(${title})`); await this.openInspectorByTitle(title); await this.inspector.openInspectorRequestsView(); @@ -230,7 +231,7 @@ export class DashboardPanelActionsService extends FtrService { return searchSessionId; } - async getSearchResponseByTitle(title = '') { + async getSearchResponseByTitle(title: string) { this.log.debug(`setSearchResponse(${title})`); await this.openInspectorByTitle(title); await this.inspector.openInspectorRequestsView(); @@ -239,23 +240,31 @@ 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}`); - await this.clickPanelActionByTitle(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title); + const header = await this.getPanelHeading(title); + await this.clickContextMenuItem(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, header); await this.testSubjects.existOrFail('unlinkPanelSuccess'); await this.expectNotLinkedToLibrary(title, true); } async unlinkFromLibrary(title = '') { this.log.debug(`unlinkFromLibrary(${title})`); - await this.clickPanelActionByTitle(UNLINK_FROM_LIBRARY_TEST_SUBJ, title); + const header = await this.getPanelHeading(title); + await this.clickContextMenuItem(UNLINK_FROM_LIBRARY_TEST_SUBJ, header); await this.testSubjects.existOrFail('unlinkPanelSuccess'); await this.expectNotLinkedToLibrary(title); } async legacySaveToLibrary(newTitle = '', oldTitle = '') { this.log.debug(`legacySaveToLibrary(${newTitle},${oldTitle})`); - await this.clickPanelActionByTitle(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle); + const header = await this.getPanelHeading(oldTitle); + await this.clickContextMenuItem(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, header); await this.testSubjects.setValue('savedObjectTitle', newTitle, { clearWithKeyboard: true, }); @@ -266,7 +275,8 @@ export class DashboardPanelActionsService extends FtrService { async saveToLibrary(newTitle = '', oldTitle = '') { this.log.debug(`saveToLibraryByTitle(${newTitle},${oldTitle})`); - await this.clickPanelActionByTitle(SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle); + const header = await this.getPanelHeading(oldTitle); + await this.clickContextMenuItem(SAVE_TO_LIBRARY_TEST_SUBJ, header); await this.testSubjects.setValue('savedObjectTitle', newTitle, { clearWithKeyboard: true, }); @@ -275,31 +285,18 @@ 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 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); + 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 }); } + await this.toggleContextMenu(panelWrapper); } async expectExistsRemovePanelAction(title = '') { @@ -327,16 +324,15 @@ export class DashboardPanelActionsService extends FtrService { } async expectMissingPanelAction(testSubject: string, title = '') { - 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); + 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(); await this.testSubjects.missingOrFail(testSubject); - await this.toggleContextMenu(wrapper); } + await this.toggleContextMenu(panelWrapper); } async expectMissingEditPanelAction(title = '') { @@ -356,21 +352,10 @@ export class DashboardPanelActionsService extends FtrService { async getPanelHeading(title = '') { this.log.debug(`getPanelHeading(${title})`); - if (!title) return await this.find.byClassName('embPanel__wrapper'); + if (!title) return await this.find.byClassName('embPanel__header'); 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'); @@ -385,23 +370,28 @@ export class DashboardPanelActionsService extends FtrService { throw new Error(`No action matching text "${text}"`); } - async canConvertToLens(wrapper?: WebElementWrapper) { + async canConvertToLens(parent?: WebElementWrapper) { this.log.debug('canConvertToLens'); - await this.openContextMenu(wrapper); - return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 500 }); + 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 }); } async canConvertToLensByTitle(title = '') { this.log.debug(`canConvertToLens(${title})`); - const wrapper = await this.getPanelWrapper(title); - return await this.canConvertToLens(wrapper); + 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 }); } - async convertToLens(wrapper?: WebElementWrapper) { + async convertToLens(parent?: WebElementWrapper) { this.log.debug('convertToLens'); await this.retry.try(async () => { - if (!(await this.canConvertToLens(wrapper))) { + if (!(await this.canConvertToLens(parent))) { throw new Error('Convert to Lens option not found'); } @@ -411,31 +401,29 @@ export class DashboardPanelActionsService extends FtrService { async convertToLensByTitle(title = '') { this.log.debug(`convertToLens(${title})`); - const wrapper = await this.getPanelWrapper(title); - return await this.convertToLens(wrapper); + const header = await this.getPanelHeading(title); + return await this.convertToLens(header); } - async expectLinkedToLibrary(title = '', legacy?: boolean) { + public 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); } - if (isViewMode) await this.dashboard.clickCancelOutOfEditMode(); + await this.expectMissingPanelAction(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, title); + await this.expectMissingPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title); } - async expectNotLinkedToLibrary(title = '', legacy?: boolean) { + public 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); } - if (isViewMode) await this.dashboard.clickCancelOutOfEditMode(); + await this.expectMissingPanelAction(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title); + await this.expectMissingPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title); } } diff --git a/test/functional/services/dashboard/panel_drilldown_actions.ts b/test/functional/services/dashboard/panel_drilldown_actions.ts index 8dad803a114a2..7c2e0278bc8e9 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.clickPanelAction(CREATE_DRILLDOWN_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(CREATE_DRILLDOWN_DATA_TEST_SUBJ); } async expectExistsManageDrilldownsAction() { @@ -52,7 +52,7 @@ export function DashboardDrilldownPanelActionsProvider({ async clickManageDrilldowns() { log.debug('clickManageDrilldowns'); - await dashboardPanelActions.clickPanelAction(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ); } async expectMultipleActionsMenuOpened() { @@ -93,13 +93,14 @@ 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.openContextMenu(panel); + await dashboardPanelActions.clickContextMenuMoreItem(); 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 186f91ba26944..8db65c13b57b6 100644 --- a/test/plugin_functional/test_suites/panel_actions/panel_actions.ts +++ b/test/plugin_functional/test_suites/panel_actions/panel_actions.ts @@ -23,6 +23,10 @@ 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 92376abbc7246..e54cfd503e197 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: 'visible', + default: 'hidden', }, 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 7b1884215a20a..223b8532d8a56 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 'visible'`, () => { + it(`defaults to 'hidden'`, () => { const result = fn(null); - expect(result).toHaveProperty('overflow', 'visible'); + expect(result).toHaveProperty('overflow', 'hidden'); }); }); }); 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 793cc423d7904..29888d862db7c 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,7 +3,6 @@ outline: none !important; background: none; border-radius: 0 !important; - box-shadow: none; .embPanel__title { margin-bottom: $euiSizeXS; @@ -25,15 +24,6 @@ } } - .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 a0bc78749c519..d27e759c63ea1 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,32 +1,10 @@ .canvasElement { height: 100%; width: 100%; - - .embPanel { - .embPanel__content { - overflow: visible; - } - - .embPanel__hoverActionsLeft, .embPanel__dragHandle { - visibility: hidden; - } - } + overflow: 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 a6fb2e1af58f1..1c65c8f9cfa4e 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,7 +7,6 @@ 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'; @@ -30,8 +29,6 @@ export interface Props { backgroundColor: string; selectElement: () => void; state: string; - selectedElementId: string | null; - id: string; } export const ElementContent = (props: Props) => { @@ -62,9 +59,7 @@ 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 e753be8cbc527..72ff04cbf2055 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, getSelectedElementId } from '../../state/selectors/workpad'; +import { getSelectedPage, getPageById } 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,7 +16,6 @@ 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; @@ -25,5 +24,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 c1a40839530fd..80b2f0497e89e 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,12 +11,11 @@ import { Positionable } from '../positionable'; import { ElementContent } from '../element_content'; export const ElementWrapper = (props) => { - const { renderable, transformMatrix, width, height, state, handlers, id } = props; + const { renderable, transformMatrix, width, height, state, handlers } = 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 dea0c1ace09a7..5db66cce872b1 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$: new BehaviorSubject(true), + canViewUnderlyingData: () => {}, 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 7d4458d02b556..3fa6586dbb83a 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,5 +77,3 @@ 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 36c157470a2f2..b4d1455290c1a 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,7 +36,6 @@ import React from 'react'; import { StartDependencies } from '../../../../plugin'; import { createDrilldownTemplatesFromSiblings, - DRILLDOWN_ACTION_GROUP, DRILLDOWN_MAX_WIDTH, ensureNestedTriggers, } from '../drilldown_shared'; @@ -63,7 +62,6 @@ 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 26f5311d5b325..ca184c23c9603 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,7 +34,6 @@ import { MenuItem } from './menu_item'; import { StartDependencies } from '../../../../plugin'; import { createDrilldownTemplatesFromSiblings, - DRILLDOWN_ACTION_GROUP, DRILLDOWN_MAX_WIDTH, ensureNestedTriggers, } from '../drilldown_shared'; @@ -58,7 +57,6 @@ 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 ce86b896d5fa0..5ef2a8d202984 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 { BehaviorSubject, Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; @@ -1034,8 +1034,6 @@ export class Embeddable this.activeData = newActiveData; this.renderUserMessages(); - - this.loadViewUnderlyingDataArgs(); }; private onRender: ExpressionWrapperProps['onRender$'] = () => { @@ -1482,7 +1480,7 @@ export class Embeddable } } - private async loadViewUnderlyingDataArgs(): Promise { + private async loadViewUnderlyingDataArgs(): Promise { if ( !this.savedVis || !this.activeData || @@ -1491,15 +1489,13 @@ export class Embeddable !this.activeVisualization || !this.activeVisualizationState ) { - this.canViewUnderlyingData$.next(false); - return; + return false; } const mergedSearchContext = this.getMergedSearchContext(); if (!mergedSearchContext.timeRange) { - this.canViewUnderlyingData$.next(false); - return; + return false; } const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({ @@ -1521,8 +1517,7 @@ export class Embeddable if (loaded) { this.viewUnderlyingDataArgs = viewUnderlyingDataArgs; } - - this.canViewUnderlyingData$.next(loaded); + return loaded; } /** @@ -1534,7 +1529,9 @@ export class Embeddable return this.viewUnderlyingDataArgs; } - public canViewUnderlyingData$ = new BehaviorSubject(false); + public canViewUnderlyingData() { + return this.loadViewUnderlyingDataArgs(); + } 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 11b70cd6e7763..3a03e63ded311 100644 --- a/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts +++ b/x-pack/plugins/lens/public/embeddable/interfaces/lens_api.ts @@ -10,7 +10,6 @@ import type { HasType, PublishesUnifiedSearch, PublishesPanelTitle, - PublishingSubject, } from '@kbn/presentation-publishing'; import { apiIsOfType, @@ -21,7 +20,7 @@ import { LensSavedObjectAttributes, ViewUnderlyingDataArgs } from '../embeddable export type HasLensConfig = HasType<'lens'> & { getSavedVis: () => Readonly; - canViewUnderlyingData$: PublishingSubject; + canViewUnderlyingData: () => Promise; getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs; getFullAttributes: () => LensSavedObjectAttributes | undefined; }; @@ -36,7 +35,7 @@ export const isLensApi = (api: unknown): api is LensApi => { api && apiIsOfType(api, 'lens') && typeof (api as HasLensConfig).getSavedVis === 'function' && - (api as HasLensConfig).canViewUnderlyingData$ && + typeof (api as HasLensConfig).canViewUnderlyingData === 'function' && 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 fd1ef4f746c41..45dc8cbe32898 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$: new BehaviorSubject(true), + canViewUnderlyingData: () => Promise.resolve(true), getFullAttributes: jest.fn(() => undefined), getViewUnderlyingDataArgs: jest.fn(() => ({ dataViewSpec: { id: 'index-pattern-id' }, @@ -78,7 +78,8 @@ describe('open in discover action', () => { // setup const embeddable = { ...compatibleEmbeddableApi, - canViewUnderlyingData$: { getValue: jest.fn(() => false) }, + canViewUnderlyingData: jest.fn(() => Promise.resolve(false)), + getViewUnderlyingDataArgs: jest.fn(() => undefined), }; // test false @@ -92,11 +93,10 @@ describe('open in discover action', () => { } as ActionExecutionContext) ).toBeFalsy(); - expect(embeddable.canViewUnderlyingData$.getValue).toHaveBeenCalledTimes(1); + expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1); // test true - embeddable.canViewUnderlyingData$.getValue = jest.fn(() => true); - + embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(true)); expect( await createOpenInDiscoverAction( {} as DiscoverAppLocator, @@ -107,7 +107,7 @@ describe('open in discover action', () => { } as ActionExecutionContext) ).toBeTruthy(); - expect(embeddable.canViewUnderlyingData$.getValue).toHaveBeenCalledTimes(1); + expect(embeddable.canViewUnderlyingData).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 d9dccab616d5b..9b48c41e41856 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,11 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import { Action, createAction, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import { createAction } 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'; @@ -20,12 +19,12 @@ export const createOpenInDiscoverAction = ( locator: DiscoverAppLocator, dataViews: Pick, hasDiscoverAccess: boolean -) => { - const actionDefinition = { +) => + createAction({ type: ACTION_OPEN_IN_DISCOVER, id: ACTION_OPEN_IN_DISCOVER, - order: 20, // right before Inspect which is 19 - getIconType: () => 'discoverApp', + order: 19, // right after Inspect which is 20 + getIconType: () => 'popout', getDisplayName: () => i18n.translate('xpack.lens.action.exploreInDiscover', { defaultMessage: 'Explore in Discover', @@ -48,26 +47,8 @@ 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 0a52ea6b4711f..0276674767120 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 function isCompatible({ hasDiscoverAccess, embeddable }: Context) { +export async function isCompatible({ hasDiscoverAccess, embeddable }: Context) { if (!hasDiscoverAccess) return false; try { - return isLensApi(embeddable) && embeddable.canViewUnderlyingData$.getValue(); + return isLensApi(embeddable) && (await embeddable.canViewUnderlyingData()); } 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 6091eccbe28ad..84f053aafcaf5 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,8 +23,6 @@ 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 21feb23a9ca9a..cf24d50473467 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,6 +190,5 @@ 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 0da947f576ef3..d20d93ae03a7c 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6603,6 +6603,7 @@ "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 9f47ccf6992a9..2f52ef5da5b61 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6595,6 +6595,7 @@ "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 af52f88a95511..41d49632573f4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6515,6 +6515,7 @@ "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 9ef299ca1cf2d..92c558f17dadc 100644 --- a/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { @@ -17,7 +18,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Failing: See https://github.com/elastic/kibana/issues/147667 describe.skip('Dashboard panel options a11y tests', () => { - const title = '[Flights] Flight count'; + let header: WebElementWrapper; before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, @@ -27,6 +28,7 @@ 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 () => { @@ -38,13 +40,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // dashboard panel options in view mode it('dashboard panel - open menu', async () => { - await dashboardPanelActions.toggleContextMenuByTitle(title); + await dashboardPanelActions.toggleContextMenu(header); await a11y.testAppSnapshot(); - await dashboardPanelActions.toggleContextMenuByTitle(title); + await dashboardPanelActions.toggleContextMenu(header); }); it('dashboard panel - customize time range', async () => { - await dashboardPanelActions.toggleContextMenuByTitle(title); + await dashboardPanelActions.toggleContextMenu(header); await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); await a11y.testAppSnapshot(); await testSubjects.click('cancelPerPanelTimeRangeButton'); @@ -77,14 +79,21 @@ 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.openContextMenuByTitle(title); + await dashboardPanelActions.openContextMenuMorePanel(header); await testSubjects.click('embeddablePanelAction-copyToDashboard'); await a11y.testAppSnapshot(); await testSubjects.click('cancelCopyToButton'); @@ -94,13 +103,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dashboard panel - clone panel', async () => { await testSubjects.click('dashboardEditMode'); - await dashboardPanelActions.openContextMenuByTitle(title); + await dashboardPanelActions.toggleContextMenu(header); 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'); @@ -110,7 +120,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel - Create drilldown panel', async () => { - await dashboardPanelActions.openContextMenuByTitle(title); + await dashboardPanelActions.toggleContextMenu(header); + await testSubjects.click('embeddablePanelMore-mainMenu'); await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); await a11y.testAppSnapshot(); await testSubjects.click('actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN'); @@ -125,16 +136,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('dashboard panel - manage drilldown', async () => { - await dashboardPanelActions.openContextMenuByTitle(title); + await dashboardPanelActions.toggleContextMenu(header); + await testSubjects.click('embeddablePanelMore-mainMenu'); 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.legacySaveToLibrary('', title); + await dashboardPanelActions.openContextMenuMorePanel(header); + await testSubjects.click('embeddablePanelAction-saveToLibrary'); 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 ebd85a0ab2720..2bd2ec820b6f3 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/lens.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/lens.ts @@ -34,8 +34,32 @@ 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({ @@ -55,6 +79,8 @@ 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(); @@ -62,34 +88,8 @@ 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 ac6a861e9796e..2a63c4f64b57a 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/maps.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/maps.ts @@ -36,6 +36,7 @@ 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 7de1ef28a43a1..9cb0d55371f72 100644 --- a/x-pack/test/functional/apps/canvas/embeddables/visualization.ts +++ b/x-pack/test/functional/apps/canvas/embeddables/visualization.ts @@ -71,6 +71,7 @@ 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 () => { @@ -92,6 +93,7 @@ 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 a974eb8c1284b..3e648f5000945 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,6 +47,7 @@ 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(); @@ -59,6 +60,7 @@ 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(); @@ -71,6 +73,7 @@ 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 4c890b41e0612..a55c3c3c0433c 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,6 +42,7 @@ 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 7d8456a9e81a8..c3d5bfce6e621 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 = undefined; + const EMPTY_TITLE = '[No Title]'; describe('panel titles', () => { before(async () => { @@ -112,6 +112,7 @@ 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 050483c98ac7b..ca6f23d09e375 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.expectExistsPanelAction(ACTION_TEST_SUBJ); + await panelActions.openContextMenu(); + await testSubjects.existOrFail(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,7 +87,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { exitFromEditMode: true, }); - await panelActions.clickPanelAction(ACTION_TEST_SUBJ); + await panelActions.openContextMenu(); + await testSubjects.clickWhenNotDisabledWithoutRetry(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 new file mode 100644 index 0000000000000..2cf3f91c6a38e --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts @@ -0,0 +1,208 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + 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([ + 'reporting', + 'common', + 'dashboard', + 'timePicker', + ]); + + const navigateToDashboardApp = async () => { + log.debug('in navigateToDashboardApp'); + await dashboard.navigateToApp(); + await retry.tryForTime(10000, async () => { + expect(await dashboard.onDashboardLandingPage()).to.be(true); + }); + }; + + const getCsvReportData = async () => { + await toasts.dismissAll(); + const url = await reporting.getReportURL(60000); + const res = await reporting.getResponse(url ?? ''); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('text/csv; charset=utf-8'); + return res.text; + }; + + const clickActionsMenu = async (headingTestSubj: string) => { + const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-' + headingTestSubj); + await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + }; + + const clickDownloadCsv = async () => { + log.debug('click "Generate CSV"'); + await dashboardPanelActions.clickContextMenuItem('embeddablePanelAction-generateCsvReport'); + await testSubjects.existOrFail('csvReportStarted'); // validate toast panel + }; + + const createPartialCsv = (csvFile: unknown) => { + const partialCsvFile = (csvFile as string).split('\n').slice(0, 4); + return partialCsvFile.join('\n'); + }; + + /* + * Tests + */ + describe('Dashboard Generate CSV', () => { + describe('Default Saved Search Data', () => { + before(async () => { + await esArchiver.emptyKibanaIndex(); + await reportingService.initEcommerce(); + await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'UTC' }); + }); + + beforeEach(async () => { + await navigateToDashboardApp(); + }); + + after(async () => { + await reportingService.teardownEcommerce(); + }); + + it('Generate CSV export of a saved search panel', async function () { + await dashboard.loadSavedDashboard('Ecom Dashboard - 3 Day Period'); + await clickActionsMenu('EcommerceData'); + await clickDownloadCsv(); + + const csvFile = await getCsvReportData(); + expect(csvFile.length).to.be(76137); + expectSnapshot(createPartialCsv(csvFile)).toMatch(); + }); + + it('Downloads a filtered CSV export of a saved search panel', async function () { + await dashboard.loadSavedDashboard('Ecom Dashboard - 3 Day Period'); + + // add a filter + await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` }); + + await clickActionsMenu('EcommerceData'); + await clickDownloadCsv(); + + const csvFile = await getCsvReportData(); + expect(csvFile.length).to.be(17106); + expectSnapshot(createPartialCsv(csvFile)).toMatch(); + }); + + 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(); + + const csvFile = await getCsvReportData(); + expect(csvFile.length).to.be(23277); + expectSnapshot(createPartialCsv(csvFile)).toMatch(); + }); + + 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"]' + ); // panel title is hidden + await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + + await clickDownloadCsv(); + await testSubjects.existOrFail('csvReportStarted'); + + const csvFile = await getCsvReportData(); + expect(csvFile).to.not.be(null); + }); + }); + + describe('Filtered Saved Search', () => { + const TEST_SEARCH_TITLE = 'Customer Betty'; + const TEST_DASHBOARD_TITLE = 'Filtered Search Data'; + const from = 'Jun 20, 2019 @ 23:56:51.374'; + const to = 'Jun 25, 2019 @ 16:18:51.821'; + + before(async () => { + await esArchiver.emptyKibanaIndex(); + await reportingService.initEcommerce(); + await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'UTC' }); + }); + + beforeEach(async () => { + await navigateToDashboardApp(); + log.info(`Creating empty dashboard`); + await dashboard.clickNewDashboard(); + await timePicker.setAbsoluteRange(from, to); + log.info(`Adding "${TEST_SEARCH_TITLE}" to dashboard`); + await dashboardAddPanel.addSavedSearch(TEST_SEARCH_TITLE); + await dashboard.saveDashboard(TEST_DASHBOARD_TITLE); + }); + + after(async () => { + await reportingService.teardownEcommerce(); + await common.unsetTime(); + }); + + it('Downloads filtered Discover saved search report', async () => { + await clickActionsMenu(TEST_SEARCH_TITLE.replace(/ /g, '')); + await clickDownloadCsv(); + + const csvFile = await getCsvReportData(); + expect(csvFile.length).to.be(2446); + expectSnapshot(createPartialCsv(csvFile)).toMatch(); + }); + }); + + describe('Field Formatters and Scripted Fields', () => { + const dashboardWithScriptedFieldsSearch = 'names dashboard'; + + before(async () => { + await esArchiver.emptyKibanaIndex(); + await reportingService.initLogs(); + await esArchiver.load('x-pack/test/functional/es_archives/reporting/hugedata'); + await kibanaServer.uiSettings.update({ 'dateFormat:tz': 'UTC' }); + }); + + beforeEach(async () => { + await navigateToDashboardApp(); + await dashboard.loadSavedDashboard(dashboardWithScriptedFieldsSearch); + await timePicker.setAbsoluteRange( + 'Nov 26, 1981 @ 21:54:15.526', + 'Mar 5, 1982 @ 18:17:44.821' + ); + + await common.sleep(1000); + await filterBar.addFilter({ field: 'name.keyword', operation: 'is', value: 'Fethany' }); + await common.sleep(1000); + }); + + after(async () => { + await reportingService.teardownLogs(); + await esArchiver.unload('x-pack/test/functional/es_archives/reporting/hugedata'); + }); + + it('Generate CSV export of a saved search panel', async () => { + await clickActionsMenu('namessearch'); + await clickDownloadCsv(); + + const csvFile = await getCsvReportData(); + expect(csvFile.length).to.be(166); + expectSnapshot(createPartialCsv(csvFile)).toMatch(); + }); + }); + }); +} 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 e188f1c6f4c1c..1ce272bd4a86f 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 b7c4cbdddd5fa..38f66db31dc92 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,7 +54,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } const checkDiscoverNavigationResult = async () => { - await dashboardPanelActions.clickPanelAction('embeddablePanelAction-ACTION_OPEN_IN_DISCOVER'); + await dashboardPanelActions.clickContextMenuItem( + '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 312bddba10eac..2d92d98dc3606 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,6 +49,7 @@ 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'); @@ -89,6 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.legacySaveToLibrary('My by reference visualization'); + await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Removes breakdown dimension'); @@ -108,6 +110,7 @@ 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'); @@ -147,6 +150,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.waitForRenderComplete(); await elasticChart.setNewChartUiDebugFlag(true); + await dashboardPanelActions.openContextMenu(); await dashboardPanelActions.clickInlineEdit(); log.debug('Adds annotation'); @@ -173,6 +177,7 @@ 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 de563366af3fb..70625ecafaa75 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.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); @@ -59,6 +59,7 @@ 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'); @@ -72,7 +73,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { exitFromEditMode: true, }); - await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles(); await browser.switchToWindow(discoverWindowHandle); @@ -89,6 +90,7 @@ 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'); @@ -117,7 +119,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.expectExistsPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); await dashboard.clickCancelOutOfEditMode(); - await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(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 56f97c8751d77..7c3c14150d2b2 100644 --- a/x-pack/test/functional/apps/lens/group6/lens_tagging.ts +++ b/x-pack/test/functional/apps/lens/group6/lens_tagging.ts @@ -97,6 +97,7 @@ 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 bf799673c2491..593f42c14db4a 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,7 +53,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await dashboardCustomizePanel.clickSaveButton(); await dashboard.waitForRenderComplete(); await dashboardBadgeActions.expectExistsTimeRangeBadgeAction(); - await panelActions.convertToLens(); + 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 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 1f5a09d144934..60ef9b8799d3c 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.clickPanelActionByTitle( + await dashboardPanelActions.clickContextMenuItemByTitle( 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.clickPanelActionByTitle( + await dashboardPanelActions.clickContextMenuItemByTitle( 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 9c3b989882469..6c57295a0c353 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.loadSavedDashboard('A Dashboard'); + await dashboard.gotoDashboardEditMode('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 2b95bfc1bd2c8..7bb49e7397e9d 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.clickPanelActionByTitle( + await dashboardPanelActions.clickContextMenuItemByTitle( '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 e887f6ee80e38..146e72ab3698e 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.clickPanelAction(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(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.clickPanelAction(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(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 aea50c207dc20..1b5c23a1f6568 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.clickPanelActionByTitle( + await dashboardPanelActions.clickContextMenuItemByTitle( GENERATE_CSV_DATA_TEST_SUBJ, savedSearchTitle ); @@ -94,7 +94,7 @@ export function createScenarios( GENERATE_CSV_DATA_TEST_SUBJ, savedSearchTitle ); - await dashboardPanelActions.clickPanelActionByTitle( + await dashboardPanelActions.clickContextMenuItemByTitle( 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 b32eafc8c6899..2942e31ee2f77 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,6 +48,7 @@ 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(); @@ -61,6 +62,7 @@ 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 de95f3b2a243a..ab86b163ef6ee 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,11 +43,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should show the "Convert to Lens" menu item', async () => { - expect(await panelActions.canConvertToLensByTitle('Gauge - Basic')).to.eql(true); + const visPanel = await panelActions.getPanelHeading('Gauge - Basic'); + expect(await panelActions.canConvertToLens(visPanel)).to.eql(true); }); it('should convert aggregation with params', async () => { - await panelActions.convertToLensByTitle('Gauge - Agg with params'); + const visPanel = await panelActions.getPanelHeading('Gauge - Agg with params'); + await panelActions.convertToLens(visPanel); 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 17caa3d6560f2..c429fef23fdd9 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.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(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.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(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 8f97f53c6275f..1f13f10d9e948 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,6 +56,7 @@ 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 d8b43ca1c4301..648309b3d5cab 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.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(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.clickPanelAction(ADD_TO_CASE_DATA_TEST_SUBJ); + await dashboardPanelActions.clickContextMenuItem(ADD_TO_CASE_DATA_TEST_SUBJ); // verify that solution filter is not visible await testSubjects.missingOrFail('options-filter-popover-button-owner');