[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:
Devon Thomson 2020-10-22 16:16:18 -04:00 committed by GitHub
parent 096aedfae1
commit 06ea880712
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 154 additions and 28 deletions

View file

@ -64,12 +64,12 @@
.embPanel__titleText {
@include euiTextTruncate;
font-weight: $euiFontWeightBold;
}
.embPanel__placeholderTitleText {
@include euiTextTruncate;
font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
font-weight: $euiFontWeightRegular;
}
}

View file

@ -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();

View file

@ -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) => ({

View file

@ -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}

View file

@ -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';