Skip to content

Commit

Permalink
[Dashboard] Hover actions for panels (elastic#182535)
Browse files Browse the repository at this point in the history
  • Loading branch information
cqliu1 authored Oct 25, 2024
1 parent c05e1c5 commit 2fdfb8d
Show file tree
Hide file tree
Showing 105 changed files with 1,349 additions and 737 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pageLoadAssetSize:
core: 564663
crossClusterReplication: 65408
customIntegrations: 22034
dashboard: 52967
dashboard: 68015
dashboardEnhanced: 65646
data: 454087
dataQuality: 19384
Expand Down
4 changes: 4 additions & 0 deletions packages/presentation/presentation_publishing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};
Expand All @@ -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');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,22 +41,17 @@ 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);
updateFilters = (filters) => filtersSubject.next(filters);
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,
},
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 ||
Expand Down Expand Up @@ -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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
};
});

Expand All @@ -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();
Expand All @@ -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');
Expand Down
Loading

0 comments on commit 2fdfb8d

Please sign in to comment.