[Time To Visualize] Edit Panel Title On Click (#81076)
* Made embeddable panel title click launch the customize panel action Co-authored-by: Ryan Keairns <contactryank@gmail.com>
This commit is contained in:
parent
096aedfae1
commit
06ea880712
|
@ -64,12 +64,12 @@
|
|||
|
||||
.embPanel__titleText {
|
||||
@include euiTextTruncate;
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
||||
.embPanel__placeholderTitleText {
|
||||
@include euiTextTruncate;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
color: $euiColorMediumShade;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { nextTick } from 'test_utils/enzyme_helpers';
|
||||
import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
|
@ -343,6 +343,88 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => {
|
|||
// expect(action.length).toBe(1);
|
||||
});
|
||||
|
||||
test('Panel title customize link does not exist in view mode', async () => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
||||
const container = new HelloWorldContainer(
|
||||
{ id: '123', panels: {}, viewMode: ViewMode.VIEW, hidePanelTitles: false },
|
||||
{ getEmbeddableFactory } as any
|
||||
);
|
||||
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
ContactCardEmbeddable
|
||||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'Vayon',
|
||||
lastName: 'Poole',
|
||||
});
|
||||
|
||||
const component = mountWithIntl(
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
|
||||
expect(titleLink.length).toBe(0);
|
||||
});
|
||||
|
||||
test('Runs customize panel action on title click when in edit mode', async () => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
||||
const container = new HelloWorldContainer(
|
||||
{ id: '123', panels: {}, viewMode: ViewMode.EDIT, hidePanelTitles: false },
|
||||
{ getEmbeddableFactory } as any
|
||||
);
|
||||
|
||||
const embeddable = await container.addNewEmbeddable<
|
||||
ContactCardEmbeddableInput,
|
||||
ContactCardEmbeddableOutput,
|
||||
ContactCardEmbeddable
|
||||
>(CONTACT_CARD_EMBEDDABLE, {
|
||||
firstName: 'Vayon',
|
||||
lastName: 'Poole',
|
||||
});
|
||||
|
||||
const component = mountWithIntl(
|
||||
<EmbeddablePanel
|
||||
embeddable={embeddable}
|
||||
getActions={() => Promise.resolve([])}
|
||||
getAllEmbeddableFactories={start.getEmbeddableFactories}
|
||||
getEmbeddableFactory={start.getEmbeddableFactory}
|
||||
notifications={{} as any}
|
||||
overlays={{} as any}
|
||||
application={applicationMock}
|
||||
inspector={inspector}
|
||||
SavedObjectFinder={() => null}
|
||||
/>
|
||||
);
|
||||
|
||||
const titleExecute = jest.fn();
|
||||
component.setState((s: any) => ({
|
||||
...s,
|
||||
universalActions: {
|
||||
...s.universalActions,
|
||||
customizePanelTitle: { execute: titleExecute, isCompatible: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
const titleLink = findTestSubject(component, 'embeddablePanelTitleLink');
|
||||
expect(titleLink.length).toBe(1);
|
||||
titleLink.simulate('click');
|
||||
await nextTick();
|
||||
expect(titleExecute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('Updates when hidePanelTitles is toggled', async () => {
|
||||
const inspector = inspectorPluginMock.createStartContract();
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ interface Props {
|
|||
|
||||
interface State {
|
||||
panels: EuiContextMenuPanelDescriptor[];
|
||||
universalActions: PanelUniversalActions;
|
||||
focusedPanelIndex?: string;
|
||||
viewMode: ViewMode;
|
||||
hidePanelTitle: boolean;
|
||||
|
@ -86,6 +87,14 @@ interface State {
|
|||
error?: EmbeddableError;
|
||||
}
|
||||
|
||||
interface PanelUniversalActions {
|
||||
customizePanelTitle: CustomizePanelTitleAction;
|
||||
addPanel: AddPanelAction;
|
||||
inspectPanel: InspectPanelAction;
|
||||
removePanel: RemovePanelAction;
|
||||
editPanel: EditPanelAction;
|
||||
}
|
||||
|
||||
export class EmbeddablePanel extends React.Component<Props, State> {
|
||||
private embeddableRoot: React.RefObject<HTMLDivElement>;
|
||||
private parentSubscription?: Subscription;
|
||||
|
@ -102,6 +111,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
Boolean(embeddable.getInput()?.hidePanelTitles);
|
||||
|
||||
this.state = {
|
||||
universalActions: this.getUniversalActions(),
|
||||
panels: [],
|
||||
viewMode,
|
||||
hidePanelTitle,
|
||||
|
@ -229,6 +239,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
getActionContextMenuPanel={this.getActionContextMenuPanel}
|
||||
hidePanelTitle={this.state.hidePanelTitle}
|
||||
isViewMode={viewOnlyMode}
|
||||
customizeTitle={this.state.universalActions.customizePanelTitle}
|
||||
closeContextMenu={this.state.closeContextMenu}
|
||||
title={title}
|
||||
badges={this.state.badges}
|
||||
|
@ -267,17 +278,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
private getActionContextMenuPanel = async () => {
|
||||
let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, {
|
||||
embeddable: this.props.embeddable,
|
||||
});
|
||||
|
||||
const { disabledActions } = this.props.embeddable.getInput();
|
||||
if (disabledActions) {
|
||||
const removeDisabledActions = removeById(disabledActions);
|
||||
regularActions = regularActions.filter(removeDisabledActions);
|
||||
}
|
||||
|
||||
private getUniversalActions = (): PanelUniversalActions => {
|
||||
const createGetUserData = (overlays: OverlayStart) =>
|
||||
async function getUserData(context: { embeddable: IEmbeddable }) {
|
||||
return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => {
|
||||
|
@ -299,27 +300,41 @@ export class EmbeddablePanel extends React.Component<Props, State> {
|
|||
});
|
||||
};
|
||||
|
||||
// These actions are exposed on the context menu for every embeddable, they bypass the trigger
|
||||
// Universal actions are exposed on the context menu for every embeddable, they bypass the trigger
|
||||
// registry.
|
||||
const extraActions: Array<Action<EmbeddableContext>> = [
|
||||
new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
|
||||
new AddPanelAction(
|
||||
return {
|
||||
customizePanelTitle: new CustomizePanelTitleAction(createGetUserData(this.props.overlays)),
|
||||
addPanel: new AddPanelAction(
|
||||
this.props.getEmbeddableFactory,
|
||||
this.props.getAllEmbeddableFactories,
|
||||
this.props.overlays,
|
||||
this.props.notifications,
|
||||
this.props.SavedObjectFinder
|
||||
),
|
||||
new InspectPanelAction(this.props.inspector),
|
||||
new RemovePanelAction(),
|
||||
new EditPanelAction(
|
||||
inspectPanel: new InspectPanelAction(this.props.inspector),
|
||||
removePanel: new RemovePanelAction(),
|
||||
editPanel: new EditPanelAction(
|
||||
this.props.getEmbeddableFactory,
|
||||
this.props.application,
|
||||
this.props.stateTransfer
|
||||
),
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField);
|
||||
private getActionContextMenuPanel = async () => {
|
||||
let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, {
|
||||
embeddable: this.props.embeddable,
|
||||
});
|
||||
|
||||
const { disabledActions } = this.props.embeddable.getInput();
|
||||
if (disabledActions) {
|
||||
const removeDisabledActions = removeById(disabledActions);
|
||||
regularActions = regularActions.filter(removeDisabledActions);
|
||||
}
|
||||
|
||||
const sortedActions = [...regularActions, ...Object.values(this.state.universalActions)].sort(
|
||||
sortByOrderField
|
||||
);
|
||||
|
||||
return await buildContextMenuForActions({
|
||||
actions: sortedActions.map((action) => ({
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
EuiToolTip,
|
||||
EuiScreenReaderOnly,
|
||||
EuiNotificationBadge,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
@ -32,6 +33,7 @@ import { PanelOptionsMenu } from './panel_options_menu';
|
|||
import { IEmbeddable } from '../../embeddables';
|
||||
import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers';
|
||||
import { uiToReactComponent } from '../../../../../kibana_react/public';
|
||||
import { CustomizePanelTitleAction } from '.';
|
||||
|
||||
export interface PanelHeaderProps {
|
||||
title?: string;
|
||||
|
@ -44,6 +46,7 @@ export interface PanelHeaderProps {
|
|||
embeddable: IEmbeddable;
|
||||
headerId?: string;
|
||||
showPlaceholderTitle?: boolean;
|
||||
customizeTitle: CustomizePanelTitleAction;
|
||||
}
|
||||
|
||||
function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmbeddable) {
|
||||
|
@ -129,6 +132,7 @@ export function PanelHeader({
|
|||
notifications,
|
||||
embeddable,
|
||||
headerId,
|
||||
customizeTitle,
|
||||
}: PanelHeaderProps) {
|
||||
const description = getViewDescription(embeddable);
|
||||
const showTitle = !hidePanelTitle && (!isViewMode || title);
|
||||
|
@ -172,11 +176,35 @@ export function PanelHeader({
|
|||
}
|
||||
|
||||
const renderTitle = () => {
|
||||
const titleComponent = showTitle ? (
|
||||
<span className={title ? 'embPanel__titleText' : 'embPanel__placeholderTitleText'}>
|
||||
{title || placeholderTitle}
|
||||
</span>
|
||||
) : undefined;
|
||||
let titleComponent;
|
||||
if (showTitle) {
|
||||
titleComponent = isViewMode ? (
|
||||
<span
|
||||
className={classNames('embPanel__titleText', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
embPanel__placeholderTitleText: !title,
|
||||
})}
|
||||
>
|
||||
{title || placeholderTitle}
|
||||
</span>
|
||||
) : (
|
||||
<EuiLink
|
||||
color="text"
|
||||
data-test-subj={'embeddablePanelTitleLink'}
|
||||
className={classNames('embPanel__titleText', {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
embPanel__placeholderTitleText: !title,
|
||||
})}
|
||||
aria-label={i18n.translate('embeddableApi.panel.editTitleAriaLabel', {
|
||||
defaultMessage: 'Click to edit title: {title}',
|
||||
values: { title: title || placeholderTitle },
|
||||
})}
|
||||
onClick={() => customizeTitle.execute({ embeddable })}
|
||||
>
|
||||
{title || placeholderTitle}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
return description ? (
|
||||
<EuiToolTip
|
||||
content={description}
|
||||
|
|
|
@ -71,6 +71,7 @@ import { SearchResponse } from 'elasticsearch';
|
|||
import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common';
|
||||
import { ShallowPromise } from '@kbn/utility-types';
|
||||
import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public';
|
||||
import { Start as Start_2 } from 'src/plugins/inspector/public';
|
||||
import { ToastInputFields as ToastInputFields_2 } from 'src/core/public/notifications';
|
||||
import { ToastsSetup as ToastsSetup_2 } from 'kibana/public';
|
||||
import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport';
|
||||
|
|
Loading…
Reference in a new issue