[Time to Visualize] Panel Title Fixes (#78365) (#78847)

* [Dashboard][Embeddable] Add placeholder title to embeddable panel, stored 'show panel title' prop in embeddable input.
Co-authored-by: Maja Grubic <maja.grubic@elastic.co>
This commit is contained in:
Devon Thomson 2020-09-29 17:14:25 -04:00 committed by GitHub
parent bcbc4ae96f
commit 2bb44a6d05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 99 deletions

View file

@ -89,6 +89,7 @@ export class BookEmbeddable
} else {
this.updateOutput({
attributes: this.attributes,
defaultTitle: this.attributes.title,
hasMatch: getHasMatch(this.input.search, this.attributes),
});
}
@ -125,6 +126,7 @@ export class BookEmbeddable
this.updateOutput({
attributes: this.attributes,
defaultTitle: this.attributes.title,
hasMatch: getHasMatch(this.input.search, this.attributes),
});
}

View file

@ -60,6 +60,12 @@
.embPanel__titleText {
@include euiTextTruncate;
}
.embPanel__placeholderTitleText {
@include euiTextTruncate;
font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
}
}
.embPanel__dragger:not(.embPanel__title) {
@ -159,4 +165,4 @@
pointer-events: none;
filter: grayscale(100%);
filter: gray;
}
}

View file

