Skip to content

Commit

Permalink
[Time To Visualize] Edit Panel Title On Click (#81076)
Browse files Browse the repository at this point in the history
* Made embeddable panel title click launch the customize panel action

Co-authored-by: Ryan Keairns <[email protected]>
  • Loading branch information
ThomThomson and ryankeairns authored Oct 22, 2020
1 parent 096aedf commit 06ea880
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@

.embPanel__titleText {
@include euiTextTruncate;
font-weight: $euiFontWeightBold;
}

.embPanel__placeholderTitleText {
@include euiTextTruncate;
font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
font-weight: $euiFontWeightRegular;
}
}

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

Expand Down
55 changes: 35 additions & 20 deletions src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ interface Props {

interface State {
panels: EuiContextMenuPanelDescriptor[];
universalActions: PanelUniversalActions;
focusedPanelIndex?: string;
viewMode: ViewMode;
hidePanelTitle: boolean;
Expand All @@ -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;
Expand All @@ -102,6 +111,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
Boolean(embeddable.getInput()?.hidePanelTitles);

this.state = {
universalActions: this.getUniversalActions(),
panels: [],
viewMode,
hidePanelTitle,
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
EuiToolTip,
EuiScreenReaderOnly,
EuiNotificationBadge,
EuiLink,
} from '@elastic/eui';
import classNames from 'classnames';
import React from 'react';
Expand All @@ -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;
Expand All @@ -44,6 +46,7 @@ export interface PanelHeaderProps {
embeddable: IEmbeddable;
headerId?: string;
showPlaceholderTitle?: boolean;
customizeTitle: CustomizePanelTitleAction;
}

function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmbeddable) {
Expand Down Expand Up @@ -129,6 +132,7 @@ export function PanelHeader({
notifications,
embeddable,
headerId,
customizeTitle,
}: PanelHeaderProps) {
const description = getViewDescription(embeddable);
const showTitle = !hidePanelTitle && (!isViewMode || title);
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/embeddable/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 06ea880

Please sign in to comment.