[Dashboard] Presentation panel refactor (#207275)

Closes https://github.com/elastic/kibana/issues/206686
Closes https://github.com/elastic/kibana/issues/197897
Part of https://github.com/elastic/kibana/issues/207852

## Summary

This PR is a major refactor of the `PresentationPanel` component,
including an overhaul of the hover action and panel title components.
Some notable highlights include:
- All styles in the `PresentationPanel` component were moved from SASS
to Emotion
- The over-complicated logic to combine hover actions when the panel
shrinks was removed in favour of CSS, driven by a [container
query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries)
Removing the `updateCombineHoverActions` function (which was defined in
a React component and not memoized) also made a difference in
performance when dragging:
    
   | Before | After |
   |--------|--------|
|
![image](https://github.com/user-attachments/assets/e66898d6-a6fc-42c7-9e24-f116d3bd85a6)
|
![image](https://github.com/user-attachments/assets/1f1d75ba-2ebc-4def-9d2e-14dfd5e1a585)
|
      

- The over-complicated logic defined in
`usePresentationPanelTitleClickHandle`, which was meant to ignore the
`onClick` that would trigger after a panel was dragged, was converted to
2 lines of CSS

### Small usability improvements

This PR also includes a few small usability improvements, such as:

- Ensuring that only the **first** row of hover actions overlaps with
the Dashboard's sticky top navigation bar, and this only happens when
the dashboard has no controls. This results in much better behaviour in
most scenarios:
  
  | Before | After |
  |--------|--------|
| ![Jan-27-2025
16-14-26](https://github.com/user-attachments/assets/2bf5eaa0-06ab-4d87-897f-d217f189daf7)
| ![Jan-27-2025
16-13-41](https://github.com/user-attachments/assets/61b0f06a-1363-4bfc-8a2b-c57a3e736552)
|

- Adding a small delay for hiding the hover actions on mouse leave,
which makes it a lot easier to grab the drag handle:

  | Before | After |
  |--------|--------|
| ![Jan-27-2025
16-21-11](https://github.com/user-attachments/assets/65138e53-1856-44f0-913f-01383b8aa6c2)
| ![Jan-27-2025
16-20-17](https://github.com/user-attachments/assets/7c8ba4d8-8b77-4bc5-85af-a082cace1f96)
|

- Preventing the resize handle from overlapping Dashboard's stick top
navigation:

  | Before | After |
  |--------|--------|
| ![Jan-27-2025
16-24-31](https://github.com/user-attachments/assets/5363a302-5f6a-4483-9782-516023567d87)
| ![Jan-27-2025
16-25-04](https://github.com/user-attachments/assets/8614d025-b45b-4af2-81d6-c62a086ca427)
|


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Hannah Mudge 2025-02-05 15:18:04 -07:00 committed by GitHub
parent 7f28ae63e3
commit c35698bcf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 455 additions and 639 deletions

View file

@ -1,133 +0,0 @@
.embPanel {
z-index: auto;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
&-isLoading {
// completely center the loading indicator
justify-content: center;
align-items: center;
}
// SASSTODO: The inheritence factor stemming from embeddables makes this class hard to change
.embPanel__content {
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
border-radius: $euiBorderRadius;
overflow: hidden;
&[data-error] {
display: none;
}
}
.embPanel__content--hidden {
display: none;
}
// SASSTODO: this MIGHT be fixing IE
.embPanel__content--fullWidth {
width: 100%;
}
}
// HEADER
.embPanel__header {
flex: 0 0 auto;
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 {
@include euiTitle('xxs');
overflow: hidden;
line-height: 1.5;
flex-grow: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
&:not(:empty) {
line-height: $euiSizeL;
padding-left: $euiSizeS;
}
.embPanel__titleInner {
overflow: hidden;
display: flex;
align-items: center;
padding-right: $euiSizeS;
}
.embPanel__titleTooltipAnchor {
max-width: 100%;
}
.embPanel__titleText {
@include euiTextTruncate;
font-weight: $euiFontWeightSemiBold;
}
.embPanel__placeholderTitleText {
color: $euiColorMediumShade;
font-weight: $euiFontWeightRegular;
}
}
.embPanel--dragHandle:not(.embPanel__title) {
flex-grow: 1;
}
.embPanel__header--floater {
position: absolute;
right: 0;
top: 0;
left: 0;
* {
z-index: $euiZLevel1; // apply high z-index to all children
}
}
// EDITING MODE
.embPanel--editing {
.embPanel--dragHandle {
.embPanel--dragHandle:hover {
background-color: transparentize($euiColorWarning, lightOrDarkTheme(.9, .7));
cursor: move;
}
}
}
// LOADING and ERRORS
.embPanel__label {
position: absolute;
padding-left: $euiSizeS;
z-index: $euiZLevel1;
}
.embPanel--dragHandle {
cursor: move;
img {
pointer-events: all !important;
}
}
.embPanel__descriptionTooltipAnchor {
padding: $euiSizeXS;
}

View file

@ -7,9 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { transparentize, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { ViewMode } from '@kbn/presentation-publishing';
import classNames from 'classnames';
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
import { PresentationPanelTitle } from './presentation_panel_title';
import { usePresentationPanelHeaderActions } from './use_presentation_panel_header_actions';
@ -38,6 +39,8 @@ export const PresentationPanelHeader = <
showBadges = true,
showNotifications = true,
}: PresentationPanelHeaderProps<ApiType>) => {
const { euiTheme } = useEuiTheme();
const { notificationElements, badgeElements } = usePresentationPanelHeaderActions<ApiType>(
showNotifications,
showBadges,
@ -53,29 +56,45 @@ export const PresentationPanelHeader = <
[setDragHandle]
);
const { captionStyles, headerStyles } = useMemo(() => {
return {
captionStyles: css`
.dshLayout--editing &:hover {
cursor: move;
background-color: ${transparentize(euiTheme.colors.warning, 0.2)};
}
`,
headerStyles: css`
height: ${euiTheme.size.l};
overflow: hidden;
line-height: ${euiTheme.size.l};
padding: 0px ${euiTheme.size.s};
display: flex;
flex-grow: 1;
flex-wrap: wrap;
column-gap: ${euiTheme.size.s};
align-items: center;
`,
};
}, [euiTheme.colors.warning, euiTheme.size]);
const showPanelBar =
(!hideTitle && panelTitle) || badgeElements.length > 0 || notificationElements.length > 0;
if (!showPanelBar) return null;
const headerClasses = classNames('embPanel__header', {
'embPanel--dragHandle': viewMode === 'edit',
'embPanel__header--floater': !showPanelBar,
});
const titleClasses = classNames('embPanel__title', {
'embPanel--dragHandle': viewMode === 'edit',
});
return (
<figcaption
className={headerClasses}
data-test-subj={`embeddablePanelHeading-${(panelTitle || '').replace(/\s/g, '')}`}
className={'embPanel__header'}
css={captionStyles}
>
<div
className="embPanel__title"
ref={memoizedSetDragHandle}
data-test-subj="dashboardPanelTitle"
className={titleClasses}
css={headerStyles}
>
<PresentationPanelTitle
api={api}

View file

@ -33,25 +33,25 @@ import {
} from '@elastic/eui';
import { ActionExecutionContext, buildContextMenuForActions } from '@kbn/ui-actions-plugin/public';
import { css } from '@emotion/react';
import {
apiCanLockHoverActions,
EmbeddableApiContext,
getViewModeSubject,
useBatchedOptionalPublishingSubjects,
ViewMode,
} from '@kbn/presentation-publishing';
import { Subscription } from 'rxjs';
import { css } from '@emotion/react';
import { ActionWithContext } from '@kbn/ui-actions-plugin/public/context_menu/build_eui_context_menu_panels';
import { Subscription } from 'rxjs';
import { uiActions } from '../../kibana_services';
import {
contextMenuTrigger,
CONTEXT_MENU_TRIGGER,
panelNotificationTrigger,
contextMenuTrigger,
PANEL_NOTIFICATION_TRIGGER,
panelNotificationTrigger,
} from '../../panel_actions';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
import { AnyApiAction } from '../../panel_actions/types';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
import { useHoverActionStyles } from './use_hover_actions_styles';
const getContextMenuAriaLabel = (title?: string, index?: number) => {
if (title) {
@ -130,106 +130,26 @@ export const PresentationPanelHoverActions = ({
const [showNotification, setShowNotification] = useState<boolean>(false);
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
const [notifications, setNotifications] = useState<AnyApiAction[]>([]);
const hoverActionsRef = useRef<HTMLDivElement | null>(null);
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
const anchorRef = useRef<HTMLDivElement | null>(null);
const rightHoverActionsRef = useRef<HTMLDivElement | null>(null);
const [combineHoverActions, setCombineHoverActions] = useState<boolean>(false);
const { euiTheme } = useEuiTheme();
const EDIT_MODE_OUTLINE = `${euiTheme.border.width.thin} dashed ${euiTheme.colors.borderBaseFormsControl}`;
const VIEW_MODE_OUTLINE = `${euiTheme.border.width.thin} solid ${euiTheme.colors.borderBasePlain}`;
const ALL_ROUNDED_CORNERS = `
border-radius: ${euiTheme.border.radius.medium};
`;
const TOP_ROUNDED_CORNERS = `
border-top-left-radius: ${euiTheme.border.radius.medium};
border-top-right-radius: ${euiTheme.border.radius.medium};
border-bottom: 0px;
`;
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) +
(dragHandleRef.current?.offsetWidth ?? 0) +
parseInt(euiTheme.size.base, 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(euiTheme.size.base, 10)) {
dragHandleRef.current?.style.removeProperty('right');
dragHandleRef.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', `-${euiTheme.size.s}`);
} else if (shouldCombine) {
hoverActionsRef.current.style.setProperty('top', `-${euiTheme.size.l}`);
} 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?.defaultTitle$,
api?.title$,
api?.description$,
api?.hideTitle$,
api?.hasLockedHoverActions$,
api?.parentApi?.hideTitle$,
/**
* 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 [defaultTitle, title, description, hidePanelTitle, hasLockedHoverActions, parentHideTitle] =
useBatchedOptionalPublishingSubjects(
api?.defaultTitle$,
api?.title$,
api?.description$,
api?.hideTitle$,
api?.hasLockedHoverActions$,
api?.parentApi?.hideTitle$
);
const hideTitle = hidePanelTitle || parentHideTitle;
const showDescription = description && (!title || hideTitle);
const quickActionIds = useMemo(
() => QUICK_ACTION_IDS[parentViewMode === 'edit' ? 'edit' : 'view'],
[parentViewMode]
() => QUICK_ACTION_IDS[viewMode === 'edit' ? 'edit' : 'view'],
[viewMode]
);
const onClose = useCallback(() => {
@ -369,15 +289,7 @@ export const PresentationPanelHoverActions = ({
return () => {
canceled = true;
};
}, [
actionPredicate,
api,
getActions,
isContextMenuOpen,
onClose,
parentViewMode,
quickActionIds,
]);
}, [actionPredicate, api, getActions, isContextMenuOpen, onClose, viewMode, quickActionIds]);
const quickActionElements = useMemo(() => {
if (!api || quickActions.length < 1) return [];
@ -472,6 +384,17 @@ export const PresentationPanelHoverActions = ({
// memoize the drag handle to avoid calling `setDragHandle` unnecessarily
() => (
<button
className={`embPanel--dragHandle`}
css={css`
cursor: move;
visibility: hidden; // default for every mode **except** edit mode
width: 0px;
.embPanel__hoverActionsAnchor--editMode & {
width: auto;
visibility: visible; // overwrite visibility in edit mode
}
`}
ref={(ref) => {
dragHandleRef.current = ref;
setDragHandle('hoverActions', ref);
@ -480,14 +403,13 @@ export const PresentationPanelHoverActions = ({
<EuiIcon
type="move"
color="text"
className={`embPanel--dragHandle`}
aria-label={i18n.translate('presentationPanel.dragHandle', {
defaultMessage: 'Move panel',
})}
data-test-subj="embeddablePanelDragHandle"
css={css`
margin: ${euiTheme.size.xs};
`}
data-test-subj="embeddablePanelDragHandle"
aria-label={i18n.translate('presentationPanel.dragHandle', {
defaultMessage: 'Move panel',
})}
/>
</button>
),
@ -495,170 +417,90 @@ export const PresentationPanelHoverActions = ({
);
const hasHoverActions = quickActionElements.length || contextMenuPanels.lastIndexOf.length;
const { containerStyles, hoverActionStyles } = useHoverActionStyles(
viewMode === 'edit',
showBorder
);
return (
<div
onMouseOver={updateCombineHoverActions}
onFocus={updateCombineHoverActions}
ref={anchorRef}
className={classNames('embPanel__hoverActionsAnchor', {
'embPanel__hoverActionsAnchor--lockHoverActions': hasLockedHoverActions,
'embPanel__hoverActionsAnchor--editMode': viewMode === 'edit',
})}
data-test-embeddable-id={api?.uuid}
data-test-subj={`embeddablePanelHoverActions-${(title || defaultTitle || '').replace(
/\s/g,
''
)}`}
css={css`
border-radius: ${euiTheme.border.radius.medium};
position: relative;
height: 100%;
.embPanel {
${showBorder
? `
outline: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
`
: ''}
}
.embPanel__hoverActions {
opacity: 0;
padding: calc(${euiTheme.size.xs} - 1px);
display: flex;
flex-wrap: nowrap;
background-color: ${euiTheme.colors.backgroundBasePlain};
height: ${euiTheme.size.xl};
pointer-events: all; // Re-enable pointer-events for hover actions
}
&:hover,
&:focus-within,
&.embPanel__hoverActionsAnchor--lockHoverActions {
.embPanel {
outline: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
z-index: ${euiTheme.levels.menu};
}
.embPanel__hoverActionsWrapper {
z-index: ${euiTheme.levels.toast};
top: -${euiTheme.size.xl};
.embPanel__hoverActions {
opacity: 1;
}
}
}
`}
css={containerStyles}
>
{children}
{api ? (
<div
ref={hoverActionsRef}
className="embPanel__hoverActionsWrapper"
css={css`
height: ${euiTheme.size.xl};
position: absolute;
top: 0;
display: flex;
justify-content: space-between;
padding: 0 ${euiTheme.size.base};
flex-wrap: nowrap;
min-width: 100%;
z-index: -1;
pointer-events: none; // Prevent hover actions wrapper from blocking interactions with other panels
`}
>
{viewMode === 'edit' && !combineHoverActions ? (
<div
data-test-subj="embPanel__hoverActions__left"
className={classNames(
'embPanel__hoverActions',
'embPanel__hoverActionsLeft',
className
)}
css={css`
border: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
${borderStyles}
`}
>
{dragHandle}
</div>
) : (
<div /> // necessary for the right hover actions to align correctly when left hover actions are not present
)}
{hasHoverActions ? (
<div
ref={rightHoverActionsRef}
data-test-subj="embPanel__hoverActions__right"
className={classNames(
'embPanel__hoverActions',
'embPanel__hoverActionsRight',
className
)}
css={css`
border: ${viewMode === 'edit' ? EDIT_MODE_OUTLINE : VIEW_MODE_OUTLINE};
${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}
{api && hasHoverActions && (
<div className={classNames('embPanel__hoverActions', className)} css={hoverActionStyles}>
{dragHandle}
{/* Wrapping all "right actions" in a span so that flex space-between works as expected */}
<span>
{showNotifications && notificationElements}
{showDescription && (
<EuiIconTip
size="m"
title={!hideTitle ? title || undefined : undefined}
content={description}
delay="regular"
position="top"
data-test-subj="embeddablePanelDescriptionTooltip"
type="iInCircle"
iconProps={{
css: css`
margin: ${euiTheme.size.xs};
`,
}}
/>
)}
{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}
/>
</EuiPopover>
) : null}
</div>
) : null}
</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}
</span>
</div>
) : null}
)}
</div>
);
};

View file

@ -1,51 +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 React, { useEffect, useState } from 'react';
import { render, screen, fireEvent, renderHook } from '@testing-library/react';
import { usePresentationPanelTitleClickHandler } from './presentation_panel_title';
describe('usePresentationPanelTitleClickHandler', () => {
it('returns null when there is no element to attach listeners to', () => {
const { result } = renderHook(usePresentationPanelTitleClickHandler);
expect(result.current).toBe(null);
});
it('calls the click subscribe handler when the enter button is clicked on the provided element', async () => {
const mockedClickHandler = jest.fn();
const TestComponent = ({ onClickHandler }: { onClickHandler: () => void }) => {
const [$elm, setElm] = useState<HTMLElement | null>(null);
const onClick$ = usePresentationPanelTitleClickHandler($elm);
useEffect(() => {
const subscription = onClick$?.subscribe(onClickHandler);
return () => subscription?.unsubscribe();
}, [onClick$, onClickHandler]);
return (
<div data-test-subj="syntheticClick" ref={setElm}>
Hello World
</div>
);
};
render(<TestComponent onClickHandler={mockedClickHandler} />);
fireEvent.keyDown(await screen.findByTestId('syntheticClick'), {
key: 'Enter',
code: 'Enter',
charCode: 13,
});
expect(mockedClickHandler).toHaveBeenCalled();
});
});

View file

@ -7,23 +7,17 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiIcon, EuiLink, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
import classNames from 'classnames';
import { once } from 'lodash';
import React, { useMemo, useEffect, useRef, useState } from 'react';
import {
type Observable,
fromEvent,
map,
race,
mergeMap,
takeUntil,
takeLast,
takeWhile,
defaultIfEmpty,
repeatWhen,
} from 'rxjs';
EuiIcon,
EuiLink,
EuiScreenReaderOnly,
EuiToolTip,
euiTextTruncate,
useEuiTheme,
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { ViewMode } from '@kbn/presentation-publishing';
import {
@ -32,66 +26,6 @@ import {
} from '../../panel_actions/customize_panel_action';
import { openCustomizePanelFlyout } from '../../panel_actions/customize_panel_action/open_customize_panel';
export const placeholderTitle = i18n.translate('presentationPanel.placeholderTitle', {
defaultMessage: '[No Title]',
});
const getAriaLabelForTitle = (title?: string) => {
return title
? i18n.translate('presentationPanel.enhancedAriaLabel', {
defaultMessage: 'Panel: {title}',
values: { title: title || placeholderTitle },
})
: i18n.translate('presentationPanel.ariaLabel', {
defaultMessage: 'Panel',
});
};
const createDocumentMouseMoveListener = once(() => fromEvent<MouseEvent>(document, 'mousemove'));
const createDocumentMouseUpListener = once(() => fromEvent<MouseEvent>(document, 'mouseup'));
export const usePresentationPanelTitleClickHandler = (titleElmRef: HTMLElement | null) => {
const onClick = useRef<Observable<{ dragged: boolean }> | null>(null);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (titleElmRef) {
const mouseup = createDocumentMouseUpListener();
const mousemove = createDocumentMouseMoveListener();
const mousedown = fromEvent<MouseEvent>(titleElmRef, 'mousedown');
const keydown = fromEvent<KeyboardEvent>(titleElmRef, 'keydown');
const mousedragExclusiveClick$ = mousedown
.pipe(
mergeMap(function (md) {
// create reference for when mouse is down
const startX = md.offsetX;
const startY = md.offsetY;
return mousemove
.pipe(
map(function (mm) {
return { dragged: startX !== mm.clientX && startY !== mm.clientY };
})
)
.pipe(takeUntil(mouseup), takeLast(1))
.pipe(defaultIfEmpty({ dragged: false }));
})
)
.pipe(repeatWhen(() => mousedown));
onClick.current = race(
keydown.pipe(takeWhile((kd) => kd.key === 'Enter')).pipe(map(() => ({ dragged: false }))),
mousedragExclusiveClick$
);
setInitialized(true);
}
}, [titleElmRef]);
return initialized ? onClick.current : null;
};
export const PresentationPanelTitle = ({
api,
headerId,
@ -107,80 +41,92 @@ export const PresentationPanelTitle = ({
panelDescription?: string;
viewMode?: ViewMode;
}) => {
const [panelTitleElmRef, setPanelTitleElmRef] = useState<HTMLElement | null>(null);
const { euiTheme } = useEuiTheme();
const onClick = useCallback(() => {
openCustomizePanelFlyout({
api: api as CustomizePanelActionApi,
focusOnTitle: true,
});
}, [api]);
const panelTitleElement = useMemo(() => {
if (hideTitle) return null;
const titleClassNames = classNames('embPanel__titleText', {
// eslint-disable-next-line @typescript-eslint/naming-convention
embPanel__placeholderTitleText: !panelTitle,
});
const titleStyles = css`
${euiTextTruncate()};
font-weight: ${euiTheme.font.weight.bold};
.kbnGridPanel--active & {
pointer-events: none; // prevent drag event from triggering onClick
}
`;
if (viewMode !== 'edit' || !isApiCompatibleWithCustomizePanelAction(api)) {
return <span className={titleClassNames}>{panelTitle}</span>;
return (
<span data-test-subj="embeddablePanelTitle" css={titleStyles}>
{panelTitle}
</span>
);
}
return (
<EuiLink
color="text"
ref={setPanelTitleElmRef}
className={titleClassNames}
onClick={onClick}
css={titleStyles}
aria-label={i18n.translate('presentationPanel.header.titleAriaLabel', {
defaultMessage: 'Click to edit title: {title}',
values: { title: panelTitle ?? placeholderTitle },
values: { title: panelTitle },
})}
data-test-subj={'embeddablePanelTitleLink'}
data-test-subj="embeddablePanelTitle"
>
{panelTitle || placeholderTitle}
{panelTitle}
</EuiLink>
);
}, [setPanelTitleElmRef, hideTitle, panelTitle, viewMode, api]);
const onClick = usePresentationPanelTitleClickHandler(panelTitleElmRef);
useEffect(() => {
const panelTitleClickSubscription = onClick?.subscribe(function onClickHandler({ dragged }) {
if (!dragged) {
openCustomizePanelFlyout({
api: api as CustomizePanelActionApi,
focusOnTitle: true,
});
}
});
return () => panelTitleClickSubscription?.unsubscribe();
}, [api, onClick]);
}, [onClick, hideTitle, panelTitle, viewMode, api, euiTheme]);
const describedPanelTitleElement = useMemo(() => {
if (hideTitle) return null;
if (!panelDescription) {
return (
<span data-test-subj="embeddablePanelTitleInner" className="embPanel__titleInner">
{panelTitleElement}
</span>
);
}
const ariaLabel = getAriaLabelForTitle(panelTitle);
const ariaLabelElement = (
<EuiScreenReaderOnly>
<span id={headerId}>{ariaLabel}</span>
</EuiScreenReaderOnly>
);
if (!panelDescription) {
return panelTitleElement;
}
return (
<EuiToolTip
title={!hideTitle ? panelTitle || undefined : undefined}
title={panelTitle}
content={panelDescription}
delay="regular"
position="top"
anchorClassName="embPanel__titleTooltipAnchor"
anchorProps={{ 'data-test-subj': 'embeddablePanelTooltipAnchor' }}
anchorProps={{
'data-test-subj': 'embeddablePanelTooltipAnchor',
css: css`
max-width: 100%;
display: flex;
flex-wrap: nowrap;
column-gap: ${euiTheme.size.xs};
align-items: center;
`,
}}
>
<div data-test-subj="embeddablePanelTitleInner" className="embPanel__titleInner">
{!hideTitle ? (
<h2>
{ariaLabelElement}
<EuiScreenReaderOnly>
<span id={headerId}>
{panelTitle
? i18n.translate('presentationPanel.ariaLabel', {
defaultMessage: 'Panel: {title}',
values: {
title: panelTitle,
},
})
: i18n.translate('presentationPanel.untitledPanelAriaLabel', {
defaultMessage: 'Untitled panel',
})}
</span>
</EuiScreenReaderOnly>
{panelTitleElement}&nbsp;
</h2>
) : null}
@ -193,7 +139,7 @@ export const PresentationPanelTitle = ({
</div>
</EuiToolTip>
);
}, [hideTitle, panelDescription, panelTitle, panelTitleElement, headerId]);
}, [hideTitle, panelDescription, panelTitle, panelTitleElement, headerId, euiTheme.size.xs]);
return describedPanelTitleElement;
};

View file

@ -0,0 +1,142 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { useMemo } from 'react';
export const useHoverActionStyles = (isEditMode: boolean, showBorder?: boolean) => {
const { euiTheme } = useEuiTheme();
const containerStyles = useMemo(() => {
const editModeOutline = `${euiTheme.border.width.thin} dashed ${euiTheme.colors.borderBaseFormsControl}`;
const viewModeOutline = `${euiTheme.border.width.thin} solid ${euiTheme.colors.borderBasePlain}`;
return css`
// the border style can be overwritten by parents who define --hoverActionsBorderStyle; otherwise, default to either
// editModeOutline or viewModeOutline depending on view mode
--internalBorderStyle: var(
--hoverActionsBorderStyle,
${isEditMode ? editModeOutline : viewModeOutline}
);
container: hoverActionsAnchor / inline-size;
border-radius: ${euiTheme.border.radius.medium};
position: relative;
display: inline-block;
width: 100%;
height: 100%;
${showBorder
? css`
.embPanel {
outline: var(--internalBorderStyle);
}
`
: css`
&:hover .embPanel {
outline: var(--internalBorderStyle);
z-index: ${euiTheme.levels.menu};
}
`}
.embPanel__hoverActions {
position: absolute;
top: -${euiTheme.size.xl};
z-index: -1;
opacity: 0;
visibility: hidden;
// delay hiding hover actions to make grabbing the drag handle easier
transition: ${euiTheme.animation.extraFast} opacity ease-in,
${euiTheme.animation.extraFast} z-index linear,
${euiTheme.animation.extraFast} visibility linear;
transition-delay: ${euiTheme.animation.fast};
}
&:hover .embPanel__hoverActions,
&:has(:focus-visible) .embPanel__hoverActions,
&.embPanel__hoverActionsAnchor--lockHoverActions .embPanel__hoverActions,
.embPanel__hoverActions:hover,
.embPanel__hoverActions:has(:focus-visible) {
z-index: ${euiTheme.levels.menu};
opacity: 1;
visibility: visible;
transition: none; // apply transition delay on hover out only
}
`;
}, [euiTheme, showBorder, isEditMode]);
const hoverActionStyles = useMemo(() => {
const singleWrapperStyles = css`
width: fit-content;
top: -${euiTheme.size.l} !important;
right: ${euiTheme.size.xs};
padding: var(--paddingAroundAction);
border-radius: ${euiTheme.border.radius.medium};
border: var(--internalBorderStyle);
background-color: ${euiTheme.colors.backgroundBasePlain};
grid-template-columns: max-content;
& > * {
// undo certain styles on all children so that parent takes precedence
border: none !important;
padding: 0px !important;
border-radius: unset !important;
background-color: transparent !important;
height: unset !important;
}
`;
return css`
--paddingAroundAction: calc(${euiTheme.size.xs} - 1px);
pointer-events: none; // prevent hover actions wrapper from blocking interactions with other panels
width: 100%;
height: ${euiTheme.size.xl};
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px ${euiTheme.size.m};
& > * {
// apply styles to all children
display: flex;
height: ${euiTheme.size.xl};
flex: 0; // do not grow
pointer-events: all; // re-enable pointer events for non-breakpoint children
background-color: ${euiTheme.colors.backgroundBasePlain};
border: var(--internalBorderStyle);
border-bottom: 0px;
padding: var(--paddingAroundAction);
padding-bottom: 0px;
border-top-left-radius: ${euiTheme.border.radius.medium};
border-top-right-radius: ${euiTheme.border.radius.medium};
}
// shrink down to single wrapped element with no breakpoint when panel gets small
@container hoverActionsAnchor (width < 200px) {
${singleWrapperStyles}
}
// when Dashboard is in fullscreen mode, combine all floating actions on first row and nudge them down;
// if the panel is **not** on the first row but it is expanded in fullscreen mode, do the same thing
.dshDashboardViewportWrapper--isFullscreen .dshDashboardGrid__item[data-grid-row='0'] &,
.dshDashboardViewportWrapper--isFullscreen .kbnGridPanel--expanded & {
${singleWrapperStyles}
top: -${euiTheme.size.s} !important;
}
`;
}, [euiTheme]);
return { containerStyles, hoverActionStyles };
};

View file

@ -13,10 +13,10 @@ import { Subscription } from 'rxjs';
import { uiActions } from '../../kibana_services';
import {
panelBadgeTrigger,
panelNotificationTrigger,
PANEL_BADGE_TRIGGER,
PANEL_NOTIFICATION_TRIGGER,
panelBadgeTrigger,
panelNotificationTrigger,
} from '../../panel_actions';
import { AnyApiAction } from '../../panel_actions/types';
import { DefaultPresentationPanelApi, PresentationPanelInternalProps } from '../types';
@ -128,7 +128,6 @@ export const usePresentationPanelHeaderActions = <
const badgeElement = (
<EuiBadge
key={badge.id}
className="embPanel__headerBadge"
iconType={badge.getIconType({ embeddable: api, trigger: panelBadgeTrigger })}
onClick={() => badge.execute({ embeddable: api, trigger: panelBadgeTrigger })}
onClickAriaLabel={badge.getDisplayName({ embeddable: api, trigger: panelBadgeTrigger })}

View file

@ -26,6 +26,8 @@ import {
} from './types';
describe('Presentation panel', () => {
const editPanelSpy = jest.spyOn(openCustomizePanel, 'openCustomizePanelFlyout');
const renderPresentationPanel = async ({
props,
api,
@ -166,7 +168,7 @@ describe('Presentation panel', () => {
};
await renderPresentationPanel({ api });
await waitFor(() => {
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('SUPER TITLE');
expect(screen.getByTestId('embeddablePanelTitle')).toHaveTextContent('SUPER TITLE');
});
});
@ -177,7 +179,7 @@ describe('Presentation panel', () => {
};
await renderPresentationPanel({ api });
await waitFor(() => {
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('SO Title');
expect(screen.getByTestId('embeddablePanelTitle')).toHaveTextContent('SO Title');
});
});
@ -238,7 +240,7 @@ describe('Presentation panel', () => {
});
it('opens customize panel flyout on title click when in edit mode', async () => {
const spy = jest.spyOn(openCustomizePanel, 'openCustomizePanelFlyout');
editPanelSpy.mockClear();
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
uuid: 'test',
@ -248,15 +250,19 @@ describe('Presentation panel', () => {
};
await renderPresentationPanel({ api });
await waitFor(() => {
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('TITLE');
expect(screen.getByTestId('embeddablePanelTitle')).toBeInTheDocument();
});
expect(screen.queryByTestId('embeddablePanelTitleLink')).toBeInTheDocument();
await userEvent.click(screen.getByTestId('embeddablePanelTitleLink'));
const titleElement = screen.getByTestId('embeddablePanelTitle');
expect(titleElement).toHaveTextContent('TITLE');
expect(titleElement.nodeName).toBe('BUTTON');
expect(spy).toHaveBeenCalled();
await userEvent.click(titleElement);
expect(editPanelSpy).toHaveBeenCalled();
});
it('does not show title customize link in view mode', async () => {
editPanelSpy.mockClear();
const api: DefaultPresentationPanelApi & PublishesDataViews & PublishesViewMode = {
uuid: 'test',
title$: new BehaviorSubject<string | undefined>('SUPER TITLE'),
@ -265,9 +271,14 @@ describe('Presentation panel', () => {
};
await renderPresentationPanel({ api });
await waitFor(() => {
expect(screen.getByTestId('embeddablePanelTitleInner')).toHaveTextContent('SUPER TITLE');
expect(screen.getByTestId('embeddablePanelTitle')).toBeInTheDocument();
});
expect(screen.queryByTestId('embeddablePanelTitleLink')).not.toBeInTheDocument();
const titleElement = screen.getByTestId('embeddablePanelTitle');
expect(titleElement).toHaveTextContent('SUPER TITLE');
expect(titleElement.nodeName).toBe('SPAN');
await userEvent.click(titleElement);
expect(editPanelSpy).not.toHaveBeenCalled();
});
it('hides title in view mode when API hide title option is true', async () => {

View file

@ -7,9 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import './_presentation_panel.scss';
import { EuiErrorBoundary, EuiFlexGroup, EuiPanel, htmlIdGenerator } from '@elastic/eui';
import { css } from '@emotion/react';
import { PanelLoader } from '@kbn/panel-loader';
import {
apiHasParentApi,
@ -129,6 +128,7 @@ export const PresentationPanelInternal = <
aria-labelledby={headerId}
data-test-subj="embeddablePanel"
{...contentAttrs}
css={styles.embPanel}
>
{!hideHeader && api && (
<PresentationPanelHeader
@ -156,7 +156,10 @@ export const PresentationPanelInternal = <
</EuiFlexGroup>
)}
{!initialLoadComplete && <PanelLoader />}
<div className={blockingError ? 'embPanel__content--hidden' : 'embPanel__content'}>
<div
className={blockingError ? 'embPanel__content--hidden' : 'embPanel__content'}
css={styles.embPanelContent}
>
<EuiErrorBoundary>
<Component
{...(componentProps as React.ComponentProps<typeof Component>)}
@ -170,3 +173,31 @@ export const PresentationPanelInternal = <
</PresentationPanelHoverActions>
);
};
/**
* if there is no reliance on EUI theme, then it is more performant to store styles as minimizable objects
* outside of the React component so that it is not parsed on every render
*/
const styles = {
embPanel: css({
zIndex: 'auto',
flex: 1,
display: 'flex',
flexDirection: 'column',
height: '100%',
position: 'relative',
}),
embPanelContent: css({
'&.embPanel__content': {
display: 'flex',
flex: '1 1 100%',
zIndex: 1,
minHeight: 0, // Absolute must for Firefox to scroll contents
borderRadius: '4px',
overflow: 'hidden',
},
'&.embPanel__content--hidden, &[data-error]': {
display: 'none',
},
}),
};

View file

@ -16,15 +16,8 @@
.dshDashboardGrid__item--expanded,
.dshDashboardGrid__item--blurred,
.dshDashboardGrid__item--focused {
.embPanel--dragHandle:hover {
background-color: unset;
cursor: default;
}
.embPanel__hoverActions {
.embPanel--dragHandle {
visibility: hidden;
}
.embPanel--dragHandle {
visibility: hidden;
}
}
@ -32,7 +25,7 @@
.dshDashboardGrid__item--blurred,
.dshDashboardGrid__item--focused {
.embPanel__hoverActions {
visibility: hidden;
visibility: hidden !important;
}
}

View file

@ -10,10 +10,12 @@
import classNames from 'classnames';
import React, { useCallback, useMemo, useRef } from 'react';
import { css } from '@emotion/react';
import { useAppFixedViewport } from '@kbn/core-rendering-browser';
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { useEuiTheme } from '@elastic/eui';
import { DashboardPanelState } from '../../../../common';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../../common/content_management/constants';
import { arePanelLayoutsEqual } from '../../../dashboard_api/are_panel_layouts_equal';
@ -30,6 +32,7 @@ export const DashboardGrid = ({
const dashboardApi = useDashboardApi();
const layoutStyles = useLayoutStyles();
const panelRefs = useRef<{ [panelId: string]: React.Ref<HTMLDivElement> }>({});
const { euiTheme } = useEuiTheme();
const [expandedPanelId, panels, useMargins, viewMode] = useBatchedPublishingSubjects(
dashboardApi.expandedPanelId$,
@ -56,6 +59,11 @@ export const DashboardGrid = ({
width: gridData.w,
height: gridData.h,
};
// update `data-grid-row` attribute for all panels because it is used for some styling
const panelRef = panelRefs.current[panelId];
if (typeof panelRef !== 'function' && panelRef?.current) {
panelRef.current.setAttribute('data-grid-row', `${gridData.y}`);
}
});
return [singleRow];
@ -96,18 +104,17 @@ export const DashboardGrid = ({
if (!panelRefs.current[id]) {
panelRefs.current[id] = React.createRef();
}
const type = currentPanels[id].type;
return (
<DashboardGridItem
ref={panelRefs.current[id]}
data-grid={currentPanels[id].gridData}
key={id}
id={id}
type={type}
setDragHandles={setDragHandles}
appFixedViewport={appFixedViewport}
dashboardContainerRef={dashboardContainerRef}
data-grid-row={currentPanels[id].gridData.y} // initialize data-grid-row
/>
);
},
@ -141,12 +148,32 @@ export const DashboardGrid = ({
viewMode,
]);
const classes = classNames({
'dshLayout-withoutMargins': !useMargins,
'dshLayout--viewing': viewMode === 'view',
'dshLayout--editing': viewMode !== 'view',
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
});
const { dashboardClasses, dashboardStyles } = useMemo(() => {
return {
dashboardClasses: classNames({
'dshLayout-withoutMargins': !useMargins,
'dshLayout--viewing': viewMode === 'view',
'dshLayout--editing': viewMode !== 'view',
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
}),
dashboardStyles: css`
// for dashboards with no controls, increase the z-index of the hover actions in the
// top row so that they overlap the sticky nav in Dashboard
.dshDashboardViewportWrapper:not(:has(.dshDashboardViewport-controls))
&
.dshDashboardGrid__item[data-grid-row='0']
.embPanel__hoverActions {
z-index: ${euiTheme.levels.toast};
}
return <div className={classes}>{memoizedgridLayout}</div>;
// when in fullscreen mode, combine all floating actions on first row and nudge them down
`,
};
}, [useMargins, viewMode, expandedPanelId, euiTheme.levels.toast]);
return (
<div className={dashboardClasses} css={dashboardStyles}>
{memoizedgridLayout}
</div>
);
};

View file

@ -30,6 +30,9 @@ export const useLayoutStyles = () => {
* is resolved, we should swap these out for the drag-specific colour tokens
*/
return css`
--dashboardActivePanelBorderStyle: ${euiTheme.border.width.thick} solid
${euiTheme.colors.vis.euiColorVis0};
&.kbnGrid {
// remove margin top + bottom on grid in favour of padding in row
padding-bottom: 0px;
@ -55,7 +58,7 @@ export const useLayoutStyles = () => {
}
.kbnGridPanel--resizeHandle {
z-index: ${euiTheme.levels.mask};
z-index: ${euiTheme.levels.maskBelowHeader};
// applying mask via ::after allows for focus borders to show
&:after {
@ -79,12 +82,12 @@ export const useLayoutStyles = () => {
}
.kbnGridPanel--active {
.embPanel {
outline: ${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0} !important;
}
// overwrite the border style on panels + hover actions for active panels
--hoverActionsBorderStyle: var(--dashboardActivePanelBorderStyle);
// prevent the hover actions transition when active to prevent "blip" on resize
.embPanel__hoverActions {
border: ${euiTheme.border.width.thick} solid ${euiTheme.colors.vis.euiColorVis0} !important;
border-bottom: 0px solid !important;
transition: none;
}
}
`;

View file

@ -101,6 +101,7 @@ export const DashboardViewport = ({
<div
className={classNames('dshDashboardViewportWrapper', {
'dshDashboardViewportWrapper--defaultBg': !useMargins,
'dshDashboardViewportWrapper--isFullscreen': fullScreenMode,
})}
>
{viewMode !== 'print' ? (

View file

@ -287,7 +287,7 @@ export class DashboardPageObject extends FtrService {
}
// 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.find.allByCssSelector('.embPanel__hoverActionsWrapper');
const panels = await this.find.allByCssSelector('[data-test-subj="embeddablePanel"]');
const dragHandles = await this.find.allByCssSelector(
'[data-test-subj="embeddablePanelDragHandle"]'
);
@ -643,7 +643,7 @@ export class DashboardPageObject extends FtrService {
public async getPanelTitles() {
this.log.debug('in getPanelTitles');
const titleObjects = await this.find.allByCssSelector(
'[data-test-subj="embeddablePanelTitleInner"] .embPanel__titleText'
'[data-test-subj="embeddablePanelTitle"]'
);
return await Promise.all(titleObjects.map(async (title) => await title.getVisibleText()));
}

View file

@ -176,7 +176,6 @@ export class DashboardPanelActionsService extends FtrService {
async clickExpandPanelToggle() {
this.log.debug(`clickExpandPanelToggle`);
await this.openContextMenu();
await this.clickPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ);
}

View file

@ -7,7 +7,7 @@
overflow: visible;
}
.embPanel__hoverActionsLeft, .embPanel__dragHandle {
.embPanel__dragHandle {
visibility: hidden;
}
}
@ -19,14 +19,10 @@
}
.canvas__element--selected {
.embPanel__hoverActionsAnchor {
.embPanel__hoverActionsWrapper {
z-index: $euiZLevel9;
top: -$euiSizeXL;
.embPanel__hoverActions {
opacity: 1;
}
}
.embPanel__hoverActions {
z-index: $euiZLevel9;
top: -$euiSizeXL;
opacity: 1;
visibility: visible;
}
}

View file

@ -6190,7 +6190,6 @@
"presentationPanel.action.inspectPanel.displayName": "Inspecter",
"presentationPanel.action.inspectPanel.untitledEmbeddableFilename": "[Aucun titre]",
"presentationPanel.action.removePanel.displayName": "Supprimer",
"presentationPanel.ariaLabel": "Panneau",
"presentationPanel.badgeTrigger.description": "Des actions de badge apparaissent dans la barre de titre lorsqu'un élément incorporable est en cours de chargement dans un panneau.",
"presentationPanel.badgeTrigger.title": "Badges du panneau",
"presentationPanel.contextMenu.ariaLabel": "Options de panneau",
@ -6199,7 +6198,6 @@
"presentationPanel.contextMenuTrigger.description": "Une nouvelle action sera ajoutée au menu contextuel du panneau",
"presentationPanel.contextMenuTrigger.title": "Menu contextuel",
"presentationPanel.emptyErrorMessage": "Erreur",
"presentationPanel.enhancedAriaLabel": "Panneau : {title}",
"presentationPanel.error.editButton": "Modifier {value}",
"presentationPanel.error.errorWhenLoadingPanel": "Une erreur s'est produite lors du chargement de ce panneau.",
"presentationPanel.filters.filtersTitle": "Filtres",
@ -6207,7 +6205,6 @@
"presentationPanel.header.titleAriaLabel": "Cliquez pour modifier le titre : {title}",
"presentationPanel.notificationTrigger.description": "Les actions de notification apparaissent dans l'angle supérieur droit des panneaux.",
"presentationPanel.notificationTrigger.title": "Notifications du panneau",
"presentationPanel.placeholderTitle": "[Aucun titre]",
"presentationUtil.dashboardDrilldownConfig.components.openInNewTab": "Ouvrir le tableau de bord dans un nouvel onglet",
"presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange": "Utiliser la plage de dates du tableau de bord d'origine",
"presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel": "Utiliser les filtres et la requête du tableau de bord d'origine",

View file

@ -6184,7 +6184,6 @@
"presentationPanel.action.inspectPanel.displayName": "検査",
"presentationPanel.action.inspectPanel.untitledEmbeddableFilename": "[タイトルなし]",
"presentationPanel.action.removePanel.displayName": "削除",
"presentationPanel.ariaLabel": "パネル",
"presentationPanel.badgeTrigger.description": "パネルに埋め込み可能なファイルが読み込まれるときに、バッジアクションがタイトルバーに表示されます。",
"presentationPanel.badgeTrigger.title": "パネルバッジ",
"presentationPanel.contextMenu.ariaLabel": "パネルオプション",
@ -6193,7 +6192,6 @@
"presentationPanel.contextMenuTrigger.description": "新しいアクションがパネルのコンテキストメニューに追加されます",
"presentationPanel.contextMenuTrigger.title": "コンテキストメニュー",
"presentationPanel.emptyErrorMessage": "エラー",
"presentationPanel.enhancedAriaLabel": "パネル:{title}",
"presentationPanel.error.editButton": "{value} を編集",
"presentationPanel.error.errorWhenLoadingPanel": "このパネルの読み込み中にエラーが発生しました。",
"presentationPanel.filters.filtersTitle": "フィルター",
@ -6201,7 +6199,6 @@
"presentationPanel.header.titleAriaLabel": "クリックしてタイトルを編集:{title}",
"presentationPanel.notificationTrigger.description": "パネルの右上に通知アクションが表示されます。",
"presentationPanel.notificationTrigger.title": "パネル通知",
"presentationPanel.placeholderTitle": "[タイトルなし]",
"presentationUtil.dashboardDrilldownConfig.components.openInNewTab": "新しいタブでダッシュボードを開く",
"presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange": "元のダッシュボードから日付範囲を使用",
"presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel": "元のダッシュボードからフィルターとクエリーを使用",

View file

@ -6141,7 +6141,6 @@
"presentationPanel.action.inspectPanel.displayName": "检查",
"presentationPanel.action.inspectPanel.untitledEmbeddableFilename": "[无标题]",
"presentationPanel.action.removePanel.displayName": "移除",
"presentationPanel.ariaLabel": "面板",
"presentationPanel.badgeTrigger.description": "可嵌入对象在面板中加载后,徽章操作便显示在标题栏中。",
"presentationPanel.badgeTrigger.title": "面板徽章",
"presentationPanel.contextMenu.ariaLabel": "面板选项",
@ -6150,7 +6149,6 @@
"presentationPanel.contextMenuTrigger.description": "会将一个新操作添加到该面板的上下文菜单",
"presentationPanel.contextMenuTrigger.title": "上下文菜单",
"presentationPanel.emptyErrorMessage": "错误",
"presentationPanel.enhancedAriaLabel": "面板:{title}",
"presentationPanel.error.editButton": "编辑 {value}",
"presentationPanel.error.errorWhenLoadingPanel": "加载此面板时出错。",
"presentationPanel.filters.filtersTitle": "筛选",
@ -6158,7 +6156,6 @@
"presentationPanel.header.titleAriaLabel": "单击可编辑标题:{title}",
"presentationPanel.notificationTrigger.description": "通知操作显示在面板右上角。",
"presentationPanel.notificationTrigger.title": "面板通知",
"presentationPanel.placeholderTitle": "[无标题]",
"presentationUtil.dashboardDrilldownConfig.components.openInNewTab": "在新选项卡中打开仪表板",
"presentationUtil.dashboardDrilldownConfig.components.useCurrentDateRange": "使用源仪表板的日期范围",
"presentationUtil.dashboardDrilldownConfig.components.useCurrentFiltersLabel": "使用源仪表板的筛选和查询",