# Backport This will backport the following commits from `main` to `8.x`: - [[Dashboard] Hover actions for panels (#182535)](https://github.com/elastic/kibana/pull/182535) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Catherine Liu","email":"catherine.liu@elastic.co"},"sourceCommit":{"committedDate":"2024-10-25T07:59:41Z","message":"[Dashboard] Hover actions for panels (#182535)","sha":"2fdfb8d769442a7591e982a0dcff40fb8eb1699a","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","Feature:Dashboard","Feature:Embedding","Team:Presentation","loe:large","impact:medium","Feature:Drilldowns","v9.0.0","Project:Dashboard Usability","ci:project-deploy-observability","Team:obs-ux-management","backport:version","v8.17.0"],"title":"[Dashboard] Hover actions for panels","number":182535,"url":"https://github.com/elastic/kibana/pull/182535","mergeCommit":{"message":"[Dashboard] Hover actions for panels (#182535)","sha":"2fdfb8d769442a7591e982a0dcff40fb8eb1699a"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/182535","number":182535,"mergeCommit":{"message":"[Dashboard] Hover actions for panels (#182535)","sha":"2fdfb8d769442a7591e982a0dcff40fb8eb1699a"}},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Catherine Liu <catherine.liu@elastic.co>
|
@ -427,16 +427,16 @@ export class WebElementWrapper {
|
|||
/**
|
||||
* Moves the remote environment’s mouse cursor to the current element with optional offset
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move
|
||||
* @param { xOffset: 0, yOffset: 0 } options
|
||||
* @param { xOffset: 0, yOffset: 0, topOffset: number } options Optional
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async moveMouseTo(options = { xOffset: 0, yOffset: 0 }) {
|
||||
public async moveMouseTo({ xOffset = 0, yOffset = 0, topOffset = 0 } = {}) {
|
||||
await this.retryCall(async function moveMouseTo(wrapper) {
|
||||
await wrapper.scrollIntoViewIfNecessary();
|
||||
await wrapper.scrollIntoViewIfNecessary(topOffset);
|
||||
await wrapper.getActions().move({ x: 0, y: 0 }).perform();
|
||||
await wrapper
|
||||
.getActions()
|
||||
.move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
|
||||
.move({ x: xOffset, y: yOffset, origin: wrapper._webElement })
|
||||
.perform();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ pageLoadAssetSize:
|
|||
core: 564663
|
||||
crossClusterReplication: 65408
|
||||
customIntegrations: 22034
|
||||
dashboard: 52967
|
||||
dashboard: 68015
|
||||
dashboardEnhanced: 65646
|
||||
data: 454087
|
||||
dataQuality: 19384
|
||||
|
|
|
@ -30,6 +30,10 @@ export {
|
|||
useInheritedViewMode,
|
||||
type CanAccessViewMode,
|
||||
} from './interfaces/can_access_view_mode';
|
||||
export {
|
||||
apiCanLockHoverActions,
|
||||
type CanLockHoverActions,
|
||||
} from './interfaces/can_lock_hover_actions';
|
||||
export { fetch$, useFetchContext, type FetchContext } from './interfaces/fetch/fetch';
|
||||
export {
|
||||
initializeTimeRange,
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
/**
|
||||
* This API can lock hover actions
|
||||
*/
|
||||
export interface CanLockHoverActions {
|
||||
hasLockedHoverActions$: PublishingSubject<boolean>;
|
||||
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'
|
||||
);
|
||||
};
|
|
@ -39,6 +39,7 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
|||
|
||||
import { coreServices } from '../services/kibana_services';
|
||||
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
|
||||
export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary';
|
||||
|
||||
|
@ -63,6 +64,7 @@ export class AddToLibraryAction implements Action<EmbeddableApiContext> {
|
|||
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();
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
HasUniqueId,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings';
|
||||
|
||||
export const ACTION_CLONE_PANEL = 'clonePanel';
|
||||
|
@ -41,6 +42,7 @@ export class ClonePanelAction implements Action<EmbeddableApiContext> {
|
|||
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();
|
||||
|
|
|
@ -28,6 +28,7 @@ import { DASHBOARD_CONTAINER_TYPE } from '../dashboard_container';
|
|||
import { coreServices } from '../services/kibana_services';
|
||||
import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities';
|
||||
import { dashboardCopyToDashboardActionStrings } from './_dashboard_actions_strings';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
import { CopyToDashboardModal } from './copy_to_dashboard_modal';
|
||||
|
||||
export const ACTION_COPY_TO_DASHBOARD = 'copyToDashboard';
|
||||
|
@ -59,6 +60,7 @@ export class CopyToDashboardAction implements Action<EmbeddableApiContext> {
|
|||
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();
|
||||
|
|
|
@ -13,15 +13,17 @@ import { ExpandPanelActionApi, ExpandPanelAction } from './expand_panel_action';
|
|||
describe('Expand panel action', () => {
|
||||
let action: ExpandPanelAction;
|
||||
let context: { embeddable: ExpandPanelActionApi };
|
||||
let expandPanelIdSubject: BehaviorSubject<string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
expandPanelIdSubject = new BehaviorSubject<string | undefined>(undefined);
|
||||
action = new ExpandPanelAction();
|
||||
context = {
|
||||
embeddable: {
|
||||
uuid: 'superId',
|
||||
parentApi: {
|
||||
expandPanel: jest.fn(),
|
||||
expandedPanelId: new BehaviorSubject<string | undefined>(undefined),
|
||||
expandedPanelId: expandPanelIdSubject,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -38,19 +40,22 @@ describe('Expand panel action', () => {
|
|||
expect(await action.isCompatible(emptyContext)).toBe(false);
|
||||
});
|
||||
|
||||
it('calls onChange when expandedPanelId changes', async () => {
|
||||
const onChange = jest.fn();
|
||||
action.subscribeToCompatibilityChanges(context, onChange);
|
||||
expandPanelIdSubject.next('superPanelId');
|
||||
expect(onChange).toHaveBeenCalledWith(true, action);
|
||||
});
|
||||
|
||||
it('returns the correct icon based on expanded panel id', async () => {
|
||||
expect(await action.getIconType(context)).toBe('expand');
|
||||
context.embeddable.parentApi.expandedPanelId = new BehaviorSubject<string | undefined>(
|
||||
'superPanelId'
|
||||
);
|
||||
expandPanelIdSubject.next('superPanelId');
|
||||
expect(await action.getIconType(context)).toBe('minimize');
|
||||
});
|
||||
|
||||
it('returns the correct display name based on expanded panel id', async () => {
|
||||
expect(await action.getDisplayName(context)).toBe('Maximize');
|
||||
context.embeddable.parentApi.expandedPanelId = new BehaviorSubject<string | undefined>(
|
||||
'superPanelId'
|
||||
);
|
||||
expandPanelIdSubject.next('superPanelId');
|
||||
expect(await action.getDisplayName(context)).toBe('Minimize');
|
||||
});
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
HasUniqueId,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { skip } from 'rxjs';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
|
||||
import { dashboardExpandPanelActionStrings } from './_dashboard_actions_strings';
|
||||
|
||||
|
@ -29,7 +31,8 @@ const isApiCompatible = (api: unknown | null): api is ExpandPanelActionApi =>
|
|||
export class ExpandPanelAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_EXPAND_PANEL;
|
||||
public readonly id = ACTION_EXPAND_PANEL;
|
||||
public order = 7;
|
||||
public order = 9;
|
||||
public grouping = [DASHBOARD_ACTION_GROUP];
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!isApiCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
|
@ -47,6 +50,20 @@ export class ExpandPanelAction implements Action<EmbeddableApiContext> {
|
|||
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);
|
||||
|
|
|
@ -41,7 +41,7 @@ const isApiCompatible = (api: unknown | null): api is ExportCsvActionApi =>
|
|||
export class ExportCSVAction implements Action<ExportContext> {
|
||||
public readonly id = ACTION_EXPORT_CSV;
|
||||
public readonly type = ACTION_EXPORT_CSV;
|
||||
public readonly order = 18; // right after Export in discover which is 19
|
||||
public readonly order = 18;
|
||||
|
||||
public getIconType() {
|
||||
return 'exportAction';
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
|
||||
import { Filter, FilterStateStore, type AggregateQuery, type Query } from '@kbn/es-query';
|
||||
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
FiltersNotificationAction,
|
||||
|
@ -42,7 +41,6 @@ describe('filters notification action', () => {
|
|||
|
||||
let updateFilters: (filters: Filter[]) => void;
|
||||
let updateQuery: (query: Query | AggregateQuery | undefined) => void;
|
||||
let updateViewMode: (viewMode: ViewMode) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
const filtersSubject = new BehaviorSubject<Filter[] | undefined>(undefined);
|
||||
|
@ -50,14 +48,10 @@ describe('filters notification action', () => {
|
|||
const querySubject = new BehaviorSubject<Query | AggregateQuery | undefined>(undefined);
|
||||
updateQuery = (query) => querySubject.next(query);
|
||||
|
||||
const viewModeSubject = new BehaviorSubject<ViewMode>('edit');
|
||||
updateViewMode = (viewMode) => viewModeSubject.next(viewMode);
|
||||
|
||||
action = new FiltersNotificationAction();
|
||||
context = {
|
||||
embeddable: {
|
||||
uuid: 'testId',
|
||||
viewMode: viewModeSubject,
|
||||
filters$: filtersSubject,
|
||||
query$: querySubject,
|
||||
},
|
||||
|
@ -83,22 +77,6 @@ describe('filters notification action', () => {
|
|||
expect(await action.isCompatible(context)).toBe(true);
|
||||
});
|
||||
|
||||
it('is incompatible when api is in view mode', async () => {
|
||||
updateFilters([getMockPhraseFilter('SuperField', 'SuperValue')]);
|
||||
updateQuery({ esql: 'FROM test_dataview' } as AggregateQuery);
|
||||
updateViewMode('view');
|
||||
expect(await action.isCompatible(context)).toBe(false);
|
||||
});
|
||||
|
||||
it('calls onChange when view mode changes', () => {
|
||||
const onChange = jest.fn();
|
||||
updateFilters([getMockPhraseFilter('SuperField', 'SuperValue')]);
|
||||
updateQuery({ esql: 'FROM test_dataview' } as AggregateQuery);
|
||||
action.subscribeToCompatibilityChanges(context, onChange);
|
||||
updateViewMode('view');
|
||||
expect(onChange).toHaveBeenCalledWith(false, action);
|
||||
});
|
||||
|
||||
it('calls onChange when filters change', async () => {
|
||||
const onChange = jest.fn();
|
||||
action.subscribeToCompatibilityChanges(context, onChange);
|
||||
|
|
|
@ -13,17 +13,15 @@ import { merge } from 'rxjs';
|
|||
import { isOfAggregateQueryType, isOfQueryType } from '@kbn/es-query';
|
||||
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
CanAccessViewMode,
|
||||
apiPublishesPartialUnifiedSearch,
|
||||
apiHasUniqueId,
|
||||
EmbeddableApiContext,
|
||||
HasParentApi,
|
||||
HasUniqueId,
|
||||
PublishesDataViews,
|
||||
PublishesUnifiedSearch,
|
||||
apiCanAccessViewMode,
|
||||
apiHasUniqueId,
|
||||
apiPublishesPartialUnifiedSearch,
|
||||
getInheritedViewMode,
|
||||
getViewModeSubject,
|
||||
CanLockHoverActions,
|
||||
CanAccessViewMode,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
|
@ -34,17 +32,16 @@ import { FiltersNotificationPopover } from './filters_notification_popover';
|
|||
export const BADGE_FILTERS_NOTIFICATION = 'ACTION_FILTERS_NOTIFICATION';
|
||||
|
||||
export type FiltersNotificationActionApi = HasUniqueId &
|
||||
CanAccessViewMode &
|
||||
Partial<PublishesUnifiedSearch> &
|
||||
Partial<HasParentApi<Partial<PublishesDataViews>>>;
|
||||
Partial<HasParentApi<Partial<PublishesDataViews>>> &
|
||||
Partial<CanLockHoverActions> &
|
||||
Partial<CanAccessViewMode>;
|
||||
|
||||
const isApiCompatible = (api: unknown | null): api is FiltersNotificationActionApi =>
|
||||
Boolean(
|
||||
apiHasUniqueId(api) && apiCanAccessViewMode(api) && apiPublishesPartialUnifiedSearch(api)
|
||||
);
|
||||
Boolean(apiHasUniqueId(api) && apiPublishesPartialUnifiedSearch(api));
|
||||
|
||||
const compatibilityCheck = (api: EmbeddableApiContext['embeddable']) => {
|
||||
if (!isApiCompatible(api) || getInheritedViewMode(api) !== 'edit') return false;
|
||||
if (!isApiCompatible(api)) return false;
|
||||
const query = api.query$?.value;
|
||||
return (
|
||||
(api.filters$?.value ?? []).length > 0 ||
|
||||
|
@ -97,9 +94,7 @@ export class FiltersNotificationAction implements Action<EmbeddableApiContext> {
|
|||
) {
|
||||
if (!isApiCompatible(embeddable)) return;
|
||||
return merge(
|
||||
...[embeddable.query$, embeddable.filters$, getViewModeSubject(embeddable)].filter((value) =>
|
||||
Boolean(value)
|
||||
)
|
||||
...[embeddable.query$, embeddable.filters$].filter((value) => Boolean(value))
|
||||
).subscribe(() => onChange(compatibilityCheck(embeddable), this));
|
||||
}
|
||||
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
import { AggregateQuery, Filter, FilterStateStore, Query } from '@kbn/es-query';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { FiltersNotificationActionApi } from './filters_notification_action';
|
||||
import { FiltersNotificationPopover } from './filters_notification_popover';
|
||||
import { ViewMode } from '@kbn/presentation-publishing';
|
||||
|
||||
const getMockPhraseFilter = (key: string, value: string): Filter => {
|
||||
return {
|
||||
|
@ -50,18 +50,23 @@ describe('filters notification popover', () => {
|
|||
let api: FiltersNotificationActionApi;
|
||||
let updateFilters: (filters: Filter[]) => void;
|
||||
let updateQuery: (query: Query | AggregateQuery | undefined) => void;
|
||||
let updateViewMode: (viewMode: ViewMode) => void;
|
||||
|
||||
beforeEach(async () => {
|
||||
const filtersSubject = new BehaviorSubject<Filter[] | undefined>(undefined);
|
||||
updateFilters = (filters) => filtersSubject.next(filters);
|
||||
const querySubject = new BehaviorSubject<Query | AggregateQuery | undefined>(undefined);
|
||||
updateQuery = (query) => querySubject.next(query);
|
||||
const viewModeSubject = new BehaviorSubject<ViewMode>('view');
|
||||
updateViewMode = (viewMode) => viewModeSubject.next(viewMode);
|
||||
|
||||
api = {
|
||||
uuid: 'testId',
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
filters$: filtersSubject,
|
||||
query$: querySubject,
|
||||
parentApi: {
|
||||
viewMode: viewModeSubject,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -87,7 +92,15 @@ describe('filters notification popover', () => {
|
|||
expect(await screen.findByTestId('filtersNotificationModal__query')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render an edit button when not in edit mode', async () => {
|
||||
await renderAndOpenPopover();
|
||||
expect(
|
||||
await screen.queryByTestId('filtersNotificationModal__editButton')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders an edit button when the edit panel action is compatible', async () => {
|
||||
updateViewMode('edit');
|
||||
updateFilters([getMockPhraseFilter('ay', 'oh')]);
|
||||
await renderAndOpenPopover();
|
||||
expect(await screen.findByTestId('filtersNotificationModal__editButton')).toBeInTheDocument();
|
||||
|
@ -104,6 +117,7 @@ describe('filters notification popover', () => {
|
|||
});
|
||||
|
||||
it('calls edit action execute when edit button is clicked', async () => {
|
||||
updateViewMode('edit');
|
||||
updateFilters([getMockPhraseFilter('ay', 'oh')]);
|
||||
await renderAndOpenPopover();
|
||||
const editButton = await screen.findByTestId('filtersNotificationModal__editButton');
|
||||
|
|
|
@ -26,8 +26,11 @@ import { css } from '@emotion/react';
|
|||
import { AggregateQuery, getAggregateQueryMode, isOfQueryType } from '@kbn/es-query';
|
||||
import { getEditPanelAction } from '@kbn/presentation-panel-plugin/public';
|
||||
import { FilterItems } from '@kbn/unified-search-plugin/public';
|
||||
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
apiCanLockHoverActions,
|
||||
getViewModeSubject,
|
||||
useBatchedOptionalPublishingSubjects,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { dashboardFilterNotificationActionStrings } from './_dashboard_actions_strings';
|
||||
import { FiltersNotificationActionApi } from './filters_notification_action';
|
||||
|
||||
|
@ -59,8 +62,10 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
|
|||
}
|
||||
}, [api, setDisableEditButton]);
|
||||
|
||||
const dataViews = useStateFromPublishingSubject(
|
||||
api.parentApi?.dataViews ? api.parentApi.dataViews : new BehaviorSubject(undefined)
|
||||
const [hasLockedHoverActions, dataViews, parentViewMode] = useBatchedOptionalPublishingSubjects(
|
||||
api.hasLockedHoverActions$,
|
||||
api.parentApi?.dataViews,
|
||||
getViewModeSubject(api ?? undefined)
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -69,13 +74,23 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
|
|||
<EuiButtonIcon
|
||||
color="text"
|
||||
iconType={'filter'}
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
if (apiCanLockHoverActions(api)) {
|
||||
api?.lockHoverActions(!hasLockedHoverActions);
|
||||
}
|
||||
}}
|
||||
data-test-subj={`embeddablePanelNotification-${api.uuid}`}
|
||||
aria-label={displayName}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
closePopover={() => {
|
||||
setIsPopoverOpen(false);
|
||||
if (apiCanLockHoverActions(api)) {
|
||||
api.lockHoverActions(false);
|
||||
}
|
||||
}}
|
||||
anchorPosition="upCenter"
|
||||
>
|
||||
<EuiPopoverTitle>{displayName}</EuiPopoverTitle>
|
||||
|
@ -112,8 +127,8 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
|
|||
</EuiFormRow>
|
||||
)}
|
||||
</EuiForm>
|
||||
<EuiPopoverFooter>
|
||||
{!disableEditbutton && (
|
||||
{!disableEditbutton && parentViewMode === 'edit' && (
|
||||
<EuiPopoverFooter>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
|
@ -132,8 +147,8 @@ export function FiltersNotificationPopover({ api }: { api: FiltersNotificationAc
|
|||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPopoverFooter>
|
||||
</EuiPopoverFooter>
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ interface BuildAllDashboardActionsProps {
|
|||
plugins: DashboardStartDependencies;
|
||||
}
|
||||
|
||||
export const DASHBOARD_ACTION_GROUP = { id: 'dashboard_actions', order: 10 } as const;
|
||||
|
||||
export const buildAllDashboardActions = async ({
|
||||
plugins,
|
||||
allowByValueEmbeddables,
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
HasLegacyLibraryTransforms,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
|
||||
import { dashboardAddToLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
import { coreServices } from '../services/kibana_services';
|
||||
|
@ -35,6 +36,7 @@ export class LegacyAddToLibraryAction implements Action<EmbeddableApiContext> {
|
|||
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();
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
HasLegacyLibraryTransforms,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
import { coreServices } from '../services/kibana_services';
|
||||
|
||||
export const ACTION_LEGACY_UNLINK_FROM_LIBRARY = 'legacyUnlinkFromLibrary';
|
||||
|
@ -37,6 +38,7 @@ export class LegacyUnlinkFromLibraryAction implements Action<EmbeddableApiContex
|
|||
public readonly type = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
|
||||
public readonly id = ACTION_LEGACY_UNLINK_FROM_LIBRARY;
|
||||
public order = 15;
|
||||
public grouping = [DASHBOARD_ACTION_GROUP];
|
||||
|
||||
public getDisplayName({ embeddable }: EmbeddableApiContext) {
|
||||
if (!legacyUnlinkActionIsCompatible(embeddable)) throw new IncompatibleActionError();
|
||||
|
|
|
@ -30,6 +30,7 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
|||
|
||||
import { coreServices } from '../services/kibana_services';
|
||||
import { dashboardUnlinkFromLibraryActionStrings } from './_dashboard_actions_strings';
|
||||
import { DASHBOARD_ACTION_GROUP } from '.';
|
||||
|
||||
export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary';
|
||||
|
||||
|
@ -54,6 +55,7 @@ export class UnlinkFromLibraryAction implements Action<EmbeddableApiContext> {
|
|||
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();
|
||||
|
|
|
@ -73,7 +73,7 @@ export function initializeTrackPanel(untilEmbeddableLoaded: (id: string) => Prom
|
|||
};
|
||||
return;
|
||||
}
|
||||
panelRef.scrollIntoView({ block: 'nearest' });
|
||||
panelRef.scrollIntoView({ block: 'start' });
|
||||
});
|
||||
},
|
||||
scrollToTop: () => {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
.dshLayout--editing {
|
||||
.react-resizable-handle {
|
||||
@include size($euiSizeL);
|
||||
z-index: $euiZLevel1; /* 1 */
|
||||
z-index: $euiZLevel2; /* 1 */
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-right: $euiSizeS;
|
||||
|
@ -33,6 +33,10 @@
|
|||
*/
|
||||
.dshLayout-isMaximizedPanel {
|
||||
height: 100% !important; /* 1. */
|
||||
|
||||
.embPanel__hoverActionsLeft {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,8 +44,7 @@
|
|||
* Shifting the rendered panels offscreen prevents a quick flash when redrawing the panels on minimize
|
||||
*/
|
||||
.dshDashboardGrid__item--hidden {
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
transform: translate(-9999px, -9999px);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,13 +101,26 @@
|
|||
*/
|
||||
&.resizing,
|
||||
&.react-draggable-dragging {
|
||||
z-index: $euiZLevel2 !important;
|
||||
z-index: $euiZLevel3 !important;
|
||||
}
|
||||
|
||||
&.react-draggable-dragging {
|
||||
transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance;
|
||||
@include euiBottomShadowLarge;
|
||||
border-radius: $euiBorderRadius; // keeps shadow within bounds
|
||||
|
||||
.embPanel__hoverActionsWrapper {
|
||||
z-index: $euiZLevel9;
|
||||
top: -$euiSizeXL;
|
||||
|
||||
.embPanel__hoverActions:has(.embPanel--dragHandle) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.embPanel__hoverActions:not(:has(.embPanel--dragHandle)) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,24 +4,42 @@
|
|||
* .embPanel--editing doesn't get updating without a hard refresh
|
||||
*/
|
||||
|
||||
.dshDashboardGrid__item {
|
||||
scroll-margin-top: calc((var(--euiFixedHeadersOffset, 100) * 2) + $euiSizeS);
|
||||
scroll-margin-bottom: $euiSizeS;
|
||||
.dshLayout--editing {
|
||||
// change the style of the hover actions border to a dashed line in edit mode
|
||||
.embPanel__hoverActionsAnchor {
|
||||
.embPanel__hoverActionsWrapper {
|
||||
.embPanel__hoverActions {
|
||||
border-color: $euiColorMediumShade;
|
||||
border-style: dashed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LAYOUT MODES
|
||||
// Adjust borders/etc... for non-spaced out and expanded panels
|
||||
.dshLayout-withoutMargins {
|
||||
.embPanel {
|
||||
.embPanel,
|
||||
.embPanel__hoverActionsAnchor {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.embPanel__content {
|
||||
border-radius: 0;
|
||||
&.dshLayout--editing {
|
||||
.embPanel__hoverActionsAnchor:hover {
|
||||
outline: 1px dashed $euiColorMediumShade;
|
||||
}
|
||||
}
|
||||
|
||||
.dshDashboardGrid__item--highlighted {
|
||||
.embPanel__hoverActionsAnchor:hover {
|
||||
outline: $euiBorderThin;
|
||||
z-index: $euiZLevel2;
|
||||
}
|
||||
|
||||
.embPanel__content,
|
||||
.dshDashboardGrid__item--highlighted,
|
||||
.lnsExpressionRenderer {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +53,20 @@
|
|||
background-color: unset;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.embPanel__hoverActions {
|
||||
.embPanel--dragHandle {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide hover actions when dashboard has an overlay
|
||||
.dshDashboardGrid__item--blurred,
|
||||
.dshDashboardGrid__item--focused {
|
||||
.embPanel__hoverActions {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes highlightOutline {
|
||||
|
@ -52,10 +84,11 @@
|
|||
}
|
||||
|
||||
.dshDashboardGrid__item--highlighted {
|
||||
border-radius: $euiSizeXS;
|
||||
animation-name: highlightOutline;
|
||||
animation-duration: 4s;
|
||||
animation-timing-function: ease-out;
|
||||
// keeps outline from getting cut off by other panels without margins
|
||||
z-index: 999 !important;
|
||||
.embPanel {
|
||||
border-radius: $euiSizeXS;
|
||||
animation-name: highlightOutline;
|
||||
animation-duration: 4s;
|
||||
animation-timing-function: ease-out;
|
||||
z-index: $euiZLevel2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
|
|||
rowHeight={DASHBOARD_GRID_HEIGHT}
|
||||
margin={useMargins ? [DASHBOARD_MARGIN_SIZE, DASHBOARD_MARGIN_SIZE] : [0, 0]}
|
||||
draggableHandle={'.embPanel--dragHandle'}
|
||||
useCSSTransforms={false}
|
||||
>
|
||||
{panelComponents}
|
||||
</ResponsiveReactGridLayout>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { css } from '@emotion/react';
|
|||
import { EmbeddablePanel, ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import { DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
|
||||
import { DashboardPanelState } from '../../../../common';
|
||||
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
|
||||
import { embeddableService, presentationUtilService } from '../../../services/kibana_services';
|
||||
|
@ -91,12 +92,21 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
|
|||
}
|
||||
}, [id, dashboardApi, scrollToPanelId, highlightPanelId, ref, blurPanel]);
|
||||
|
||||
const dashboardContainerTopOffset =
|
||||
(document.querySelector('.dashboardContainer') as HTMLDivElement)?.offsetTop || 0;
|
||||
const globalNavTopOffset =
|
||||
(document.querySelector('#app-fixed-viewport') as HTMLDivElement)?.offsetTop || 0;
|
||||
|
||||
const focusStyles = blurPanel
|
||||
? css`
|
||||
pointer-events: none;
|
||||
opacity: 0.25;
|
||||
`
|
||||
: undefined;
|
||||
: css`
|
||||
scroll-margin-top: ${dashboardContainerTopOffset +
|
||||
globalNavTopOffset +
|
||||
DASHBOARD_MARGIN_SIZE}px;
|
||||
`;
|
||||
|
||||
const renderedEmbeddable = useMemo(() => {
|
||||
const panelProps = {
|
||||
|
|
|
@ -23,3 +23,9 @@
|
|||
.dashboardViewport--screenshotMode .controlsWrapper--empty {
|
||||
display:none
|
||||
}
|
||||
|
||||
.dshDashboardViewportWrapper--isFullscreen {
|
||||
.dshDashboardGrid__item--expanded {
|
||||
padding: $euiSizeS;
|
||||
}
|
||||
}
|
|
@ -57,6 +57,7 @@ export const DashboardViewportComponent = () => {
|
|||
viewMode,
|
||||
useMargins,
|
||||
uuid,
|
||||
fullScreenMode,
|
||||
] = useBatchedPublishingSubjects(
|
||||
dashboardApi.controlGroupApi$,
|
||||
dashboardApi.panelTitle,
|
||||
|
@ -66,7 +67,8 @@ export const DashboardViewportComponent = () => {
|
|||
dashboardApi.panels$,
|
||||
dashboardApi.viewMode,
|
||||
dashboardApi.useMargins$,
|
||||
dashboardApi.uuid$
|
||||
dashboardApi.uuid$,
|
||||
dashboardApi.fullScreenMode$
|
||||
);
|
||||
|
||||
const panelCount = useMemo(() => {
|
||||
|
@ -114,6 +116,7 @@ export const DashboardViewportComponent = () => {
|
|||
<div
|
||||
className={classNames('dshDashboardViewportWrapper', {
|
||||
'dshDashboardViewportWrapper--defaultBg': !useMargins,
|
||||
'dshDashboardViewportWrapper--isFullscreen': fullScreenMode,
|
||||
})}
|
||||
>
|
||||
{viewMode !== ViewMode.PRINT ? (
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
.dashboardTopNav {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: $euiZLevel2;
|
||||
z-index: $euiZLevel3;
|
||||
top: var(--euiFixedHeadersOffset, 0);
|
||||
background: $euiPageBackgroundColor;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ export const ACTION_VIEW_SAVED_SEARCH = 'ACTION_VIEW_SAVED_SEARCH';
|
|||
export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
|
||||
public id = ACTION_VIEW_SAVED_SEARCH;
|
||||
public readonly type = ACTION_VIEW_SAVED_SEARCH;
|
||||
public readonly order = 20; // Same order as ACTION_OPEN_IN_DISCOVER
|
||||
|
||||
constructor(
|
||||
private readonly application: ApplicationStart,
|
||||
|
@ -43,7 +44,7 @@ export class ViewSavedSearchAction implements Action<EmbeddableApiContext> {
|
|||
}
|
||||
|
||||
getIconType(): string | undefined {
|
||||
return 'inspect';
|
||||
return 'discoverApp';
|
||||
}
|
||||
|
||||
async isCompatible({ embeddable }: EmbeddableApiContext) {
|
||||
|
|
|
@ -245,6 +245,8 @@ export const legacyEmbeddableToApi = (
|
|||
return !isInputControl && !isMarkdown && !isImage && !isLinks;
|
||||
};
|
||||
|
||||
const hasLockedHoverActions$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
return {
|
||||
api: {
|
||||
parentApi: parentApi as LegacyEmbeddableAPI['parentApi'],
|
||||
|
@ -270,6 +272,9 @@ export const legacyEmbeddableToApi = (
|
|||
disabledActionIds,
|
||||
setDisabledActionIds: (ids) => disabledActionIds.next(ids),
|
||||
|
||||
hasLockedHoverActions$,
|
||||
lockHoverActions: (lock: boolean) => hasLockedHoverActions$.next(lock),
|
||||
|
||||
panelTitle,
|
||||
setPanelTitle,
|
||||
defaultPanelTitle,
|
||||
|
|
|
@ -148,6 +148,8 @@ export abstract class Embeddable<
|
|||
canUnlinkFromLibrary: this.canUnlinkFromLibrary,
|
||||
isCompatibleWithUnifiedSearch: this.isCompatibleWithUnifiedSearch,
|
||||
savedObjectId: this.savedObjectId,
|
||||
hasLockedHoverActions$: this.hasLockedHoverActions$,
|
||||
lockHoverActions: this.lockHoverActions,
|
||||
} = api);
|
||||
|
||||
setTimeout(() => {
|
||||
|
@ -191,6 +193,8 @@ export abstract class Embeddable<
|
|||
public canUnlinkFromLibrary: LegacyEmbeddableAPI['canUnlinkFromLibrary'];
|
||||
public isCompatibleWithUnifiedSearch: LegacyEmbeddableAPI['isCompatibleWithUnifiedSearch'];
|
||||
public savedObjectId: LegacyEmbeddableAPI['savedObjectId'];
|
||||
public hasLockedHoverActions$: LegacyEmbeddableAPI['hasLockedHoverActions$'];
|
||||
public lockHoverActions: LegacyEmbeddableAPI['lockHoverActions'];
|
||||
|
||||
public async getEditHref(): Promise<string | undefined> {
|
||||
return this.getOutput().editUrl ?? undefined;
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
PublishesSavedObjectId,
|
||||
HasLegacyLibraryTransforms,
|
||||
EmbeddableAppContext,
|
||||
CanLockHoverActions,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EmbeddableInput } from '../../../common/types';
|
||||
|
@ -58,7 +59,8 @@ export type LegacyEmbeddableAPI = HasType &
|
|||
Partial<HasLegacyLibraryTransforms> &
|
||||
HasParentApi<DefaultPresentationPanelApi['parentApi']> &
|
||||
EmbeddableHasTimeRange &
|
||||
PublishesSavedObjectId;
|
||||
PublishesSavedObjectId &
|
||||
CanLockHoverActions;
|
||||
|
||||
export interface EmbeddableOutput {
|
||||
// Whether the embeddable is actively loading.
|
||||
|
|
|
@ -194,6 +194,8 @@ describe('react embeddable renderer', () => {
|
|||
resetUnsavedChanges: expect.any(Function),
|
||||
snapshotRuntimeState: expect.any(Function),
|
||||
phase$: expect.any(Object),
|
||||
hasLockedHoverActions$: expect.any(Object),
|
||||
lockHoverActions: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -122,11 +122,16 @@ export const ReactEmbeddableRenderer = <
|
|||
const setApi = (
|
||||
apiRegistration: SetReactEmbeddableApiRegistration<SerializedState, RuntimeState, Api>
|
||||
) => {
|
||||
const hasLockedHoverActions$ = new BehaviorSubject(false);
|
||||
return {
|
||||
...apiRegistration,
|
||||
uuid,
|
||||
phase$,
|
||||
parentApi,
|
||||
hasLockedHoverActions$,
|
||||
lockHoverActions: (lock: boolean) => {
|
||||
hasLockedHoverActions$.next(lock);
|
||||
},
|
||||
type: factory.type,
|
||||
} as unknown as Api;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from '@kbn/presentation-containers';
|
||||
import { DefaultPresentationPanelApi } from '@kbn/presentation-panel-plugin/public/panel_component/types';
|
||||
import {
|
||||
CanLockHoverActions,
|
||||
HasType,
|
||||
PublishesPhaseEvents,
|
||||
PublishesUnsavedChanges,
|
||||
|
@ -48,7 +49,7 @@ export type SetReactEmbeddableApiRegistration<
|
|||
SerializedState,
|
||||
RuntimeState
|
||||
>
|
||||
> = Omit<Api, 'uuid' | 'parent' | 'type' | 'phase$'>;
|
||||
> = Omit<Api, 'uuid' | 'parent' | 'type' | 'phase$' | keyof CanLockHoverActions>;
|
||||
|
||||
/**
|
||||
* Defines the subset of the default embeddable API that the `buildApi` method uses, which allows implementors
|
||||
|
|
|
@ -248,6 +248,7 @@ export const getLinksEmbeddableFactory = () => {
|
|||
data-shared-item
|
||||
data-rendering-count={1}
|
||||
data-test-subj="links--component"
|
||||
borderRadius="none"
|
||||
>
|
||||
<EuiListGroup
|
||||
maxWidth={false}
|
||||
|
|
|
@ -30,7 +30,7 @@ const isApiCompatible = (api: unknown | null): api is InspectPanelActionApi => {
|
|||
export class InspectPanelAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_INSPECT_PANEL;
|
||||
public readonly id = ACTION_INSPECT_PANEL;
|
||||
public order = 20;
|
||||
public order = 19; // right after Explore in Discover which is 20
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
|
|
@ -33,14 +33,8 @@ const isApiCompatible = (api: unknown | null): api is RemovePanelActionApi =>
|
|||
export class RemovePanelAction implements Action<EmbeddableApiContext> {
|
||||
public readonly type = ACTION_REMOVE_PANEL;
|
||||
public readonly id = ACTION_REMOVE_PANEL;
|
||||
public order = 1;
|
||||
|
||||
public grouping = [
|
||||
{
|
||||
id: 'delete_panel_action',
|
||||
order: 1,
|
||||
},
|
||||
];
|
||||
public order = 0;
|
||||
public grouping = [{ id: 'remove_panel_group', order: 1 }];
|
||||
|
||||
constructor() {}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
height: 100%;
|
||||
min-height: $euiSizeL + 2px; // + 2px to account for border
|
||||
position: relative;
|
||||
border: none;
|
||||
outline: $euiBorderThin;
|
||||
|
||||
&-isLoading {
|
||||
// completely center the loading indicator
|
||||
|
@ -44,6 +46,13 @@
|
|||
display: flex;
|
||||
// ensure menu button is on the right even if the title doesn't exist
|
||||
justify-content: flex-end;
|
||||
height: $euiSizeL;
|
||||
}
|
||||
|
||||
.embPanel__header + .embPanel__content {
|
||||
border-radius: 0;
|
||||
border-bottom-left-radius: $euiBorderRadius;
|
||||
border-bottom-right-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
.embPanel__title {
|
||||
|
@ -112,7 +121,6 @@
|
|||
&:focus {
|
||||
background-color: transparentize($euiColorLightestShade, .5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.embPanel__optionsMenuPopover-loading {
|
||||
|
@ -129,43 +137,20 @@
|
|||
font-size: $euiSizeL;
|
||||
}
|
||||
|
||||
.embPanel .embPanel__optionsMenuButton {
|
||||
opacity: 0; /* 1 */
|
||||
|
||||
&:focus {
|
||||
opacity: 1; /* 2 */
|
||||
}
|
||||
}
|
||||
|
||||
.embPanel:hover {
|
||||
.embPanel__optionsMenuButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// EDITING MODE
|
||||
|
||||
.embPanel--editing {
|
||||
transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
|
||||
outline: 1px dashed $euiColorMediumShade;
|
||||
|
||||
.embPanel--dragHandle {
|
||||
transition: background-color $euiAnimSpeedFast $euiAnimSlightResistance;
|
||||
|
||||
&:hover {
|
||||
.embPanel--dragHandle:hover {
|
||||
background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7));
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.embPanel__content {
|
||||
border-radius: 0;
|
||||
border-bottom-left-radius: $euiBorderRadius;
|
||||
border-bottom-right-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
.embPanel__optionsMenuButton {
|
||||
opacity: 1; /* 3 */
|
||||
}
|
||||
}
|
||||
|
||||
// LOADING and ERRORS
|
||||
|
@ -184,3 +169,57 @@
|
|||
padding-left: $euiSizeS;
|
||||
z-index: $euiZLevel1;
|
||||
}
|
||||
|
||||
.embPanel__hoverActionsAnchor {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.embPanel__hoverActionsWrapper {
|
||||
height: $euiSizeXL;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 $euiSize;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none; // Prevent hover actions wrapper from blocking interactions with other panels
|
||||
}
|
||||
|
||||
.embPanel__hoverActions {
|
||||
opacity: 0;
|
||||
padding: calc($euiSizeXS - 1px);
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
border: $euiBorderThin;
|
||||
|
||||
background-color: $euiColorEmptyShade;
|
||||
height: $euiSizeXL;
|
||||
|
||||
pointer-events: all; // Re-enable pointer-events for hover actions
|
||||
}
|
||||
|
||||
.embPanel--dragHandle {
|
||||
cursor: move;
|
||||
|
||||
img {
|
||||
pointer-events: all !important;
|
||||
}
|
||||
}
|
||||
|
||||
.embPanel__descriptionTooltipAnchor {
|
||||
padding: $euiSizeXS;
|
||||
}
|
||||
|
||||
&:hover .embPanel__hoverActionsWrapper,
|
||||
&:focus-within .embPanel__hoverActionsWrapper,
|
||||
.embPanel__hoverActionsWrapper--lockHoverActions {
|
||||
z-index: $euiZLevel9;
|
||||
top: -$euiSizeXL;
|
||||
|
||||
.embPanel__hoverActions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiPopover,
|
||||
EuiSkeletonText,
|
||||
} from '@elastic/eui';
|
||||
import { Action, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import {
|
||||
getViewModeSubject,
|
||||
useBatchedOptionalPublishingSubjects,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { uiActions } from '../../kibana_services';
|
||||
import { contextMenuTrigger, CONTEXT_MENU_TRIGGER } from '../../panel_actions';
|
||||
import { getContextMenuAriaLabel } from '../presentation_panel_strings';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
|
||||
export const PresentationPanelContextMenu = ({
|
||||
api,
|
||||
index,
|
||||
getActions,
|
||||
actionPredicate,
|
||||
}: {
|
||||
index?: number;
|
||||
api: DefaultPresentationPanelApi;
|
||||
getActions: PresentationPanelInternalProps['getActions'];
|
||||
actionPredicate?: (actionId: string) => boolean;
|
||||
}) => {
|
||||
const [menuPanelsLoading, setMenuPanelsLoading] = useState(false);
|
||||
const [contextMenuActions, setContextMenuActions] = useState<Array<Action<object>>>([]);
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean | undefined>(undefined);
|
||||
const [contextMenuPanels, setContextMenuPanels] = useState<EuiContextMenuPanelDescriptor[]>([]);
|
||||
|
||||
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<Action<object>> = 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 = (
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
className="embPanel__optionsMenuButton"
|
||||
data-test-subj="embeddablePanelToggleMenuIcon"
|
||||
aria-label={getContextMenuAriaLabel(title, index)}
|
||||
onClick={() => setIsContextMenuOpen((isOpen) => !isOpen)}
|
||||
iconType={'boxesHorizontal'}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
repositionOnScroll
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
button={ContextMenuButton}
|
||||
isOpen={isContextMenuOpen}
|
||||
className={contextMenuClasses}
|
||||
closePopover={() => setIsContextMenuOpen(false)}
|
||||
data-test-subj={
|
||||
isContextMenuOpen ? 'embeddablePanelContextMenuOpen' : 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
>
|
||||
{menuPanelsLoading ? (
|
||||
<EuiContextMenuPanel
|
||||
className="embPanel__optionsMenuPopover-loading"
|
||||
title={i18n.translate('presentationPanel.contextMenu.loadingTitle', {
|
||||
defaultMessage: 'Options',
|
||||
})}
|
||||
>
|
||||
<EuiContextMenuItem>
|
||||
<EuiSkeletonText />
|
||||
</EuiContextMenuItem>
|
||||
</EuiContextMenuPanel>
|
||||
) : (
|
||||
<EuiContextMenu
|
||||
data-test-subj="presentationPanelContextMenuItems"
|
||||
initialPanelId="mainMenu"
|
||||
panels={contextMenuPanels}
|
||||
/>
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -13,7 +13,6 @@ import classNames from 'classnames';
|
|||
import React from 'react';
|
||||
import { getAriaLabelForTitle } from '../presentation_panel_strings';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
import { PresentationPanelContextMenu } from './presentation_panel_context_menu';
|
||||
import { PresentationPanelTitle } from './presentation_panel_title';
|
||||
import { usePresentationPanelHeaderActions } from './use_presentation_panel_header_actions';
|
||||
|
||||
|
@ -24,23 +23,18 @@ export type PresentationPanelHeaderProps<ApiType extends DefaultPresentationPane
|
|||
hideTitle?: boolean;
|
||||
panelTitle?: string;
|
||||
panelDescription?: string;
|
||||
} & Pick<
|
||||
PresentationPanelInternalProps,
|
||||
'index' | 'showBadges' | 'getActions' | 'actionPredicate' | 'showNotifications'
|
||||
>;
|
||||
} & Pick<PresentationPanelInternalProps, 'showBadges' | 'getActions' | 'showNotifications'>;
|
||||
|
||||
export const PresentationPanelHeader = <
|
||||
ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi
|
||||
>({
|
||||
api,
|
||||
index,
|
||||
viewMode,
|
||||
headerId,
|
||||
getActions,
|
||||
hideTitle,
|
||||
panelTitle,
|
||||
panelDescription,
|
||||
actionPredicate,
|
||||
showBadges = true,
|
||||
showNotifications = true,
|
||||
}: PresentationPanelHeaderProps<ApiType>) => {
|
||||
|
@ -52,11 +46,9 @@ export const PresentationPanelHeader = <
|
|||
);
|
||||
|
||||
const showPanelBar =
|
||||
!hideTitle ||
|
||||
panelDescription ||
|
||||
viewMode !== 'view' ||
|
||||
badgeElements.length > 0 ||
|
||||
notificationElements.length > 0;
|
||||
(!hideTitle && panelTitle) || badgeElements.length > 0 || notificationElements.length > 0;
|
||||
|
||||
if (!showPanelBar) return null;
|
||||
|
||||
const ariaLabel = getAriaLabelForTitle(showPanelBar ? panelTitle : undefined);
|
||||
const ariaLabelElement = (
|
||||
|
@ -66,6 +58,7 @@ export const PresentationPanelHeader = <
|
|||
);
|
||||
|
||||
const headerClasses = classNames('embPanel__header', {
|
||||
'embPanel--dragHandle': viewMode === 'edit',
|
||||
'embPanel__header--floater': !showPanelBar,
|
||||
});
|
||||
|
||||
|
@ -73,19 +66,6 @@ export const PresentationPanelHeader = <
|
|||
'embPanel--dragHandle': viewMode === 'edit',
|
||||
});
|
||||
|
||||
const contextMenuElement = (
|
||||
<PresentationPanelContextMenu {...{ index, api, getActions, actionPredicate }} />
|
||||
);
|
||||
|
||||
if (!showPanelBar) {
|
||||
return (
|
||||
<div data-test-subj={`embeddablePanelHeading`} className={headerClasses}>
|
||||
{contextMenuElement}
|
||||
{ariaLabelElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<figcaption
|
||||
className={headerClasses}
|
||||
|
@ -103,7 +83,6 @@ export const PresentationPanelHeader = <
|
|||
{showBadges && badgeElements}
|
||||
</h2>
|
||||
{showNotifications && notificationElements}
|
||||
{contextMenuElement}
|
||||
</figcaption>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,563 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import React, {
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiNotificationBadge,
|
||||
EuiPopover,
|
||||
EuiToolTip,
|
||||
IconType,
|
||||
} from '@elastic/eui';
|
||||
import { ActionExecutionContext, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
import {
|
||||
apiCanLockHoverActions,
|
||||
EmbeddableApiContext,
|
||||
getViewModeSubject,
|
||||
useBatchedOptionalPublishingSubjects,
|
||||
ViewMode,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { css } from '@emotion/react';
|
||||
import { ActionWithContext } from '@kbn/ui-actions-plugin/public/context_menu/build_eui_context_menu_panels';
|
||||
import { uiActions } from '../../kibana_services';
|
||||
import {
|
||||
contextMenuTrigger,
|
||||
CONTEXT_MENU_TRIGGER,
|
||||
panelNotificationTrigger,
|
||||
PANEL_NOTIFICATION_TRIGGER,
|
||||
} from '../../panel_actions';
|
||||
import { getContextMenuAriaLabel } from '../presentation_panel_strings';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
import { AnyApiAction } from '../../panel_actions/types';
|
||||
|
||||
const QUICK_ACTION_IDS = {
|
||||
edit: [
|
||||
'editPanel',
|
||||
'ACTION_CONFIGURE_IN_LENS',
|
||||
'ACTION_CUSTOMIZE_PANEL',
|
||||
'ACTION_OPEN_IN_DISCOVER',
|
||||
'ACTION_VIEW_SAVED_SEARCH',
|
||||
],
|
||||
view: ['ACTION_OPEN_IN_DISCOVER', 'ACTION_VIEW_SAVED_SEARCH', 'openInspector', 'togglePanel'],
|
||||
} as const;
|
||||
|
||||
const ALLOWED_NOTIFICATIONS = ['ACTION_FILTERS_NOTIFICATION'] as const;
|
||||
|
||||
const ALL_ROUNDED_CORNERS = `border-radius: ${euiThemeVars.euiBorderRadius};
|
||||
`;
|
||||
const TOP_ROUNDED_CORNERS = `border-top-left-radius: ${euiThemeVars.euiBorderRadius};
|
||||
border-top-right-radius: ${euiThemeVars.euiBorderRadius};
|
||||
border-bottom: 0 !important;
|
||||
`;
|
||||
|
||||
const createClickHandler =
|
||||
(action: AnyApiAction, context: ActionExecutionContext<EmbeddableApiContext>) =>
|
||||
(event: React.MouseEvent) => {
|
||||
if (event.currentTarget instanceof HTMLAnchorElement) {
|
||||
// from react-router's <Link/>
|
||||
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<AnyApiAction[]>([]);
|
||||
const [contextMenuPanels, setContextMenuPanels] = useState<EuiContextMenuPanelDescriptor[]>([]);
|
||||
const [showNotification, setShowNotification] = useState<boolean>(false);
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
|
||||
const [notifications, setNotifications] = useState<AnyApiAction[]>([]);
|
||||
const hoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const anchorRef = useRef<HTMLDivElement | null>(null);
|
||||
const leftHoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const rightHoverActionsRef = useRef<HTMLDivElement | null>(null);
|
||||
const [combineHoverActions, setCombineHoverActions] = useState<boolean>(false);
|
||||
const [borderStyles, setBorderStyles] = useState<string>(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,
|
||||
},
|
||||
})
|
||||
) : (
|
||||
<EuiNotificationBadge
|
||||
data-test-subj={`embeddablePanelNotification-${notification.id}`}
|
||||
key={notification.id}
|
||||
style={{ marginTop: euiThemeVars.euiSizeXS, marginRight: euiThemeVars.euiSizeXS }}
|
||||
onClick={() =>
|
||||
notification.execute({ embeddable: api, trigger: panelNotificationTrigger })
|
||||
}
|
||||
>
|
||||
{notification.getDisplayName({ embeddable: api, trigger: panelNotificationTrigger })}
|
||||
</EuiNotificationBadge>
|
||||
);
|
||||
|
||||
if (notification.getDisplayNameTooltip) {
|
||||
const tooltip = notification.getDisplayNameTooltip({
|
||||
embeddable: api,
|
||||
trigger: panelNotificationTrigger,
|
||||
});
|
||||
|
||||
if (tooltip) {
|
||||
notificationComponent = (
|
||||
<EuiToolTip position="top" delay="regular" content={tooltip} key={notification.id}>
|
||||
{notificationComponent}
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 = (
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
data-test-subj="embeddablePanelToggleMenuIcon"
|
||||
aria-label={getContextMenuAriaLabel(title, index)}
|
||||
onClick={() => {
|
||||
setIsContextMenuOpen(!isContextMenuOpen);
|
||||
if (apiCanLockHoverActions(api)) {
|
||||
api?.lockHoverActions(!hasLockedHoverActions);
|
||||
}
|
||||
}}
|
||||
iconType="boxesVertical"
|
||||
/>
|
||||
);
|
||||
|
||||
const dragHandle = (
|
||||
<EuiIcon
|
||||
type="move"
|
||||
color="text"
|
||||
className={`${viewMode === 'edit' ? 'embPanel--dragHandle' : ''}`}
|
||||
aria-label={i18n.translate('presentationPanel.dragHandle', {
|
||||
defaultMessage: 'Move panel',
|
||||
})}
|
||||
data-test-subj="embeddablePanelDragHandle"
|
||||
css={css`
|
||||
margin: ${euiThemeVars.euiSizeXS};
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseOver={updateCombineHoverActions}
|
||||
onFocus={updateCombineHoverActions}
|
||||
ref={anchorRef}
|
||||
className="embPanel__hoverActionsAnchor"
|
||||
data-test-embeddable-id={api?.uuid}
|
||||
data-test-subj={`embeddablePanelHoverActions-${(title || defaultTitle || '').replace(
|
||||
/\s/g,
|
||||
''
|
||||
)}`}
|
||||
>
|
||||
{children}
|
||||
{api ? (
|
||||
<div
|
||||
ref={hoverActionsRef}
|
||||
css={css`anchorStyles`}
|
||||
className={classNames('embPanel__hoverActionsWrapper', {
|
||||
'embPanel__hoverActionsWrapper--lockHoverActions': hasLockedHoverActions,
|
||||
})}
|
||||
>
|
||||
{viewMode === 'edit' && !combineHoverActions ? (
|
||||
<div
|
||||
ref={leftHoverActionsRef}
|
||||
data-test-subj="embPanel__hoverActions__left"
|
||||
className={classNames(
|
||||
'embPanel__hoverActions',
|
||||
'embPanel__hoverActionsLeft',
|
||||
className
|
||||
)}
|
||||
css={css`
|
||||
${borderStyles}
|
||||
`}
|
||||
>
|
||||
{dragHandle}
|
||||
</div>
|
||||
) : (
|
||||
<div /> // necessary for the right hover actions to align correctly when left hover actions are not present
|
||||
)}
|
||||
<div
|
||||
ref={rightHoverActionsRef}
|
||||
data-test-subj="embPanel__hoverActions__right"
|
||||
className={classNames(
|
||||
'embPanel__hoverActions',
|
||||
'embPanel__hoverActionsRight',
|
||||
className
|
||||
)}
|
||||
css={css`
|
||||
${borderStyles}
|
||||
`}
|
||||
>
|
||||
{viewMode === 'edit' && combineHoverActions && dragHandle}
|
||||
{showNotifications && notificationElements}
|
||||
{showDescription && (
|
||||
<EuiIconTip
|
||||
title={!hideTitle ? title || undefined : undefined}
|
||||
content={description}
|
||||
delay="regular"
|
||||
position="top"
|
||||
anchorClassName="embPanel__descriptionTooltipAnchor"
|
||||
data-test-subj="embeddablePanelDescriptionTooltip"
|
||||
type="iInCircle"
|
||||
/>
|
||||
)}
|
||||
{quickActionElements.map(
|
||||
({ iconType, 'data-test-subj': dataTestSubj, onClick, name }, i) => (
|
||||
<EuiToolTip key={`main_action_${dataTestSubj}_${api?.uuid}`} content={name}>
|
||||
<EuiButtonIcon
|
||||
iconType={iconType}
|
||||
color="text"
|
||||
onClick={onClick as MouseEventHandler}
|
||||
data-test-subj={dataTestSubj}
|
||||
aria-label={name as string}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)
|
||||
)}
|
||||
{contextMenuPanels.length ? (
|
||||
<EuiPopover
|
||||
repositionOnScroll
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downRight"
|
||||
button={ContextMenuButton}
|
||||
isOpen={isContextMenuOpen}
|
||||
className={contextMenuClasses}
|
||||
closePopover={onClose}
|
||||
data-test-subj={
|
||||
isContextMenuOpen
|
||||
? 'embeddablePanelContextMenuOpen'
|
||||
: 'embeddablePanelContextMenuClosed'
|
||||
}
|
||||
focusTrapProps={{
|
||||
closeOnMouseup: true,
|
||||
clickOutsideDisables: false,
|
||||
onClickOutside: onClose,
|
||||
}}
|
||||
>
|
||||
<EuiContextMenu
|
||||
data-test-subj="presentationPanelContextMenuItems"
|
||||
initialPanelId={'mainMenu'}
|
||||
panels={contextMenuPanels}
|
||||
/>
|
||||
</EuiPopover>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -131,8 +131,8 @@ export const PresentationPanelTitle = ({
|
|||
}, [api, onClick]);
|
||||
|
||||
const describedPanelTitleElement = useMemo(() => {
|
||||
if (hideTitle) return null;
|
||||
if (!panelDescription) {
|
||||
if (hideTitle) return null;
|
||||
return (
|
||||
<span data-test-subj="embeddablePanelTitleInner" className="embPanel__titleInner">
|
||||
{panelTitleElement}
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
import { AnyApiAction } from '../../panel_actions/types';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
|
||||
|
||||
const disabledNotifications = ['ACTION_FILTERS_NOTIFICATION'];
|
||||
|
||||
export const usePresentationPanelHeaderActions = <
|
||||
ApiType extends DefaultPresentationPanelApi = DefaultPresentationPanelApi
|
||||
>(
|
||||
|
@ -47,10 +49,8 @@ export const usePresentationPanelHeaderActions = <
|
|||
embeddable: api,
|
||||
})) as AnyApiAction[]) ?? [];
|
||||
|
||||
const disabledActions = api.disabledActionIds?.value;
|
||||
if (disabledActions) {
|
||||
nextActions = nextActions.filter((badge) => disabledActions.indexOf(badge.id) === -1);
|
||||
}
|
||||
const disabledActions = (api.disabledActionIds?.value ?? []).concat(disabledNotifications);
|
||||
nextActions = nextActions.filter((badge) => disabledActions.indexOf(badge.id) === -1);
|
||||
return nextActions;
|
||||
};
|
||||
|
||||
|
@ -85,8 +85,8 @@ export const usePresentationPanelHeaderActions = <
|
|||
);
|
||||
for (const badge of frequentlyChangingBadges) {
|
||||
subscriptions.add(
|
||||
badge.subscribeToCompatibilityChanges(apiContext, (isComptaible, action) =>
|
||||
handleActionCompatibilityChange('badge', isComptaible, action as AnyApiAction)
|
||||
badge.subscribeToCompatibilityChanges(apiContext, (isCompatible, action) =>
|
||||
handleActionCompatibilityChange('badge', isCompatible, action as AnyApiAction)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -97,11 +97,12 @@ export const usePresentationPanelHeaderActions = <
|
|||
apiContext
|
||||
);
|
||||
for (const notification of frequentlyChangingNotifications) {
|
||||
subscriptions.add(
|
||||
notification.subscribeToCompatibilityChanges(apiContext, (isComptaible, action) =>
|
||||
handleActionCompatibilityChange('notification', isComptaible, action as AnyApiAction)
|
||||
)
|
||||
);
|
||||
if (!disabledNotifications.includes(notification.id))
|
||||
subscriptions.add(
|
||||
notification.subscribeToCompatibilityChanges(apiContext, (isCompatible, action) =>
|
||||
handleActionCompatibilityChange('notification', isCompatible, action as AnyApiAction)
|
||||
)
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('Presentation panel', () => {
|
|||
<PresentationPanel {...props} Component={getMockPresentationPanelCompatibleComponent(api)} />
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('embeddablePanelToggleMenuIcon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('embeddablePanel')).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -223,12 +223,10 @@ describe('Presentation panel', () => {
|
|||
viewMode: new BehaviorSubject<ViewMode>('view'),
|
||||
};
|
||||
await renderPresentationPanel({ api });
|
||||
const header = await screen.findByTestId('embeddablePanelHeading');
|
||||
const titleComponent = screen.queryByTestId('dashboardPanelTitle');
|
||||
expect(header).not.toContainElement(titleComponent);
|
||||
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a placeholder title when in edit mode and the provided title is blank', async () => {
|
||||
it('does not render a title when in edit mode and the provided title is blank', async () => {
|
||||
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
panelTitle: new BehaviorSubject<string | undefined>(''),
|
||||
|
@ -236,9 +234,7 @@ describe('Presentation panel', () => {
|
|||
dataViews: new BehaviorSubject<DataView[] | undefined>([]),
|
||||
};
|
||||
await renderPresentationPanel({ api });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('[No Title]');
|
||||
});
|
||||
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens customize panel flyout on title click when in edit mode', async () => {
|
||||
|
@ -274,7 +270,7 @@ describe('Presentation panel', () => {
|
|||
expect(screen.queryByTestId('embeddablePanelTitleLink')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides title when API hide title option is true', async () => {
|
||||
it('hides title in view mode when API hide title option is true', async () => {
|
||||
const api: DefaultPresentationPanelApi & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
|
||||
|
@ -285,7 +281,18 @@ describe('Presentation panel', () => {
|
|||
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides title when parent hide title option is true', async () => {
|
||||
it('hides title in edit mode when API hide title option is true', async () => {
|
||||
const api: DefaultPresentationPanelApi & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
|
||||
hidePanelTitle: new BehaviorSubject<boolean | undefined>(true),
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
};
|
||||
await renderPresentationPanel({ api });
|
||||
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides title in view mode when parent hide title option is true', async () => {
|
||||
const api: DefaultPresentationPanelApi & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
|
||||
|
@ -298,5 +305,19 @@ describe('Presentation panel', () => {
|
|||
await renderPresentationPanel({ api });
|
||||
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides title in edit mode when parent hide title option is true', async () => {
|
||||
const api: DefaultPresentationPanelApi & PublishesViewMode = {
|
||||
uuid: 'test',
|
||||
panelTitle: new BehaviorSubject<string | undefined>('SUPER TITLE'),
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
parentApi: {
|
||||
viewMode: new BehaviorSubject<ViewMode>('edit'),
|
||||
...getMockPresentationContainer(),
|
||||
},
|
||||
};
|
||||
await renderPresentationPanel({ api });
|
||||
expect(screen.queryByTestId('presentationPanelTitle')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '@kbn/presentation-publishing';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { PresentationPanelHoverActions } from './panel_header/presentation_panel_hover_actions';
|
||||
import { PresentationPanelHeader } from './panel_header/presentation_panel_header';
|
||||
import { PresentationPanelError } from './presentation_panel_error';
|
||||
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from './types';
|
||||
|
@ -76,7 +77,7 @@ export const PresentationPanelInternal = <
|
|||
const hideTitle =
|
||||
Boolean(hidePanelTitle) ||
|
||||
Boolean(parentHidePanelTitle) ||
|
||||
(viewMode === 'view' && !Boolean(panelTitle ?? defaultPanelTitle));
|
||||
!Boolean(panelTitle ?? defaultPanelTitle);
|
||||
|
||||
const contentAttrs = useMemo(() => {
|
||||
const attrs: { [key: string]: boolean } = {};
|
||||
|
@ -90,55 +91,56 @@ export const PresentationPanelInternal = <
|
|||
}, [dataLoading, blockingError]);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
role="figure"
|
||||
paddingSize="none"
|
||||
className={classNames('embPanel', {
|
||||
'embPanel--editing': viewMode === 'edit',
|
||||
})}
|
||||
hasShadow={showShadow}
|
||||
hasBorder={showBorder}
|
||||
aria-labelledby={headerId}
|
||||
data-test-embeddable-id={api?.uuid}
|
||||
data-test-subj="embeddablePanel"
|
||||
{...contentAttrs}
|
||||
<PresentationPanelHoverActions
|
||||
{...{ index, api, getActions, actionPredicate, viewMode, showNotifications }}
|
||||
>
|
||||
{!hideHeader && api && (
|
||||
<PresentationPanelHeader
|
||||
api={api}
|
||||
index={index}
|
||||
headerId={headerId}
|
||||
viewMode={viewMode}
|
||||
hideTitle={hideTitle}
|
||||
showBadges={showBadges}
|
||||
getActions={getActions}
|
||||
actionPredicate={actionPredicate}
|
||||
showNotifications={showNotifications}
|
||||
panelTitle={panelTitle ?? defaultPanelTitle}
|
||||
panelDescription={panelDescription ?? defaultPanelDescription}
|
||||
/>
|
||||
)}
|
||||
{blockingError && api && (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="eui-fullHeight embPanel__error"
|
||||
data-test-subj="embeddableError"
|
||||
justifyContent="center"
|
||||
>
|
||||
<PresentationPanelError api={api} error={blockingError} />
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{!initialLoadComplete && <PanelLoader />}
|
||||
<div className={blockingError ? 'embPanel__content--hidden' : 'embPanel__content'}>
|
||||
<EuiErrorBoundary>
|
||||
<Component
|
||||
{...(componentProps as React.ComponentProps<typeof Component>)}
|
||||
ref={(newApi) => {
|
||||
if (newApi && !api) setApi(newApi);
|
||||
}}
|
||||
<EuiPanel
|
||||
role="figure"
|
||||
paddingSize="none"
|
||||
className={classNames('embPanel', {
|
||||
'embPanel--editing': viewMode === 'edit',
|
||||
})}
|
||||
hasShadow={showShadow}
|
||||
hasBorder={showBorder}
|
||||
aria-labelledby={headerId}
|
||||
data-test-subj="embeddablePanel"
|
||||
{...contentAttrs}
|
||||
>
|
||||
{!hideHeader && api && (
|
||||
<PresentationPanelHeader
|
||||
api={api}
|
||||
headerId={headerId}
|
||||
viewMode={viewMode}
|
||||
hideTitle={hideTitle}
|
||||
showBadges={showBadges}
|
||||
getActions={getActions}
|
||||
showNotifications={showNotifications}
|
||||
panelTitle={panelTitle ?? defaultPanelTitle}
|
||||
panelDescription={panelDescription ?? defaultPanelDescription}
|
||||
/>
|
||||
</EuiErrorBoundary>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
)}
|
||||
{blockingError && api && (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
className="eui-fullHeight embPanel__error"
|
||||
data-test-subj="embeddableError"
|
||||
justifyContent="center"
|
||||
>
|
||||
<PresentationPanelError api={api} error={blockingError} />
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
{!initialLoadComplete && <PanelLoader />}
|
||||
<div className={blockingError ? 'embPanel__content--hidden' : 'embPanel__content'}>
|
||||
<EuiErrorBoundary>
|
||||
<Component
|
||||
{...(componentProps as React.ComponentProps<typeof Component>)}
|
||||
ref={(newApi) => {
|
||||
if (newApi && !api) setApi(newApi);
|
||||
}}
|
||||
/>
|
||||
</EuiErrorBoundary>
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</PresentationPanelHoverActions>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import {
|
||||
CanLockHoverActions,
|
||||
HasParentApi,
|
||||
HasUniqueId,
|
||||
PublishesBlockingError,
|
||||
|
@ -74,7 +75,8 @@ export interface DefaultPresentationPanelApi
|
|||
HasParentApi<
|
||||
PresentationContainer &
|
||||
Partial<Pick<PublishesPanelTitle, 'hidePanelTitle'> & PublishesViewMode>
|
||||
>
|
||||
> &
|
||||
CanLockHoverActions
|
||||
> {}
|
||||
|
||||
export type PresentationPanelProps<
|
||||
|
|
|
@ -21,7 +21,7 @@ export const txtMore = i18n.translate('uiActions.actionPanel.more', {
|
|||
defaultMessage: 'More',
|
||||
});
|
||||
|
||||
interface ActionWithContext<Context extends object = object> {
|
||||
export interface ActionWithContext<Context extends object = object> {
|
||||
action: Action<Context> | ActionInternal<Context>;
|
||||
context: Context;
|
||||
|
||||
|
@ -37,6 +37,7 @@ type ItemDescriptor = EuiContextMenuPanelItemDescriptor & {
|
|||
};
|
||||
|
||||
type PanelDescriptor = EuiContextMenuPanelDescriptor & {
|
||||
_order?: number;
|
||||
_level?: number;
|
||||
_icon?: string;
|
||||
items: ItemDescriptor[];
|
||||
|
@ -101,7 +102,7 @@ const removeItemMetaFields = (items: ItemDescriptor[]): EuiContextMenuPanelItemD
|
|||
const removePanelMetaFields = (panels: PanelDescriptor[]): EuiContextMenuPanelDescriptor[] => {
|
||||
const euiPanels: EuiContextMenuPanelDescriptor[] = [];
|
||||
for (const panel of panels) {
|
||||
const { _level: omit, _icon: omit2, ...rest } = panel;
|
||||
const { _level: omit, _icon: omit2, _order: omit3, ...rest } = panel;
|
||||
euiPanels.push({ ...rest, items: removeItemMetaFields(rest.items) });
|
||||
}
|
||||
return euiPanels;
|
||||
|
@ -124,15 +125,18 @@ export async function buildContextMenuForActions({
|
|||
const panels: Record<string, PanelDescriptor> = {
|
||||
mainMenu: {
|
||||
id: 'mainMenu',
|
||||
title,
|
||||
items: [],
|
||||
},
|
||||
};
|
||||
const promises = actions.map(async (item) => {
|
||||
const { action } = item;
|
||||
const context: ActionExecutionContext<object> = { ...item.context, trigger: item.trigger };
|
||||
const context: ActionExecutionContext<object> = {
|
||||
...item.context,
|
||||
trigger: item.trigger,
|
||||
};
|
||||
const isCompatible = await item.action.isCompatible(context);
|
||||
if (!isCompatible) return;
|
||||
|
||||
let parentPanel = '';
|
||||
let currentPanel = '';
|
||||
if (action.grouping) {
|
||||
|
@ -146,6 +150,7 @@ export async function buildContextMenuForActions({
|
|||
title: name,
|
||||
items: [],
|
||||
_level: i,
|
||||
_order: group.order || 0,
|
||||
_icon: group.getIconType ? group.getIconType(context) : 'empty',
|
||||
};
|
||||
if (parentPanel) {
|
||||
|
@ -190,7 +195,11 @@ export async function buildContextMenuForActions({
|
|||
|
||||
wrapMainPanelItemsIntoSubmenu(panels, 'mainMenu');
|
||||
|
||||
for (const panel of Object.values(panels)) {
|
||||
const sortedPanels = Object.values(panels).sort((a, b) => {
|
||||
return (b._order || 0) - (a._order || 0);
|
||||
});
|
||||
|
||||
for (const panel of sortedPanels) {
|
||||
if (panel._level === 0) {
|
||||
if (panels.mainMenu.items.length > 0) {
|
||||
panels.mainMenu.items.push({
|
||||
|
@ -198,7 +207,7 @@ export async function buildContextMenuForActions({
|
|||
key: panel.id + '__separator',
|
||||
});
|
||||
}
|
||||
if (panel.items.length > 3) {
|
||||
if (panel.items.length > 4) {
|
||||
panels.mainMenu.items.push({
|
||||
name: panel.title || panel.id,
|
||||
icon: panel._icon || 'empty',
|
||||
|
|
|
@ -21,6 +21,6 @@ export const dynamicActionGrouping: PresentableGrouping<{
|
|||
defaultMessage: 'Custom actions',
|
||||
}),
|
||||
getIconType: () => 'symlink',
|
||||
order: 26,
|
||||
order: 0,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -61,7 +61,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const newTitle = 'wowee, my title just got cooler';
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
const originalPanelCount = await dashboard.getPanelCount();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await dashboardPanelActions.editPanelByTitle('wowee, looks like I have a new title');
|
||||
await visualize.saveVisualizationExpectSuccess(newTitle, {
|
||||
saveAsNew: true,
|
||||
redirectToOrigin: true,
|
||||
|
@ -76,7 +76,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('loses originatingApp connection after save as when redirectToOrigin is false', async () => {
|
||||
const newTitle = 'wowee, my title just got cooler again';
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.editPanelByTitle('wowee, my title just got cooler');
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await visualize.linkedToOriginatingApp();
|
||||
await visualize.saveVisualizationExpectSuccess(newTitle, {
|
||||
saveAsNew: true,
|
||||
|
|
|
@ -48,9 +48,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
it('are shown in edit mode', async function () {
|
||||
await dashboard.switchToEditMode();
|
||||
|
||||
const isContextMenuIconVisible = await dashboardPanelActions.isContextMenuIconVisible();
|
||||
expect(isContextMenuIconVisible).to.equal(true);
|
||||
|
||||
await dashboardPanelActions.expectExistsEditPanelAction();
|
||||
await dashboardPanelActions.expectExistsClonePanelAction();
|
||||
await dashboardPanelActions.expectExistsRemovePanelAction();
|
||||
|
|
|
@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
||||
await dashboardPanelActions.clickContextMenuItem(
|
||||
await dashboardPanelActions.clickPanelAction(
|
||||
'embeddablePanelAction-ACTION_VIEW_SAVED_SEARCH'
|
||||
);
|
||||
|
||||
|
|
|
@ -296,11 +296,13 @@ export class DashboardPageObject extends FtrService {
|
|||
// if the dashboard is not already in edit mode
|
||||
await this.testSubjects.click('dashboardEditMode');
|
||||
}
|
||||
// wait until the count of dashboard panels equals the count of toggle menu icons
|
||||
// wait until the count of dashboard panels equals the count of drag handles
|
||||
await this.retry.waitFor('in edit mode', async () => {
|
||||
const panels = await this.testSubjects.findAll('embeddablePanel', 2500);
|
||||
const menuIcons = await this.testSubjects.findAll('embeddablePanelToggleMenuIcon', 2500);
|
||||
return panels.length === menuIcons.length;
|
||||
const panels = await this.find.allByCssSelector('.embPanel__hoverActionsWrapper');
|
||||
const dragHandles = await this.find.allByCssSelector(
|
||||
'[data-test-subj="embeddablePanelDragHandle"]'
|
||||
);
|
||||
return panels.length === dragHandles.length;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 44 KiB |
|
@ -26,106 +26,109 @@ const LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-legacyUnlink
|
|||
const UNLINK_FROM_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-unlinkFromLibrary';
|
||||
const CONVERT_TO_LENS_TEST_SUBJ = 'embeddablePanelAction-ACTION_EDIT_IN_LENS';
|
||||
|
||||
const DASHBOARD_TOP_OFFSET = 96 + 105; // 96 for Kibana navigation bar + 105 for dashboard top nav bar (in edit mode)
|
||||
const DASHBOARD_MARGIN_SIZE = 8;
|
||||
|
||||
export class DashboardPanelActionsService extends FtrService {
|
||||
private readonly log = this.ctx.getService('log');
|
||||
private readonly retry = this.ctx.getService('retry');
|
||||
private readonly browser = this.ctx.getService('browser');
|
||||
private readonly find = this.ctx.getService('find');
|
||||
private readonly inspector = this.ctx.getService('inspector');
|
||||
private readonly testSubjects = this.ctx.getService('testSubjects');
|
||||
private readonly browser = this.ctx.getService('browser');
|
||||
|
||||
private readonly header = this.ctx.getPageObject('header');
|
||||
private readonly common = this.ctx.getPageObject('common');
|
||||
private readonly dashboard = this.ctx.getPageObject('dashboard');
|
||||
|
||||
async findContextMenu(parent?: WebElementWrapper) {
|
||||
async getContainerTopOffset() {
|
||||
const containerSelector = (await this.find.existsByCssSelector('.dashboardContainer'))
|
||||
? '.dashboardContainer'
|
||||
: '.canvasContainer';
|
||||
return (
|
||||
(await (await this.find.byCssSelector(containerSelector)).getPosition()).y +
|
||||
DASHBOARD_MARGIN_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
async findContextMenu(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('findContextMenu');
|
||||
return parent
|
||||
? await this.testSubjects.findDescendant(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ, parent)
|
||||
return wrapper
|
||||
? await wrapper.findByTestSubject(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ)
|
||||
: await this.testSubjects.find(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async isContextMenuIconVisible() {
|
||||
this.log.debug('isContextMenuIconVisible');
|
||||
return await this.testSubjects.exists(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ);
|
||||
async scrollPanelIntoView(wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`scrollPanelIntoView`);
|
||||
wrapper = wrapper || (await this.getPanelWrapper());
|
||||
const yOffset = (await wrapper.getPosition()).y;
|
||||
await this.browser.execute(`
|
||||
const scrollY = window.scrollY;
|
||||
window.scrollBy(0, scrollY - ${yOffset});
|
||||
`);
|
||||
|
||||
const containerTop = await this.getContainerTopOffset();
|
||||
|
||||
await wrapper.moveMouseTo({
|
||||
topOffset: containerTop,
|
||||
});
|
||||
}
|
||||
|
||||
async toggleContextMenu(parent?: WebElementWrapper) {
|
||||
this.log.debug(`toggleContextMenu(${parent})`);
|
||||
if (parent) {
|
||||
await parent.scrollIntoViewIfNecessary(DASHBOARD_TOP_OFFSET);
|
||||
await this.browser.getActions().move({ x: 0, y: 0, origin: parent._webElement }).perform();
|
||||
} else {
|
||||
await this.testSubjects.moveMouseTo('dashboardPanelTitle');
|
||||
}
|
||||
const toggleMenuItem = await this.findContextMenu(parent);
|
||||
await toggleMenuItem.click(DASHBOARD_TOP_OFFSET);
|
||||
async toggleContextMenu(wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`toggleContextMenu`);
|
||||
await this.scrollPanelIntoView(wrapper);
|
||||
const toggleMenuItem = await this.findContextMenu(wrapper);
|
||||
await toggleMenuItem.click(await this.getContainerTopOffset());
|
||||
}
|
||||
|
||||
async toggleContextMenuByTitle(title = '') {
|
||||
this.log.debug(`toggleContextMenu(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.toggleContextMenu(header);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
await this.toggleContextMenu(wrapper);
|
||||
}
|
||||
|
||||
async expectContextMenuToBeOpen() {
|
||||
this.log.debug('expectContextMenuToBeOpen');
|
||||
await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen');
|
||||
await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen', { allowHidden: true });
|
||||
}
|
||||
|
||||
async openContextMenu(parent?: WebElementWrapper) {
|
||||
this.log.debug(`openContextMenu`);
|
||||
async openContextMenu(wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`openContextMenu(${wrapper}`);
|
||||
const open = await this.testSubjects.exists('embeddablePanelContextMenuOpen');
|
||||
if (!open) await this.toggleContextMenu(parent);
|
||||
if (!open) await this.toggleContextMenu(wrapper);
|
||||
await this.expectContextMenuToBeOpen();
|
||||
}
|
||||
|
||||
async openContextMenuByTitle(title = '') {
|
||||
this.log.debug(`openContextMenuByTitle(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.openContextMenu(header);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
await this.openContextMenu(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');
|
||||
}
|
||||
}
|
||||
|
||||
async openContextMenuMorePanel(parent?: WebElementWrapper) {
|
||||
this.log.debug('openContextMenuMorePanel');
|
||||
await this.openContextMenu(parent);
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
|
||||
async clickContextMenuItem(testSubject: string, parent?: WebElementWrapper) {
|
||||
this.log.debug(`clickContextMenuItem(${testSubject})`);
|
||||
await this.openContextMenu(parent);
|
||||
const exists = await this.testSubjects.exists(testSubject, { timeout: 500 });
|
||||
async clickPanelAction(testSubject: string, wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`clickPanelAction(${testSubject})`);
|
||||
wrapper = wrapper || (await this.getPanelWrapper());
|
||||
await this.scrollPanelIntoView(wrapper);
|
||||
const exists = await this.testSubjects.descendantExists(testSubject, wrapper);
|
||||
let action;
|
||||
if (!exists) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
await this.openContextMenu(wrapper);
|
||||
action = await this.testSubjects.find(testSubject);
|
||||
} else {
|
||||
action = await this.testSubjects.findDescendant(testSubject, wrapper);
|
||||
}
|
||||
await this.testSubjects.clickWhenNotDisabledWithoutRetry(testSubject, { timeout: 500 });
|
||||
|
||||
await action.click(await this.getContainerTopOffset());
|
||||
}
|
||||
|
||||
async clickContextMenuItemByTitle(testSubject: string, title = '') {
|
||||
this.log.debug(`openContextMenuByTitle(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickContextMenuItem(testSubject, header);
|
||||
async clickPanelActionByTitle(testSubject: string, title = '') {
|
||||
this.log.debug(`clickPanelActionByTitle(${testSubject},${title})`);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
await this.clickPanelAction(testSubject, wrapper);
|
||||
}
|
||||
|
||||
async navigateToEditorFromFlyout() {
|
||||
async navigateToEditorFromFlyout(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('navigateToEditorFromFlyout');
|
||||
await this.clickContextMenuItem(INLINE_EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ, wrapper);
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_IN_LENS_EDITOR_DATA_TEST_SUBJ);
|
||||
const isConfirmModalVisible = await this.testSubjects.exists('confirmModalConfirmButton');
|
||||
|
@ -138,7 +141,7 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
|
||||
async clickInlineEdit() {
|
||||
this.log.debug('clickInlineEditAction');
|
||||
await this.clickContextMenuItem(INLINE_EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.clickPanelAction(INLINE_EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
await this.common.waitForTopNavToBeVisible();
|
||||
}
|
||||
|
@ -147,20 +150,16 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
* The dashboard/canvas panels can be either edited on their editor or inline.
|
||||
* The inline editing panels allow the navigation to the editor after the flyout opens
|
||||
*/
|
||||
async clickEdit(parent?: WebElementWrapper) {
|
||||
this.log.debug('clickEdit');
|
||||
await this.openContextMenu(parent);
|
||||
const isActionVisible = await this.testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
const isInlineEditingActionVisible = await this.testSubjects.exists(
|
||||
INLINE_EDIT_PANEL_DATA_TEST_SUBJ
|
||||
);
|
||||
if (!isActionVisible && !isInlineEditingActionVisible) await this.clickContextMenuMoreItem();
|
||||
// navigate to the editor
|
||||
if (await this.testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ)) {
|
||||
await this.testSubjects.clickWhenNotDisabledWithoutRetry(EDIT_PANEL_DATA_TEST_SUBJ);
|
||||
// open the flyout and then navigate to the editor
|
||||
async clickEdit(wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`clickEdit`);
|
||||
wrapper = wrapper || (await this.getPanelWrapper());
|
||||
await this.scrollPanelIntoView(wrapper);
|
||||
if (await this.testSubjects.descendantExists(EDIT_PANEL_DATA_TEST_SUBJ, wrapper)) {
|
||||
// navigate to the editor
|
||||
await this.clickPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, wrapper);
|
||||
} else {
|
||||
await this.navigateToEditorFromFlyout();
|
||||
// open the flyout and then navigate to the editor
|
||||
await this.navigateToEditorFromFlyout(wrapper);
|
||||
}
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
await this.common.waitForTopNavToBeVisible();
|
||||
|
@ -172,55 +171,55 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
*/
|
||||
async editPanelByTitle(title = '') {
|
||||
this.log.debug(`editPanelByTitle(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickEdit(header);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
await this.clickEdit(wrapper);
|
||||
}
|
||||
|
||||
async clickExpandPanelToggle() {
|
||||
this.log.debug(`clickExpandPanelToggle`);
|
||||
await this.openContextMenu();
|
||||
await this.clickContextMenuItem(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
await this.clickPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async removePanel(parent?: WebElementWrapper) {
|
||||
async removePanel(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('removePanel');
|
||||
await this.openContextMenu(parent);
|
||||
await this.clickContextMenuItem(REMOVE_PANEL_DATA_TEST_SUBJ, parent);
|
||||
await this.clickPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ, wrapper);
|
||||
}
|
||||
|
||||
async removePanelByTitle(title = '') {
|
||||
this.log.debug(`removePanel(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
this.log.debug('found header? ', Boolean(header));
|
||||
await this.removePanel(header);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
await this.removePanel(wrapper);
|
||||
}
|
||||
|
||||
async customizePanel(title = '') {
|
||||
this.log.debug(`customizePanel(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickContextMenuItem(CUSTOMIZE_PANEL_DATA_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(CUSTOMIZE_PANEL_DATA_TEST_SUBJ, title);
|
||||
}
|
||||
|
||||
async clonePanel(title = '') {
|
||||
this.log.debug(`clonePanel(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickContextMenuItem(CLONE_PANEL_DATA_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(CLONE_PANEL_DATA_TEST_SUBJ, title);
|
||||
await this.dashboard.waitForRenderComplete();
|
||||
}
|
||||
|
||||
async openCopyToModalByTitle(title = '') {
|
||||
this.log.debug(`copyPanelTo(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickContextMenuItem(COPY_PANEL_TO_DATA_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(COPY_PANEL_TO_DATA_TEST_SUBJ, title);
|
||||
}
|
||||
|
||||
async openInspectorByTitle(title: string) {
|
||||
async openInspector(wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`openInspector`);
|
||||
await this.clickPanelAction(OPEN_INSPECTOR_TEST_SUBJ, wrapper);
|
||||
}
|
||||
|
||||
async openInspectorByTitle(title = '') {
|
||||
this.log.debug(`openInspector(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.openInspector(header);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
await this.openInspector(wrapper);
|
||||
}
|
||||
|
||||
async getSearchSessionIdByTitle(title: string) {
|
||||
async getSearchSessionIdByTitle(title = '') {
|
||||
this.log.debug(`getSearchSessionId(${title})`);
|
||||
await this.openInspectorByTitle(title);
|
||||
await this.inspector.openInspectorRequestsView();
|
||||
|
@ -231,7 +230,7 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
return searchSessionId;
|
||||
}
|
||||
|
||||
async getSearchResponseByTitle(title: string) {
|
||||
async getSearchResponseByTitle(title = '') {
|
||||
this.log.debug(`setSearchResponse(${title})`);
|
||||
await this.openInspectorByTitle(title);
|
||||
await this.inspector.openInspectorRequestsView();
|
||||
|
@ -240,31 +239,23 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
return response;
|
||||
}
|
||||
|
||||
async openInspector(parent?: WebElementWrapper) {
|
||||
this.log.debug(`openInspector`);
|
||||
await this.clickContextMenuItem(OPEN_INSPECTOR_TEST_SUBJ, parent);
|
||||
}
|
||||
|
||||
async legacyUnlinkFromLibrary(title = '') {
|
||||
this.log.debug(`legacyUnlinkFromLibrary(${title}`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickContextMenuItem(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
|
||||
await this.testSubjects.existOrFail('unlinkPanelSuccess');
|
||||
await this.expectNotLinkedToLibrary(title, true);
|
||||
}
|
||||
|
||||
async unlinkFromLibrary(title = '') {
|
||||
this.log.debug(`unlinkFromLibrary(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.clickContextMenuItem(UNLINK_FROM_LIBRARY_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
|
||||
await this.testSubjects.existOrFail('unlinkPanelSuccess');
|
||||
await this.expectNotLinkedToLibrary(title);
|
||||
}
|
||||
|
||||
async legacySaveToLibrary(newTitle = '', oldTitle = '') {
|
||||
this.log.debug(`legacySaveToLibrary(${newTitle},${oldTitle})`);
|
||||
const header = await this.getPanelHeading(oldTitle);
|
||||
await this.clickContextMenuItem(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle);
|
||||
await this.testSubjects.setValue('savedObjectTitle', newTitle, {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
|
@ -275,8 +266,7 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
|
||||
async saveToLibrary(newTitle = '', oldTitle = '') {
|
||||
this.log.debug(`saveToLibraryByTitle(${newTitle},${oldTitle})`);
|
||||
const header = await this.getPanelHeading(oldTitle);
|
||||
await this.clickContextMenuItem(SAVE_TO_LIBRARY_TEST_SUBJ, header);
|
||||
await this.clickPanelActionByTitle(SAVE_TO_LIBRARY_TEST_SUBJ, oldTitle);
|
||||
await this.testSubjects.setValue('savedObjectTitle', newTitle, {
|
||||
clearWithKeyboard: true,
|
||||
});
|
||||
|
@ -285,18 +275,31 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
await this.expectLinkedToLibrary(newTitle);
|
||||
}
|
||||
|
||||
async panelActionExists(testSubject: string, wrapper?: WebElementWrapper) {
|
||||
this.log.debug(`panelActionExists(${testSubject})`);
|
||||
return wrapper
|
||||
? await this.testSubjects.descendantExists(testSubject, wrapper)
|
||||
: await this.testSubjects.exists(testSubject, { allowHidden: true });
|
||||
}
|
||||
|
||||
async panelActionExistsByTitle(testSubject: string, title = '') {
|
||||
this.log.debug(`panelActionExists(${testSubject}) on "${title}"`);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
return await this.panelActionExists(testSubject, wrapper);
|
||||
}
|
||||
|
||||
async expectExistsPanelAction(testSubject: string, title = '') {
|
||||
this.log.debug('expectExistsPanelAction', testSubject, title);
|
||||
|
||||
const panelWrapper = await this.getPanelHeading(title);
|
||||
await this.openContextMenu(panelWrapper);
|
||||
if (!(await this.testSubjects.exists(testSubject, { timeout: 1000 }))) {
|
||||
if (await this.hasContextMenuMoreItem()) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
}
|
||||
await this.testSubjects.existOrFail(testSubject, { timeout: 1000 });
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
|
||||
const exists = await this.panelActionExists(testSubject, wrapper);
|
||||
|
||||
if (!exists) {
|
||||
await this.openContextMenu(wrapper);
|
||||
await this.testSubjects.existOrFail(testSubject, { allowHidden: true });
|
||||
await this.toggleContextMenu(wrapper);
|
||||
}
|
||||
await this.toggleContextMenu(panelWrapper);
|
||||
}
|
||||
|
||||
async expectExistsRemovePanelAction(title = '') {
|
||||
|
@ -324,15 +327,16 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
}
|
||||
|
||||
async expectMissingPanelAction(testSubject: string, title = '') {
|
||||
this.log.debug(`expectMissingPanelAction(${title})`, testSubject);
|
||||
const panelWrapper = await this.getPanelHeading(title);
|
||||
await this.openContextMenu(panelWrapper);
|
||||
await this.testSubjects.missingOrFail(testSubject);
|
||||
if (await this.hasContextMenuMoreItem()) {
|
||||
await this.clickContextMenuMoreItem();
|
||||
this.log.debug('expectMissingPanelAction', testSubject, title);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
|
||||
const exists = await this.panelActionExists(testSubject, wrapper);
|
||||
|
||||
if (!exists) {
|
||||
await this.openContextMenu(wrapper);
|
||||
await this.testSubjects.missingOrFail(testSubject);
|
||||
await this.toggleContextMenu(wrapper);
|
||||
}
|
||||
await this.toggleContextMenu(panelWrapper);
|
||||
}
|
||||
|
||||
async expectMissingEditPanelAction(title = '') {
|
||||
|
@ -352,10 +356,21 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
|
||||
async getPanelHeading(title = '') {
|
||||
this.log.debug(`getPanelHeading(${title})`);
|
||||
if (!title) return await this.find.byClassName('embPanel__header');
|
||||
if (!title) return await this.find.byClassName('embPanel__wrapper');
|
||||
return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`);
|
||||
}
|
||||
|
||||
async getPanelWrapper(title = '') {
|
||||
this.log.debug(`getPanelWrapper(${title})`);
|
||||
if (!title) return await this.find.byClassName('embPanel__hoverActionsAnchor');
|
||||
return await this.testSubjects.find(`embeddablePanelHoverActions-${title.replace(/\s/g, '')}`);
|
||||
}
|
||||
|
||||
async getPanelWrapperById(embeddableId: string) {
|
||||
this.log.debug(`getPanelWrapperById(${embeddableId})`);
|
||||
return await this.find.byCssSelector(`[data-test-embeddable-id="${embeddableId}"]`);
|
||||
}
|
||||
|
||||
async getActionWebElementByText(text: string): Promise<WebElementWrapper> {
|
||||
this.log.debug(`getActionWebElement: "${text}"`);
|
||||
const menu = await this.testSubjects.find('multipleActionsContextMenu');
|
||||
|
@ -370,28 +385,23 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
throw new Error(`No action matching text "${text}"`);
|
||||
}
|
||||
|
||||
async canConvertToLens(parent?: WebElementWrapper) {
|
||||
async canConvertToLens(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('canConvertToLens');
|
||||
await this.openContextMenu(parent);
|
||||
const isActionVisible = await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ);
|
||||
if (!isActionVisible) await this.clickContextMenuMoreItem();
|
||||
return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 1000 });
|
||||
await this.openContextMenu(wrapper);
|
||||
return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 500 });
|
||||
}
|
||||
|
||||
async canConvertToLensByTitle(title = '') {
|
||||
this.log.debug(`canConvertToLens(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
await this.openContextMenu(header);
|
||||
const isActionVisible = await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ);
|
||||
if (!isActionVisible) await this.clickContextMenuMoreItem();
|
||||
return await this.testSubjects.exists(CONVERT_TO_LENS_TEST_SUBJ, { timeout: 1000 });
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
return await this.canConvertToLens(wrapper);
|
||||
}
|
||||
|
||||
async convertToLens(parent?: WebElementWrapper) {
|
||||
async convertToLens(wrapper?: WebElementWrapper) {
|
||||
this.log.debug('convertToLens');
|
||||
|
||||
await this.retry.try(async () => {
|
||||
if (!(await this.canConvertToLens(parent))) {
|
||||
if (!(await this.canConvertToLens(wrapper))) {
|
||||
throw new Error('Convert to Lens option not found');
|
||||
}
|
||||
|
||||
|
@ -401,29 +411,31 @@ export class DashboardPanelActionsService extends FtrService {
|
|||
|
||||
async convertToLensByTitle(title = '') {
|
||||
this.log.debug(`convertToLens(${title})`);
|
||||
const header = await this.getPanelHeading(title);
|
||||
return await this.convertToLens(header);
|
||||
const wrapper = await this.getPanelWrapper(title);
|
||||
return await this.convertToLens(wrapper);
|
||||
}
|
||||
|
||||
public async expectLinkedToLibrary(title = '', legacy?: boolean) {
|
||||
async expectLinkedToLibrary(title = '', legacy?: boolean) {
|
||||
this.log.debug(`expectLinkedToLibrary(${title})`);
|
||||
const isViewMode = await this.dashboard.getIsInViewMode();
|
||||
if (isViewMode) await this.dashboard.switchToEditMode();
|
||||
if (legacy) {
|
||||
await this.expectExistsPanelAction(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
|
||||
} else {
|
||||
await this.expectExistsPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
|
||||
}
|
||||
await this.expectMissingPanelAction(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, title);
|
||||
await this.expectMissingPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title);
|
||||
if (isViewMode) await this.dashboard.clickCancelOutOfEditMode();
|
||||
}
|
||||
|
||||
public async expectNotLinkedToLibrary(title = '', legacy?: boolean) {
|
||||
async expectNotLinkedToLibrary(title = '', legacy?: boolean) {
|
||||
this.log.debug(`expectNotLinkedToLibrary(${title})`);
|
||||
const isViewMode = await this.dashboard.getIsInViewMode();
|
||||
if (isViewMode) await this.dashboard.switchToEditMode();
|
||||
if (legacy) {
|
||||
await this.expectExistsPanelAction(LEGACY_SAVE_TO_LIBRARY_TEST_SUBJ, title);
|
||||
} else {
|
||||
await this.expectExistsPanelAction(SAVE_TO_LIBRARY_TEST_SUBJ, title);
|
||||
}
|
||||
await this.expectMissingPanelAction(LEGACY_UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
|
||||
await this.expectMissingPanelAction(UNLINK_FROM_LIBRARY_TEST_SUBJ, title);
|
||||
if (isViewMode) await this.dashboard.clickCancelOutOfEditMode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export function DashboardDrilldownPanelActionsProvider({
|
|||
async clickCreateDrilldown() {
|
||||
log.debug('clickCreateDrilldown');
|
||||
await this.expectExistsCreateDrilldownAction();
|
||||
await dashboardPanelActions.clickContextMenuItem(CREATE_DRILLDOWN_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(CREATE_DRILLDOWN_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectExistsManageDrilldownsAction() {
|
||||
|
@ -52,7 +52,7 @@ export function DashboardDrilldownPanelActionsProvider({
|
|||
|
||||
async clickManageDrilldowns() {
|
||||
log.debug('clickManageDrilldowns');
|
||||
await dashboardPanelActions.clickContextMenuItem(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ);
|
||||
}
|
||||
|
||||
async expectMultipleActionsMenuOpened() {
|
||||
|
@ -93,14 +93,13 @@ export function DashboardDrilldownPanelActionsProvider({
|
|||
async getPanelDrilldownCount(panelIndex = 0): Promise<number> {
|
||||
log.debug('getPanelDrilldownCount');
|
||||
const panel = (await dashboard.getDashboardPanels())[panelIndex];
|
||||
await dashboardPanelActions.openContextMenu(panel);
|
||||
|
||||
try {
|
||||
const exists = await testSubjects.exists(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ, {
|
||||
timeout: 500,
|
||||
});
|
||||
if (!exists) {
|
||||
await dashboardPanelActions.clickContextMenuMoreItem();
|
||||
await dashboardPanelActions.openContextMenu(panel);
|
||||
if (!(await testSubjects.exists(MANAGE_DRILLDOWNS_DATA_TEST_SUBJ, { timeout: 500 }))) {
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -23,10 +23,6 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
|
||||
it('allows to register links into the context menu', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
const actionExists = await testSubjects.exists('embeddablePanelAction-samplePanelLink');
|
||||
if (!actionExists) {
|
||||
await dashboardPanelActions.clickContextMenuMoreItem();
|
||||
}
|
||||
const actionElement = await testSubjects.find('embeddablePanelAction-samplePanelLink');
|
||||
const actionElementTag = await actionElement.getTagName();
|
||||
expect(actionElementTag).to.be('a');
|
||||
|
|
|
@ -66,7 +66,7 @@ export function containerStyle(): ExpressionFunctionDefinition<
|
|||
types: ['string'],
|
||||
help: argHelp.overflow,
|
||||
options: Object.values(Overflow),
|
||||
default: 'hidden',
|
||||
default: 'visible',
|
||||
},
|
||||
padding: {
|
||||
types: ['string'],
|
||||
|
|
|
@ -140,9 +140,9 @@ describe('containerStyle', () => {
|
|||
result = fn(null, { overflow: 'hidden' });
|
||||
expect(result).toHaveProperty('overflow', 'hidden');
|
||||
});
|
||||
it(`defaults to 'hidden'`, () => {
|
||||
it(`defaults to 'visible'`, () => {
|
||||
const result = fn(null);
|
||||
expect(result).toHaveProperty('overflow', 'hidden');
|
||||
expect(result).toHaveProperty('overflow', 'visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
outline: none !important;
|
||||
background: none;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none;
|
||||
|
||||
.embPanel__title {
|
||||
margin-bottom: $euiSizeXS;
|
||||
|
@ -24,6 +25,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.embPanel__hoverActionsLeft, .embPanel__hoverActions > .embPanel--dragHandle {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.embPanel--dragHandle:hover {
|
||||
background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7));
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.euiTable {
|
||||
background: none;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,32 @@
|
|||
.canvasElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.embPanel {
|
||||
.embPanel__content {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.embPanel__hoverActionsLeft, .embPanel__dragHandle {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.canvasElement__content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canvas__element--selected {
|
||||
.embPanel__hoverActionsAnchor {
|
||||
.embPanel__hoverActionsWrapper {
|
||||
z-index: $euiZLevel9;
|
||||
top: -$euiSizeXL;
|
||||
|
||||
.embPanel__hoverActions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { omitBy, isNil } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { ExpressionRenderer } from '@kbn/expressions-plugin/common';
|
||||
|
@ -29,6 +30,8 @@ export interface Props {
|
|||
backgroundColor: string;
|
||||
selectElement: () => void;
|
||||
state: string;
|
||||
selectedElementId: string | null;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const ElementContent = (props: Props) => {
|
||||
|
@ -59,7 +62,9 @@ export const ElementContent = (props: Props) => {
|
|||
<div
|
||||
css={css(renderable.css)}
|
||||
// TODO: 'canvas__element' was added for BWC, It can be removed after a while
|
||||
className={'canvas__element canvasElement'}
|
||||
className={classNames('canvas__element', 'canvasElement', {
|
||||
'canvas__element--selected': props?.selectedElementId === props.id,
|
||||
})}
|
||||
style={{ ...containerStyle, width, height }}
|
||||
data-test-subj="canvasWorkpadPageElementContent"
|
||||
>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getSelectedPage, getPageById } from '../../state/selectors/workpad';
|
||||
import { getSelectedPage, getPageById, getSelectedElementId } from '../../state/selectors/workpad';
|
||||
import { ElementContent as Component, Props as ComponentProps } from './element_content';
|
||||
import { State } from '../../../types';
|
||||
import { getCanvasExpressionService } from '../../services/canvas_expressions_service';
|
||||
|
@ -16,6 +16,7 @@ export type Props = Omit<ComponentProps, 'renderFunction' | 'backgroundColor'>;
|
|||
|
||||
export const ElementContent = (props: Props) => {
|
||||
const selectedPageId = useSelector(getSelectedPage);
|
||||
const selectedElementId = useSelector(getSelectedElementId);
|
||||
const backgroundColor =
|
||||
useSelector((state: State) => getPageById(state, selectedPageId)?.style.background) || '';
|
||||
const { renderable } = props;
|
||||
|
@ -24,5 +25,5 @@ export const ElementContent = (props: Props) => {
|
|||
return renderable ? getCanvasExpressionService().getRenderer(renderable.as) : null;
|
||||
}, [renderable]);
|
||||
|
||||
return <Component {...{ ...props, renderFunction, backgroundColor }} />;
|
||||
return <Component {...{ ...props, renderFunction, backgroundColor, selectedElementId }} />;
|
||||
};
|
||||
|
|
|
@ -11,11 +11,12 @@ import { Positionable } from '../positionable';
|
|||
import { ElementContent } from '../element_content';
|
||||
|
||||
export const ElementWrapper = (props) => {
|
||||
const { renderable, transformMatrix, width, height, state, handlers } = props;
|
||||
const { renderable, transformMatrix, width, height, state, handlers, id } = props;
|
||||
|
||||
return (
|
||||
<Positionable transformMatrix={transformMatrix} width={width} height={height}>
|
||||
<ElementContent
|
||||
id={id}
|
||||
renderable={renderable}
|
||||
state={state}
|
||||
handlers={handlers}
|
||||
|
@ -35,4 +36,5 @@ ElementWrapper.propTypes = {
|
|||
renderable: PropTypes.object,
|
||||
state: PropTypes.string,
|
||||
handlers: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
|
@ -71,7 +71,7 @@ export const ElementWrapper = compose(
|
|||
// remove element and createHandlers from props passed to component
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { element, createHandlers, selectedPage, ...restProps } = props;
|
||||
return restProps;
|
||||
return { ...restProps, id: element.id };
|
||||
})
|
||||
)(Component);
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ export const createAddToExistingCaseLensAction = (
|
|||
return createAction<EmbeddableApiContext>({
|
||||
id: ACTION_ID,
|
||||
type: 'actionButton',
|
||||
order: 10,
|
||||
grouping: [{ id: 'cases', order: 6 }],
|
||||
getIconType: () => 'casesApp',
|
||||
getDisplayName: () => ADD_TO_EXISTING_CASE_DISPLAYNAME,
|
||||
isCompatible: async ({ embeddable }) => {
|
||||
|
|
|
@ -42,13 +42,13 @@ export const getMockLensApi = (
|
|||
({
|
||||
type: 'lens',
|
||||
getSavedVis: () => {},
|
||||
canViewUnderlyingData: () => {},
|
||||
canViewUnderlyingData$: new BehaviorSubject(true),
|
||||
getViewUnderlyingDataArgs: () => {},
|
||||
getFullAttributes: () => {
|
||||
return mockLensAttributes;
|
||||
},
|
||||
panelTitle: new BehaviorSubject('myPanel'),
|
||||
hidePanelTitle: new BehaviorSubject('false'),
|
||||
hidePanelTitle: new BehaviorSubject(false),
|
||||
timeslice$: new BehaviorSubject<[number, number] | undefined>(undefined),
|
||||
timeRange$: new BehaviorSubject<TimeRange | undefined>({
|
||||
from,
|
||||
|
|
|
@ -77,3 +77,5 @@ export const createDrilldownTemplatesFromSiblings = (
|
|||
};
|
||||
|
||||
export const DRILLDOWN_MAX_WIDTH = 500;
|
||||
|
||||
export const DRILLDOWN_ACTION_GROUP = { id: 'drilldown', order: 3 } as const;
|
||||
|
|
|
@ -36,6 +36,7 @@ import React from 'react';
|
|||
import { StartDependencies } from '../../../../plugin';
|
||||
import {
|
||||
createDrilldownTemplatesFromSiblings,
|
||||
DRILLDOWN_ACTION_GROUP,
|
||||
DRILLDOWN_MAX_WIDTH,
|
||||
ensureNestedTriggers,
|
||||
} from '../drilldown_shared';
|
||||
|
@ -62,6 +63,7 @@ export class FlyoutCreateDrilldownAction implements Action<EmbeddableApiContext>
|
|||
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) {}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import { MenuItem } from './menu_item';
|
|||
import { StartDependencies } from '../../../../plugin';
|
||||
import {
|
||||
createDrilldownTemplatesFromSiblings,
|
||||
DRILLDOWN_ACTION_GROUP,
|
||||
DRILLDOWN_MAX_WIDTH,
|
||||
ensureNestedTriggers,
|
||||
} from '../drilldown_shared';
|
||||
|
@ -57,6 +58,7 @@ export class FlyoutEditDrilldownAction implements Action<EmbeddableApiContext> {
|
|||
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) {}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { partition, uniqBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
@ -1034,6 +1034,8 @@ export class Embeddable
|
|||
this.activeData = newActiveData;
|
||||
|
||||
this.renderUserMessages();
|
||||
|
||||
this.loadViewUnderlyingDataArgs();
|
||||
};
|
||||
|
||||
private onRender: ExpressionWrapperProps['onRender$'] = () => {
|
||||
|
@ -1480,7 +1482,7 @@ export class Embeddable
|
|||
}
|
||||
}
|
||||
|
||||
private async loadViewUnderlyingDataArgs(): Promise<boolean> {
|
||||
private async loadViewUnderlyingDataArgs(): Promise<void> {
|
||||
if (
|
||||
!this.savedVis ||
|
||||
!this.activeData ||
|
||||
|
@ -1489,13 +1491,15 @@ export class Embeddable
|
|||
!this.activeVisualization ||
|
||||
!this.activeVisualizationState
|
||||
) {
|
||||
return false;
|
||||
this.canViewUnderlyingData$.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedSearchContext = this.getMergedSearchContext();
|
||||
|
||||
if (!mergedSearchContext.timeRange) {
|
||||
return false;
|
||||
this.canViewUnderlyingData$.next(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const viewUnderlyingDataArgs = getViewUnderlyingDataArgs({
|
||||
|
@ -1517,7 +1521,8 @@ export class Embeddable
|
|||
if (loaded) {
|
||||
this.viewUnderlyingDataArgs = viewUnderlyingDataArgs;
|
||||
}
|
||||
return loaded;
|
||||
|
||||
this.canViewUnderlyingData$.next(loaded);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1529,9 +1534,7 @@ export class Embeddable
|
|||
return this.viewUnderlyingDataArgs;
|
||||
}
|
||||
|
||||
public canViewUnderlyingData() {
|
||||
return this.loadViewUnderlyingDataArgs();
|
||||
}
|
||||
public canViewUnderlyingData$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
async initializeOutput() {
|
||||
if (!this.savedVis) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
HasType,
|
||||
PublishesUnifiedSearch,
|
||||
PublishesPanelTitle,
|
||||
PublishingSubject,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import {
|
||||
apiIsOfType,
|
||||
|
@ -20,7 +21,7 @@ import { LensSavedObjectAttributes, ViewUnderlyingDataArgs } from '../embeddable
|
|||
|
||||
export type HasLensConfig = HasType<'lens'> & {
|
||||
getSavedVis: () => Readonly<LensSavedObjectAttributes | undefined>;
|
||||
canViewUnderlyingData: () => Promise<boolean>;
|
||||
canViewUnderlyingData$: PublishingSubject<boolean>;
|
||||
getViewUnderlyingDataArgs: () => ViewUnderlyingDataArgs;
|
||||
getFullAttributes: () => LensSavedObjectAttributes | undefined;
|
||||
};
|
||||
|
@ -35,7 +36,7 @@ export const isLensApi = (api: unknown): api is LensApi => {
|
|||
api &&
|
||||
apiIsOfType(api, 'lens') &&
|
||||
typeof (api as HasLensConfig).getSavedVis === 'function' &&
|
||||
typeof (api as HasLensConfig).canViewUnderlyingData === 'function' &&
|
||||
(api as HasLensConfig).canViewUnderlyingData$ &&
|
||||
typeof (api as HasLensConfig).getViewUnderlyingDataArgs === 'function' &&
|
||||
typeof (api as HasLensConfig).getFullAttributes === 'function' &&
|
||||
apiPublishesPanelTitle(api) &&
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('open in discover action', () => {
|
|||
query$: new BehaviorSubject({ query: 'test', language: 'kuery' }),
|
||||
timeRange$: new BehaviorSubject({ from: 'now-15m', to: 'now' }),
|
||||
getSavedVis: jest.fn(() => undefined),
|
||||
canViewUnderlyingData: () => Promise.resolve(true),
|
||||
canViewUnderlyingData$: new BehaviorSubject(true),
|
||||
getFullAttributes: jest.fn(() => undefined),
|
||||
getViewUnderlyingDataArgs: jest.fn(() => ({
|
||||
dataViewSpec: { id: 'index-pattern-id' },
|
||||
|
@ -78,8 +78,7 @@ describe('open in discover action', () => {
|
|||
// setup
|
||||
const embeddable = {
|
||||
...compatibleEmbeddableApi,
|
||||
canViewUnderlyingData: jest.fn(() => Promise.resolve(false)),
|
||||
getViewUnderlyingDataArgs: jest.fn(() => undefined),
|
||||
canViewUnderlyingData$: { getValue: jest.fn(() => false) },
|
||||
};
|
||||
|
||||
// test false
|
||||
|
@ -93,10 +92,11 @@ describe('open in discover action', () => {
|
|||
} as ActionExecutionContext<EmbeddableApiContext>)
|
||||
).toBeFalsy();
|
||||
|
||||
expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1);
|
||||
expect(embeddable.canViewUnderlyingData$.getValue).toHaveBeenCalledTimes(1);
|
||||
|
||||
// test true
|
||||
embeddable.canViewUnderlyingData = jest.fn(() => Promise.resolve(true));
|
||||
embeddable.canViewUnderlyingData$.getValue = jest.fn(() => true);
|
||||
|
||||
expect(
|
||||
await createOpenInDiscoverAction(
|
||||
{} as DiscoverAppLocator,
|
||||
|
@ -107,7 +107,7 @@ describe('open in discover action', () => {
|
|||
} as ActionExecutionContext<EmbeddableApiContext>)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(embeddable.canViewUnderlyingData).toHaveBeenCalledTimes(1);
|
||||
expect(embeddable.canViewUnderlyingData$.getValue).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -6,10 +6,11 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { createAction } from '@kbn/ui-actions-plugin/public';
|
||||
import { Action, createAction, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import type { DataViewsService } from '@kbn/data-views-plugin/public';
|
||||
import type { DiscoverAppLocator } from './open_in_discover_helpers';
|
||||
import { LensApi } from '../embeddable';
|
||||
|
||||
const ACTION_OPEN_IN_DISCOVER = 'ACTION_OPEN_IN_DISCOVER';
|
||||
|
||||
|
@ -19,12 +20,12 @@ export const createOpenInDiscoverAction = (
|
|||
locator: DiscoverAppLocator,
|
||||
dataViews: Pick<DataViewsService, 'get'>,
|
||||
hasDiscoverAccess: boolean
|
||||
) =>
|
||||
createAction<EmbeddableApiContext>({
|
||||
) => {
|
||||
const actionDefinition = {
|
||||
type: ACTION_OPEN_IN_DISCOVER,
|
||||
id: ACTION_OPEN_IN_DISCOVER,
|
||||
order: 19, // right after Inspect which is 20
|
||||
getIconType: () => 'popout',
|
||||
order: 20, // right before Inspect which is 19
|
||||
getIconType: () => 'discoverApp',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.lens.action.exploreInDiscover', {
|
||||
defaultMessage: 'Explore in Discover',
|
||||
|
@ -47,8 +48,26 @@ export const createOpenInDiscoverAction = (
|
|||
embeddable: context.embeddable,
|
||||
});
|
||||
},
|
||||
couldBecomeCompatible: ({ embeddable }: EmbeddableApiContext) => {
|
||||
if (!typeof (embeddable as LensApi).canViewUnderlyingData$)
|
||||
throw new IncompatibleActionError();
|
||||
return hasDiscoverAccess && Boolean((embeddable as LensApi).canViewUnderlyingData$);
|
||||
},
|
||||
subscribeToCompatibilityChanges: (
|
||||
{ embeddable }: EmbeddableApiContext,
|
||||
onChange: (isCompatible: boolean, action: Action<EmbeddableApiContext>) => 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<EmbeddableApiContext>(actionDefinition);
|
||||
};
|
||||
|
|
|
@ -31,10 +31,10 @@ type Context = EmbeddableApiContext & {
|
|||
timeFieldName?: string;
|
||||
};
|
||||
|
||||
export async function isCompatible({ hasDiscoverAccess, embeddable }: Context) {
|
||||
export function isCompatible({ hasDiscoverAccess, embeddable }: Context) {
|
||||
if (!hasDiscoverAccess) return false;
|
||||
try {
|
||||
return isLensApi(embeddable) && (await embeddable.canViewUnderlyingData());
|
||||
return isLensApi(embeddable) && embeddable.canViewUnderlyingData$.getValue();
|
||||
} catch (e) {
|
||||
// Fetching underlying data failed, log the error and behave as if the action is not compatible
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
@ -23,6 +23,8 @@ export function createVisToADJobAction(
|
|||
return {
|
||||
id: 'create-ml-ad-job-action',
|
||||
type: CREATE_LENS_VIS_TO_ML_AD_JOB_ACTION,
|
||||
order: 8,
|
||||
grouping: [{ id: 'ml', order: 3 }],
|
||||
getIconType(context): string {
|
||||
return 'machineLearningApp';
|
||||
},
|
||||
|
|
|
@ -190,5 +190,6 @@ const getAddToCaseAction = ({ callback }: { callback: () => void }): Action => {
|
|||
return;
|
||||
},
|
||||
order: 48,
|
||||
grouping: [{ id: 'observability', order: 5 }],
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6018,7 +6018,6 @@
|
|||
"presentationPanel.contextMenu.ariaLabel": "Options de panneau",
|
||||
"presentationPanel.contextMenu.ariaLabelWithIndex": "Options pour le panneau {index}",
|
||||
"presentationPanel.contextMenu.ariaLabelWithTitle": "Options de panneau pour {title}",
|
||||
"presentationPanel.contextMenu.loadingTitle": "Options",
|
||||
"presentationPanel.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau",
|
||||
"presentationPanel.contextMenuTrigger.title": "Menu contextuel",
|
||||
"presentationPanel.emptyErrorMessage": "Erreur",
|
||||
|
|
|
@ -5772,7 +5772,6 @@
|
|||
"presentationPanel.contextMenu.ariaLabel": "パネルオプション",
|
||||
"presentationPanel.contextMenu.ariaLabelWithIndex": "パネル{index}のオプション",
|
||||
"presentationPanel.contextMenu.ariaLabelWithTitle": "{title} のパネルオプション",
|
||||
"presentationPanel.contextMenu.loadingTitle": "オプション",
|
||||
"presentationPanel.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます",
|
||||
"presentationPanel.contextMenuTrigger.title": "コンテキストメニュー",
|
||||
"presentationPanel.emptyErrorMessage": "エラー",
|
||||
|
|
|
@ -5785,7 +5785,6 @@
|
|||
"presentationPanel.contextMenu.ariaLabel": "面板选项",
|
||||
"presentationPanel.contextMenu.ariaLabelWithIndex": "面板 {index} 的选项",
|
||||
"presentationPanel.contextMenu.ariaLabelWithTitle": "{title} 的面板选项",
|
||||
"presentationPanel.contextMenu.loadingTitle": "选项",
|
||||
"presentationPanel.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单",
|
||||
"presentationPanel.contextMenuTrigger.title": "上下文菜单",
|
||||
"presentationPanel.emptyErrorMessage": "错误",
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
|
@ -18,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// Failing: See https://github.com/elastic/kibana/issues/147667
|
||||
describe.skip('Dashboard panel options a11y tests', () => {
|
||||
let header: WebElementWrapper;
|
||||
const title = '[Flights] Flight count';
|
||||
before(async () => {
|
||||
await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', {
|
||||
useActualUrl: true,
|
||||
|
@ -28,7 +27,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard');
|
||||
header = await dashboardPanelActions.getPanelHeading('[Flights] Flight count');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -40,13 +38,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// dashboard panel options in view mode
|
||||
it('dashboard panel - open menu', async () => {
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await dashboardPanelActions.toggleContextMenuByTitle(title);
|
||||
await a11y.testAppSnapshot();
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await dashboardPanelActions.toggleContextMenuByTitle(title);
|
||||
});
|
||||
|
||||
it('dashboard panel - customize time range', async () => {
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await dashboardPanelActions.toggleContextMenuByTitle(title);
|
||||
await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE');
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('cancelPerPanelTimeRangeButton');
|
||||
|
@ -79,21 +77,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await inspector.close();
|
||||
});
|
||||
|
||||
it('dashboard panel- more options in view mode', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
it('dashboard panel - maximize', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await dashboardPanelActions.clickExpandPanelToggle();
|
||||
await a11y.testAppSnapshot();
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await dashboardPanelActions.clickExpandPanelToggle();
|
||||
});
|
||||
|
||||
it('dashboard panel - copy to dashboard', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await dashboardPanelActions.openContextMenuByTitle(title);
|
||||
await testSubjects.click('embeddablePanelAction-copyToDashboard');
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('cancelCopyToButton');
|
||||
|
@ -103,14 +94,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('dashboard panel - clone panel', async () => {
|
||||
await testSubjects.click('dashboardEditMode');
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await dashboardPanelActions.openContextMenuByTitle(title);
|
||||
await testSubjects.click('embeddablePanelAction-clonePanel');
|
||||
await toasts.dismissAll();
|
||||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
it('dashboard panel - edit panel title', async () => {
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await dashboardPanelActions.customizePanel();
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('customEmbeddablePanelHideTitleSwitch');
|
||||
|
@ -120,8 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('dashboard panel - Create drilldown panel', async () => {
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await testSubjects.click('embeddablePanelMore-mainMenu');
|
||||
await dashboardPanelActions.openContextMenuByTitle(title);
|
||||
await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN');
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN');
|
||||
|
@ -136,30 +125,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('dashboard panel - manage drilldown', async () => {
|
||||
await dashboardPanelActions.toggleContextMenu(header);
|
||||
await testSubjects.click('embeddablePanelMore-mainMenu');
|
||||
await dashboardPanelActions.openContextMenuByTitle(title);
|
||||
await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_EDIT_DRILLDOWN');
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('euiFlyoutCloseButton');
|
||||
});
|
||||
|
||||
it('dashboard panel - more options in edit view', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await a11y.testAppSnapshot();
|
||||
});
|
||||
|
||||
it('dashboard panel - save to library', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await testSubjects.click('embeddablePanelAction-saveToLibrary');
|
||||
await dashboardPanelActions.legacySaveToLibrary('', title);
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('saveCancelButton');
|
||||
});
|
||||
|
||||
it('dashboard panel - replace panel', async () => {
|
||||
await dashboardPanelActions.openContextMenuMorePanel(header);
|
||||
await testSubjects.click('embeddablePanelAction-replacePanel');
|
||||
await a11y.testAppSnapshot();
|
||||
await testSubjects.click('euiFlyoutCloseButton');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -34,8 +34,38 @@ export default function canvasLensTest({ getService, getPageObjects }: FtrProvid
|
|||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
describe('by-value', () => {
|
||||
it('creates new lens embeddable', async () => {
|
||||
await canvas.createNewVis('lens');
|
||||
await lens.goToTimeRange();
|
||||
await lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
await lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
await lens.saveAndReturn();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.exists('xyVisChart');
|
||||
});
|
||||
|
||||
it('edits lens by-value embeddable', async () => {
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await lens.saveAndReturn();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.exists('xyVisChart');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
@ -57,39 +87,9 @@ export default function canvasLensTest({ getService, getPageObjects }: FtrProvid
|
|||
});
|
||||
});
|
||||
|
||||
describe('by-value', () => {
|
||||
it('creates new lens embeddable', async () => {
|
||||
await canvas.addNewPage();
|
||||
await canvas.createNewVis('lens');
|
||||
await lens.goToTimeRange();
|
||||
await lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
await lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
await lens.saveAndReturn();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.exists('xyVisChart');
|
||||
});
|
||||
|
||||
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();
|
||||
await testSubjects.exists('xyVisChart');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
|
|
@ -36,7 +36,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('edits map by-value embeddable', async () => {
|
||||
const originalEmbeddableCount = await canvas.getEmbeddableCount();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await maps.saveMap('canvas test map');
|
||||
const embeddableCount = await canvas.getEmbeddableCount();
|
||||
|
|
|
@ -71,7 +71,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('edits tsvb by-value embeddable', async () => {
|
||||
const originalEmbeddableCount = await canvas.getEmbeddableCount();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await visualize.saveVisualizationAndReturn();
|
||||
await retry.try(async () => {
|
||||
|
@ -93,7 +92,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('edits vega by-value embeddable', async () => {
|
||||
const originalEmbeddableCount = await canvas.getEmbeddableCount();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await visualize.saveVisualizationAndReturn();
|
||||
await retry.try(async () => {
|
||||
|
|
|
@ -47,7 +47,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
|
||||
it('edits to a by value lens panel are properly applied', async () => {
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await lens.switchToVisualization('pie');
|
||||
await lens.saveAndReturn();
|
||||
|
@ -60,7 +59,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('editing and saving a lens by value panel retains number of panels', async () => {
|
||||
const originalPanelCount = await dashboard.getPanelCount();
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await lens.switchToVisualization('treemap');
|
||||
await lens.saveAndReturn();
|
||||
|
@ -73,7 +71,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const newTitle = 'look out library, here I come!';
|
||||
const originalPanelCount = await dashboard.getPanelCount();
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await lens.save(newTitle, false, true);
|
||||
await dashboard.waitForRenderComplete();
|
||||
|
|
|
@ -42,7 +42,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await dashboard.switchToEditMode();
|
||||
}
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await maps.clickAddLayer();
|
||||
await maps.selectLayerGroupCard();
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const { dashboard, lens } = getPageObjects(['dashboard', 'lens']);
|
||||
|
||||
const EMPTY_TITLE = '[No Title]';
|
||||
const EMPTY_TITLE = undefined;
|
||||
|
||||
describe('panel titles', () => {
|
||||
before(async () => {
|
||||
|
@ -112,7 +112,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('resetting description on a by reference panel sets it to the library title', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.navigateToEditorFromFlyout();
|
||||
// legacySaveToLibrary UI cannot set description
|
||||
await lens.save(
|
||||
|
|
|
@ -50,11 +50,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('action exists in panel context menu', async () => {
|
||||
await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
|
||||
await panelActions.openContextMenu();
|
||||
await testSubjects.existOrFail(ACTION_TEST_SUBJ);
|
||||
await panelActions.expectExistsPanelAction(ACTION_TEST_SUBJ);
|
||||
});
|
||||
|
||||
it('is a link <a> element', async () => {
|
||||
await panelActions.openContextMenuByTitle('Visualization PieChart');
|
||||
const actionElement = await testSubjects.find(ACTION_TEST_SUBJ);
|
||||
const tag = await actionElement.getTagName();
|
||||
|
||||
|
@ -87,8 +87,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
exitFromEditMode: true,
|
||||
});
|
||||
|
||||
await panelActions.openContextMenu();
|
||||
await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ);
|
||||
await panelActions.clickPanelAction(ACTION_TEST_SUBJ);
|
||||
await discover.waitForDiscoverAppOnScreen();
|
||||
|
||||
const text = await timePicker.getShowDatesButtonText();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
|
@ -17,7 +18,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const reportingService = getService('reporting');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const filterBar = getService('filterBar');
|
||||
const find = getService('find');
|
||||
const retry = getService('retry');
|
||||
const toasts = getService('toasts');
|
||||
const { reporting, common, dashboard, timePicker } = getPageObjects([
|
||||
|
@ -45,14 +45,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
return res.text;
|
||||
};
|
||||
|
||||
const clickActionsMenu = async (headingTestSubj: string) => {
|
||||
const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-' + headingTestSubj);
|
||||
await dashboardPanelActions.toggleContextMenu(savedSearchPanel);
|
||||
const clickDownloadCsv = async (wrapper?: WebElementWrapper) => {
|
||||
log.debug('click "Generate CSV"');
|
||||
await dashboardPanelActions.clickPanelAction(
|
||||
'embeddablePanelAction-generateCsvReport',
|
||||
wrapper
|
||||
);
|
||||
await testSubjects.existOrFail('csvReportStarted'); // validate toast panel
|
||||
};
|
||||
|
||||
const clickDownloadCsv = async () => {
|
||||
log.debug('click "Generate CSV"');
|
||||
await dashboardPanelActions.clickContextMenuItem('embeddablePanelAction-generateCsvReport');
|
||||
const clickDownloadCsvByTitle = async (title?: string) => {
|
||||
log.debug(`click "Generate CSV" on "${title}"`);
|
||||
await dashboardPanelActions.clickPanelActionByTitle(
|
||||
'embeddablePanelAction-generateCsvReport',
|
||||
title
|
||||
);
|
||||
await testSubjects.existOrFail('csvReportStarted'); // validate toast panel
|
||||
};
|
||||
|
||||
|
@ -82,8 +89,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('Generate CSV export of a saved search panel', async function () {
|
||||
await dashboard.loadSavedDashboard('Ecom Dashboard - 3 Day Period');
|
||||
await clickActionsMenu('EcommerceData');
|
||||
await clickDownloadCsv();
|
||||
await clickDownloadCsvByTitle('EcommerceData');
|
||||
|
||||
const csvFile = await getCsvReportData();
|
||||
expect(csvFile.length).to.be(76137);
|
||||
|
@ -95,9 +101,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// add a filter
|
||||
await filterBar.addFilter({ field: 'category', operation: 'is', value: `Men's Shoes` });
|
||||
|
||||
await clickActionsMenu('EcommerceData');
|
||||
await clickDownloadCsv();
|
||||
await clickDownloadCsvByTitle('EcommerceData');
|
||||
|
||||
const csvFile = await getCsvReportData();
|
||||
expect(csvFile.length).to.be(17106);
|
||||
|
@ -106,9 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('Downloads a saved search panel with a custom time range that does not intersect with dashboard time range', async function () {
|
||||
await dashboard.loadSavedDashboard('Ecom Dashboard - 3 Day Period - custom time range');
|
||||
|
||||
await clickActionsMenu('EcommerceData');
|
||||
await clickDownloadCsv();
|
||||
await clickDownloadCsvByTitle('EcommerceData');
|
||||
|
||||
const csvFile = await getCsvReportData();
|
||||
expect(csvFile.length).to.be(23277);
|
||||
|
@ -117,12 +119,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('Gets the correct filename if panel titles are hidden', async () => {
|
||||
await dashboard.loadSavedDashboard('Ecom Dashboard Hidden Panel Titles');
|
||||
const savedSearchPanel = await find.byCssSelector(
|
||||
'[data-test-embeddable-id="94eab06f-60ac-4a85-b771-3a8ed475c9bb"]'
|
||||
const savedSearchPanel = await dashboardPanelActions.getPanelWrapperById(
|
||||
'94eab06f-60ac-4a85-b771-3a8ed475c9bb'
|
||||
); // panel title is hidden
|
||||
await dashboardPanelActions.toggleContextMenu(savedSearchPanel);
|
||||
|
||||
await clickDownloadCsv();
|
||||
await clickDownloadCsv(savedSearchPanel);
|
||||
await testSubjects.existOrFail('csvReportStarted');
|
||||
|
||||
const csvFile = await getCsvReportData();
|
||||
|
@ -158,8 +159,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('Downloads filtered Discover saved search report', async () => {
|
||||
await clickActionsMenu(TEST_SEARCH_TITLE.replace(/ /g, ''));
|
||||
await clickDownloadCsv();
|
||||
await clickDownloadCsvByTitle(TEST_SEARCH_TITLE);
|
||||
|
||||
const csvFile = await getCsvReportData();
|
||||
expect(csvFile.length).to.be(2446);
|
||||
|
@ -196,8 +196,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('Generate CSV export of a saved search panel', async () => {
|
||||
await clickActionsMenu('namessearch');
|
||||
await clickDownloadCsv();
|
||||
await clickDownloadCsvByTitle('namessearch');
|
||||
|
||||
const csvFile = await getCsvReportData();
|
||||
expect(csvFile.length).to.be(166);
|
||||
|
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.2 MiB |
|
@ -54,9 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
}
|
||||
|
||||
const checkDiscoverNavigationResult = async () => {
|
||||
await dashboardPanelActions.clickContextMenuItem(
|
||||
'embeddablePanelAction-ACTION_OPEN_IN_DISCOVER'
|
||||
);
|
||||
await dashboardPanelActions.clickPanelAction('embeddablePanelAction-ACTION_OPEN_IN_DISCOVER');
|
||||
|
||||
const [, discoverHandle] = await browser.getAllWindowHandles();
|
||||
await browser.switchToWindow(discoverHandle);
|
||||
|
|
|
@ -49,7 +49,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await lens.save('New Lens from Modal', false, false, false, 'new');
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
||||
log.debug('Adds a secondary dimension');
|
||||
|
@ -90,7 +89,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
await dashboardPanelActions.legacySaveToLibrary('My by reference visualization');
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
||||
log.debug('Removes breakdown dimension');
|
||||
|
@ -110,7 +108,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await lens.save('New Lens from Modal', false, false, false, 'new');
|
||||
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
||||
log.debug('Adds a secondary dimension');
|
||||
|
@ -150,7 +147,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboard.waitForRenderComplete();
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
||||
log.debug('Adds annotation');
|
||||
|
@ -177,7 +173,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboard.waitForRenderComplete();
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickInlineEdit();
|
||||
|
||||
log.debug('Adds reference line');
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
exitFromEditMode: true,
|
||||
});
|
||||
|
||||
await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
|
||||
const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles();
|
||||
await browser.switchToWindow(discoverWindowHandle);
|
||||
|
@ -59,7 +59,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should show the open button for a compatible saved visualization with annotations and reference line', async () => {
|
||||
await dashboard.switchToEditMode();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await lens.createLayer('annotations');
|
||||
|
@ -73,7 +72,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
exitFromEditMode: true,
|
||||
});
|
||||
|
||||
await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
|
||||
const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles();
|
||||
await browser.switchToWindow(discoverWindowHandle);
|
||||
|
@ -90,7 +89,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should bring both dashboard context and visualization context to discover', async () => {
|
||||
await dashboard.switchToEditMode();
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await savedQueryManagementComponent.openSavedQueryManagementComponent();
|
||||
await queryBar.switchQueryLanguage('lucene');
|
||||
|
@ -119,7 +117,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await dashboardPanelActions.expectExistsPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
await dashboard.clickCancelOutOfEditMode();
|
||||
|
||||
await dashboardPanelActions.clickContextMenuItem(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(OPEN_IN_DISCOVER_DATA_TEST_SUBJ);
|
||||
|
||||
const [dashboardWindowHandle, discoverWindowHandle] = await browser.getAllWindowHandles();
|
||||
await browser.switchToWindow(discoverWindowHandle);
|
||||
|
|
|
@ -97,7 +97,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('retains its saved object tags after save and return', async () => {
|
||||
await dashboardPanelActions.openContextMenu();
|
||||
await dashboardPanelActions.clickEdit();
|
||||
await lens.saveAndReturn();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
|
|
@ -53,14 +53,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await dashboardCustomizePanel.clickSaveButton();
|
||||
await dashboard.waitForRenderComplete();
|
||||
await dashboardBadgeActions.expectExistsTimeRangeBadgeAction();
|
||||
await panelActions.openContextMenu();
|
||||
const editInLensExists = await testSubjects.exists(
|
||||
'embeddablePanelAction-ACTION_EDIT_IN_LENS'
|
||||
);
|
||||
if (!editInLensExists) {
|
||||
await testSubjects.click('embeddablePanelMore-mainMenu');
|
||||
}
|
||||
await testSubjects.click('embeddablePanelAction-ACTION_EDIT_IN_LENS');
|
||||
await panelActions.convertToLens();
|
||||
|
||||
await lens.waitForVisualization('xyVisChart');
|
||||
await retry.try(async () => {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should filter dashboard by map extent when "filter by map extent" is enabled', async () => {
|
||||
await dashboardPanelActions.clickContextMenuItemByTitle(
|
||||
await dashboardPanelActions.clickPanelActionByTitle(
|
||||
FILTER_BY_MAP_EXTENT_DATA_TEST_SUBJ,
|
||||
'document example'
|
||||
);
|
||||
|
@ -59,7 +59,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should remove map extent filter dashboard when "filter by map extent" is disabled', async () => {
|
||||
await dashboardPanelActions.clickContextMenuItemByTitle(
|
||||
await dashboardPanelActions.clickPanelActionByTitle(
|
||||
FILTER_BY_MAP_EXTENT_DATA_TEST_SUBJ,
|
||||
'document example'
|
||||
);
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function (ctx: FtrProviderContext) {
|
|||
break;
|
||||
case 'dashboard':
|
||||
await dashboard.navigateToApp();
|
||||
await dashboard.gotoDashboardEditMode('A Dashboard');
|
||||
await dashboard.loadSavedDashboard('A Dashboard');
|
||||
break;
|
||||
case 'maps':
|
||||
await maps.openNewMap();
|
||||
|
|
|
@ -18,7 +18,7 @@ export function MachineLearningLensVisualizationsProvider(
|
|||
|
||||
return {
|
||||
async clickCreateMLJobMenuAction(title = '') {
|
||||
await dashboardPanelActions.clickContextMenuItemByTitle(
|
||||
await dashboardPanelActions.clickPanelActionByTitle(
|
||||
'embeddablePanelAction-create-ml-ad-job-action',
|
||||
title
|
||||
);
|
||||
|
|
|
@ -402,7 +402,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
await common.navigateToApp('dashboard');
|
||||
await dashboard.preserveCrossAppState();
|
||||
await dashboard.loadSavedDashboard(myDashboardName);
|
||||
await dashboardPanelActions.clickContextMenuItem(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ);
|
||||
await testSubjects.click('cases-table-add-case-filter-bar');
|
||||
|
||||
await cases.create.createCase({
|
||||
|
@ -434,7 +434,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
await dashboard.preserveCrossAppState();
|
||||
await dashboard.loadSavedDashboard(myDashboardName);
|
||||
|
||||
await dashboardPanelActions.clickContextMenuItem(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ);
|
||||
await dashboardPanelActions.clickPanelAction(ADD_TO_EXISTING_CASE_DATA_TEST_SUBJ);
|
||||
|
||||
await testSubjects.click(`cases-table-row-select-${theCase.id}`);
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ export function createScenarios(
|
|||
};
|
||||
|
||||
const tryDashboardGenerateCsvFail = async (savedSearchTitle: string) => {
|
||||
await dashboardPanelActions.clickContextMenuItemByTitle(
|
||||
await dashboardPanelActions.clickPanelActionByTitle(
|
||||
GENERATE_CSV_DATA_TEST_SUBJ,
|
||||
savedSearchTitle
|
||||
);
|
||||
|
@ -94,7 +94,7 @@ export function createScenarios(
|
|||
GENERATE_CSV_DATA_TEST_SUBJ,
|
||||
savedSearchTitle
|
||||
);
|
||||
await dashboardPanelActions.clickContextMenuItemByTitle(
|
||||
await dashboardPanelActions.clickPanelActionByTitle(
|
||||
GENERATE_CSV_DATA_TEST_SUBJ,
|
||||
savedSearchTitle
|
||||
);
|
||||
|
|