@ -32,7 +32,12 @@ import {
EmbeddableContext,
contextMenuTrigger,
} from '../triggers';
import { IEmbeddable, EmbeddableOutput, EmbeddableError } from '../embeddables/i_embeddable';
import {
IEmbeddable,
EmbeddableOutput,
EmbeddableError,
EmbeddableInput,
} from '../embeddables/i_embeddable';
import { ViewMode } from '../types';
import { RemovePanelAction } from './panel_header/panel_actions';
@ -55,7 +60,7 @@ const removeById = (disabledActions: string[]) => ({ id }: { id: string }) =>
disabledActions.indexOf(id) === -1;
interface Props {
embeddable: IEmbeddable<any, any>;
embeddable: IEmbeddable<EmbeddableInput, EmbeddableOutput>;
getActions: UiActionsService['getTriggerCompatibleActions'];
getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'];
getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'];
@ -72,7 +77,7 @@ interface State {
panels: EuiContextMenuPanelDescriptor[];
focusedPanelIndex?: string;
viewMode: ViewMode;
hidePanelTitles: boolean;
hidePanelTitle: boolean;
closeContextMenu: boolean;
badges: Array<Action<EmbeddableContext>>;
notifications: Array<Action<EmbeddableContext>>;
@ -90,17 +95,15 @@ export class EmbeddablePanel extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const { embeddable } = this.props;
const viewMode = embeddable.getInput().viewMode
? embeddable.getInput().viewMode
: ViewMode.EDIT;
const hidePanelTitles = embeddable.parent
? Boolean(embeddable.parent.getInput().hidePanelTitles)
: false;
const viewMode = embeddable.getInput().viewMode ?? ViewMode.EDIT;
const hidePanelTitle =
Boolean(embeddable.parent?.getInput()?.hidePanelTitles) ||
Boolean(embeddable.getInput()?.hidePanelTitles);
this.state = {
panels: [],
viewMode,
hidePanelTitles,
hidePanelTitle,
closeContextMenu: false,
badges: [],
notifications: [],
@ -150,9 +153,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
embeddable.getInput$().subscribe(async () => {
if (this.mounted) {
this.setState({
viewMode: embeddable.getInput().viewMode
? embeddable.getInput().viewMode
: ViewMode.EDIT,
viewMode: embeddable.getInput().viewMode ?? ViewMode.EDIT,
});
this.refreshBadges();
@ -165,7 +166,9 @@ export class EmbeddablePanel extends React.Component<Props, State> {
this.parentSubscription = parent.getInput$().subscribe(async () => {
if (this.mounted && parent) {
this.setState({
hidePanelTitles: Boolean(parent.getInput().hidePanelTitles),
hidePanelTitle:
Boolean(embeddable.parent?.getInput()?.hidePanelTitles) ||
Boolean(embeddable.getInput()?.hidePanelTitles),
});
this.refreshBadges();
@ -219,7 +222,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
{!this.props.hideHeader && (
<PanelHeader
getActionContextMenuPanel={this.getActionContextMenuPanel}
hidePanelTitles={this.state.hidePanelTitles}
hidePanelTitle={this.state.hidePanelTitle}
isViewMode={viewOnlyMode}
closeContextMenu={this.state.closeContextMenu}
title={title}
@ -272,15 +275,16 @@ export class EmbeddablePanel extends React.Component<Props, State> {
const createGetUserData = (overlays: OverlayStart) =>
async function getUserData(context: { embeddable: IEmbeddable }) {
return new Promise<{ title: string | undefined }>((resolve) => {
return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => {
const session = overlays.openModal(
toMountPoint(
<CustomizePanelModal
embeddable={context.embeddable}
updateTitle={(title) => {
updateTitle={(title, hideTitle) => {
session.close();
resolve({ title });
resolve({ title, hideTitle });
}}
cancel={() => session.close()}
/>
),
{

View file

@ -24,7 +24,9 @@ import { IEmbeddable } from '../../../../embeddables';
export const ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
type GetUserData = (context: ActionContext) => Promise<{ title: string | undefined }>;
type GetUserData = (
context: ActionContext
) => Promise<{ title: string | undefined; hideTitle?: boolean }>;
interface ActionContext {
embeddable: IEmbeddable;
@ -52,7 +54,8 @@ export class CustomizePanelTitleAction implements Action<ActionContext> {
}
public async execute({ embeddable }: ActionContext) {
const customTitle = await this.getDataFromUser({ embeddable });
embeddable.updateInput(customTitle);
const data = await this.getDataFromUser({ embeddable });
const { title, hideTitle } = data;
embeddable.updateInput({ title, hidePanelTitles: hideTitle });
}
}

View file

@ -36,31 +36,28 @@ import { IEmbeddable } from '../../../../';
interface CustomizePanelProps {
embeddable: IEmbeddable;
updateTitle: (newTitle: string | undefined) => void;
updateTitle: (newTitle: string | undefined, hideTitle: boolean | undefined) => void;
cancel: () => void;
}
interface State {
title: string | undefined;
hideTitle: boolean;
hideTitle: boolean | undefined;
}
export class CustomizePanelModal extends Component<CustomizePanelProps, State> {
constructor(props: CustomizePanelProps) {
super(props);
this.state = {
hideTitle: props.embeddable.getOutput().title === '',
title: props.embeddable.getInput().title,
hideTitle: props.embeddable.getInput().hidePanelTitles,
title: props.embeddable.getInput().title ?? this.props.embeddable.getOutput().defaultTitle,
};
}
private updateTitle = (title: string | undefined) => {
// An empty string will mean "use the default value", which is represented by setting
// title to undefined (where as an empty string is actually used to indicate "hide title").
this.setState({ title: title === '' ? undefined : title });
};
private reset = () => {
this.setState({ title: undefined });
this.setState({
title: this.props.embeddable.getOutput().defaultTitle,
});
};
private onHideTitleToggle = () => {
@ -70,12 +67,11 @@ export class CustomizePanelModal extends Component<CustomizePanelProps, State> {
};
private save = () => {
if (this.state.hideTitle) {
this.props.updateTitle('');
} else {
const newTitle = this.state.title === '' ? undefined : this.state.title;
this.props.updateTitle(newTitle);
}
const newTitle =
this.state.title === this.props.embeddable.getOutput().defaultTitle
? undefined
: this.state.title;
this.props.updateTitle(newTitle, this.state.hideTitle);
};
public render() {
@ -116,9 +112,8 @@ export class CustomizePanelModal extends Component<CustomizePanelProps, State> {
name="min"
type="text"
disabled={this.state.hideTitle}
placeholder={this.props.embeddable.getOutput().defaultTitle}
value={this.state.title || ''}
onChange={(e) => this.updateTitle(e.target.value)}
onChange={(e) => this.setState({ title: e.target.value })}
aria-label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel',
{
@ -141,9 +136,7 @@ export class CustomizePanelModal extends Component<CustomizePanelProps, State> {
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
onClick={() => this.props.updateTitle(this.props.embeddable.getOutput().title)}
>
<EuiButtonEmpty onClick={() => this.props.cancel()}>
<FormattedMessage
id="embeddableApi.customizePanel.modal.cancel"
defaultMessage="Cancel"

View file

@ -36,13 +36,14 @@ import { uiToReactComponent } from '../../../../../kibana_react/public';
export interface PanelHeaderProps {
title?: string;
isViewMode: boolean;
hidePanelTitles: boolean;
hidePanelTitle: boolean;
getActionContextMenuPanel: () => Promise<EuiContextMenuPanelDescriptor[]>;
closeContextMenu: boolean;
badges: Array<Action<EmbeddableContext>>;
notifications: Array<Action<EmbeddableContext>>;
embeddable: IEmbeddable;
headerId?: string;
showPlaceholderTitle?: boolean;
}
function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmbeddable) {
@ -126,7 +127,7 @@ function getViewDescription(embeddable: IEmbeddable | VisualizeEmbeddable) {
export function PanelHeader({
title,
isViewMode,
hidePanelTitles,
hidePanelTitle,
getActionContextMenuPanel,
closeContextMenu,
badges,
@ -135,12 +136,30 @@ export function PanelHeader({
headerId,
}: PanelHeaderProps) {
const viewDescription = getViewDescription(embeddable);
const showTitle = !isViewMode || (title && !hidePanelTitles) || viewDescription !== '';
const showPanelBar = badges.length > 0 || showTitle;
const showTitle = !hidePanelTitle && (!isViewMode || title || viewDescription !== '');
const showPanelBar = badges.length > 0 || notifications.length > 0 || showTitle;
const classes = classNames('embPanel__header', {
// eslint-disable-next-line @typescript-eslint/naming-convention
'embPanel__header--floater': !showPanelBar,
});
const placeholderTitle = i18n.translate('embeddableApi.panel.placeholderTitle', {
defaultMessage: '[No Title]',
});
const getAriaLabel = () => {
return (
<span id={headerId}>
{showPanelBar && title
? i18n.translate('embeddableApi.panel.enhancedDashboardPanelAriaLabel', {
defaultMessage: 'Dashboard panel: {title}',
values: { title: title || placeholderTitle },
})
: i18n.translate('embeddableApi.panel.dashboardPanelAriaLabel', {
defaultMessage: 'Dashboard panel',
})}
</span>
);
};
if (!showPanelBar) {
return (
@ -151,6 +170,7 @@ export function PanelHeader({
closeContextMenu={closeContextMenu}
title={title}
/>
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly>
</div>
);
}
@ -160,34 +180,20 @@ export function PanelHeader({
className={classes}
data-test-subj={`embeddablePanelHeading-${(title || '').replace(/\s/g, '')}`}
>
<h2
id={headerId}
data-test-subj="dashboardPanelTitle"
className="embPanel__title embPanel__dragger"
>
<h2 data-test-subj="dashboardPanelTitle" className="embPanel__title embPanel__dragger">
{showTitle ? (
<span className="embPanel__titleInner">
<span className="embPanel__titleText" aria-hidden="true">
{title}
<span
className={title ? 'embPanel__titleText' : 'embPanel__placeholderTitleText'}
aria-hidden="true"
>
{title || placeholderTitle}
</span>
<EuiScreenReaderOnly>
<span>
{i18n.translate('embeddableApi.panel.enhancedDashboardPanelAriaLabel', {
defaultMessage: 'Dashboard panel: {title}',
values: { title },
})}
</span>
</EuiScreenReaderOnly>
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly>
{renderTooltip(viewDescription)}
</span>
) : (
<EuiScreenReaderOnly>
<span>
{i18n.translate('embeddableApi.panel.dashboardPanelAriaLabel', {
defaultMessage: 'Dashboard panel',
})}
</span>
</EuiScreenReaderOnly>
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly>
)}
{renderBadges(badges, embeddable)}
</h2>

View file

@ -33,9 +33,9 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world
import { coreMock } from '../../../../core/public/mocks';
import { testPlugin } from './test_plugin';
import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal';
import { mount } from 'enzyme';
import { EmbeddableStart } from '../plugin';
import { createEmbeddablePanelMock } from '../mocks';
import { mountWithIntl } from '../../../../test_utils/public/enzyme_helpers';
let api: EmbeddableStart;
let container: Container;
@ -84,19 +84,20 @@ beforeEach(async () => {
}
});
test('Is initialized with the embeddables title', async () => {
const component = mount(<CustomizePanelModal embeddable={embeddable} updateTitle={() => {}} />);
test('Value is initialized with the embeddables title', async () => {
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={() => {}} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
expect(inputField.props().placeholder).toBe(embeddable.getOutput().title);
expect(inputField.props().placeholder).toBe(embeddable.getOutput().defaultTitle);
expect(inputField.props().value).toBe('');
expect(inputField.props().value).toBe(embeddable.getOutput().title);
expect(inputField.props().value).toBe(embeddable.getOutput().defaultTitle);
});
test('Calls updateTitle with a new title', async () => {
const updateTitle = jest.fn();
const component = mount(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} />
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
@ -105,15 +106,15 @@ test('Calls updateTitle with a new title', async () => {
findTestSubject(component, 'saveNewTitleButton').simulate('click');
expect(updateTitle).toBeCalledWith('new title');
expect(updateTitle).toBeCalledWith('new title', undefined);
});
test('Input value shows custom title if one given', async () => {
embeddable.updateInput({ title: 'new title' });
const updateTitle = jest.fn();
const component = mount(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} />
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
@ -122,12 +123,12 @@ test('Input value shows custom title if one given', async () => {
expect(inputField.props().value).toBe('new title');
});
test('Reset updates the input with the default title when the embeddable has no title override', async () => {
test('Reset updates the input value with the default title when the embeddable has a title override', async () => {
const updateTitle = jest.fn();
embeddable.updateInput({ title: 'my custom title' });
const component = mount(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} />
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
@ -135,13 +136,14 @@ test('Reset updates the input with the default title when the embeddable has no
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click');
expect(inputField.props().placeholder).toBe(embeddable.getOutput().defaultTitle);
const inputAfter = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
expect(inputAfter.props().value).toBe(embeddable.getOutput().defaultTitle);
});
test('Reset updates the input with the default title when the embeddable has a title override', async () => {
test('Reset updates the input with the default title when the embeddable has no title override', async () => {
const updateTitle = jest.fn();
const component = mount(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} />
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
@ -149,13 +151,14 @@ test('Reset updates the input with the default title when the embeddable has a t
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click');
expect(inputField.props().placeholder).toBe(embeddable.getOutput().defaultTitle);
await component.update();
expect(inputField.props().value).toBe(embeddable.getOutput().defaultTitle);
});
test('Reset calls updateTitle with undefined', async () => {
const updateTitle = jest.fn();
const component = mount(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} />
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
@ -165,19 +168,21 @@ test('Reset calls updateTitle with undefined', async () => {
findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click');
findTestSubject(component, 'saveNewTitleButton').simulate('click');
expect(updateTitle).toBeCalledWith(undefined);
expect(updateTitle).toBeCalledWith(undefined, undefined);
});
test('Can set title to an empty string', async () => {
const updateTitle = jest.fn();
const component = mount(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} />
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customizePanelHideTitle');
inputField.simulate('click');
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput');
const event = { target: { value: '' } };
inputField.simulate('change', event);
findTestSubject(component, 'saveNewTitleButton').simulate('click');
expect(inputField.props().value).toBeUndefined();
expect(updateTitle).toBeCalledWith('');
const inputFieldAfter = findTestSubject(component, 'customEmbeddablePanelTitleInput');
expect(inputFieldAfter.props().value).toBe('');
expect(updateTitle).toBeCalledWith('', undefined);
});

View file

@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.dashboard.checkHideTitle();
await retry.try(async () => {
const titles = await PageObjects.dashboard.getPanelTitles();
expect(titles[0]).to.eql('');
expect(titles[0]).to.eql(undefined);
});
});