[Dashboard Usability] Unified panel options pane (#148301)

This commit is contained in:
Nick Peihl 2023-02-02 16:30:31 -05:00 committed by GitHub
parent f90bf811f8
commit ace2c30c29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 1370 additions and 1891 deletions

View file

@ -139,9 +139,7 @@ Add a panel that you saved in *Visualize Library* to your workpad.
* *Edit Visualization* — Opens the visualization editor so that you can edit the panel.
* *Edit panel title* — Allows you to change the panel title.
* *Customize time range* — Allows you to change the time filter dedicated to the panel.
* *Edit panel settings* — Allows you to change the title, description, and time range for the panel.
* *Inspect* — Allows you to drill down into the panel data.

View file

@ -238,9 +238,9 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.

View file

@ -158,9 +158,9 @@ If you created the panel from the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
If you created the panel from the *Visualize Library*:
@ -236,9 +236,9 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
[float]
[[add-image]]
@ -293,7 +293,7 @@ To make changes to the panel, use the panel menu options.
+
To make changes without changing the original version, open the panel menu, then click *More > Unlink from library*.
* *Edit panel title* — Opens the *Customize panel* window to change the *Panel title*.
* *Edit panel settings* — Opens the *Panel settings* window to change the *title*, *description*, and *time range*.
* *More > Replace panel* — Opens the *Visualize Library* so you can select a new panel to replace the existing panel.
@ -341,9 +341,11 @@ For more information about {kib} and {es} filters, refer to <<kibana-concepts-an
To apply a panel-level time filter:
. Open the panel menu, then select *More > Customize time range*.
. Open the panel menu, then select *More > Edit panel settings*.
. Enter the time range you want to view, then click *Add to panel*.
. Toggle the switch labelled *Apply a custom time range*.
. Enter the time range you want to view, then click *Save*.
[float]
[[apply-design-options]]

View file

@ -380,9 +380,9 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
[float]
[[lens-faq]]

View file

@ -271,9 +271,9 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
[float]
[[timelion-tutorial-create-visualizations-with-mathematical-functions]]
@ -406,9 +406,9 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
[float]
[[timelion-tutorial-create-visualizations-withconditional-logic-and-tracking-trends]]
@ -594,8 +594,8 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
For more information about *Timelion* conditions, refer to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()].

View file

@ -184,9 +184,9 @@ To save the panel to the dashboard:
.. In the panel header, click *No Title*.
.. On the *Customize panel* window, select *Show panel title*.
.. On the *Panel settings* window, select *Show title*.
.. Enter the *Panel title*, then click *Save*.
.. Enter the *Title*, then click *Save*.
[float]
[[tsvb-faq]]
@ -304,4 +304,4 @@ For other types of month over month calculations, use <<timelion, *Timelion*>> o
Calculating the duration between the start and end of an event is unsupported in *TSVB* because *TSVB* requires correlation between different time periods.
*TSVB* requires that the duration is pre-calculated.
====
====

View file

@ -141,9 +141,9 @@ image::images/lens_lineChartMetricOverTimeBottomAxis_8.3.png[Bottom axis menu]
Since you removed the axis labels, add a panel title:
. Open the panel menu, then select *Edit panel title*.
. Open the panel menu, then select *Edit panel settings*.
. In the *Panel title* field, enter `Median of bytes`, then click *Save*.
. In the *Title* field, enter `Median of bytes`, then click *Save*.
+
[role="screenshot"]
image::images/lens_lineChartMetricOverTime_8.4.0.png[Line chart that displays metric data over time]
@ -245,9 +245,9 @@ image::images/lens_pieChartCompareSubsetOfDocs_7.16.png[Pie chart that compares
Add a panel title:
. Open the panel menu, then select *Edit panel title*.
. Open the panel menu, then select *Edit panel settings*.
. In the *Panel title* field, enter `Sum of bytes from large requests`, then click *Save*.
. In the *Title* field, enter `Sum of bytes from large requests`, then click *Save*.
[discrete]
[[histogram]]
@ -278,9 +278,9 @@ image::images/lens_barChartDistributionOfNumberField_7.16.png[Bar chart that dis
Add a panel title:
. Open the panel menu, then select *Edit panel title*.
. Open the panel menu, then select *Edit panel settings*.
. In the *Panel title* field, enter `Website traffic`, then click *Save*.
. In the *Title* field, enter `Website traffic`, then click *Save*.
[discrete]
[[treemap]]
@ -342,9 +342,9 @@ image::images/lens_treemapMultiLevelChart_7.16.png[Treemap visualization]
Add a panel title:
. Open the panel menu, then select *Edit panel title*.
. Open the panel menu, then select *Edit panel settings*.
. In the *Panel title* field, enter `Page views by location and referrer`, then click *Save*.
. In the *Title* field, enter `Page views by location and referrer`, then click *Save*.
[float]
[[arrange-the-lens-panels]]
@ -376,4 +376,4 @@ Now that you have a complete overview of your web server data, save the dashboar
. Select *Store time with dashboard*.
. Click *Save*.
. Click *Save*.

View file

@ -156,7 +156,10 @@ export const useDashboardMenuItems = ({
iconType: 'pencil',
testId: 'dashboardEditMode',
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
run: () => dispatch(setViewMode(ViewMode.EDIT)),
run: () => {
dashboardContainer.clearOverlays();
dispatch(setViewMode(ViewMode.EDIT));
},
} as TopNavMenuData,
quickSave: {

View file

@ -131,6 +131,7 @@ export class SavedSearchEmbeddable
initialInput,
{
defaultTitle: savedSearch.title,
defaultDescription: savedSearch.description,
editUrl,
editPath,
editApp: 'discover',
@ -595,10 +596,6 @@ export class SavedSearchEmbeddable
return this.inspectorAdapters;
}
public getDescription() {
return this.savedSearch.description;
}
/**
* @returns Local/panel-level array of filters for Saved Search embeddable
*/

View file

@ -24,6 +24,7 @@ export enum ViewMode {
export type EmbeddableInput = {
viewMode?: ViewMode;
title?: string;
description?: string;
/**
* Note this is not a saved object id. It is used to uniquely identify this
* Embeddable instance from others (e.g. inside a container). It's possible to

View file

@ -8,7 +8,7 @@
"githubTeam": "kibana-app-services"
},
"description": "Adds embeddables service to Kibana",
"requiredPlugins": ["inspector", "uiActions"],
"requiredPlugins": ["data", "inspector", "uiActions"],
"extraPublicDirs": ["common"],
"requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"]
}

View file

@ -19,8 +19,14 @@ import { EmbeddableInput, ViewMode } from '../../../common/types';
import { genericEmbeddableInputIsEqual, omitGenericEmbeddableInput } from './diff_embeddable_input';
function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) {
return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title;
if (input.hidePanelTitles) return '';
return input.title ?? output.defaultTitle;
}
function getPanelDescription(input: EmbeddableInput, output: EmbeddableOutput) {
if (input.hidePanelTitles) return '';
return input.description ?? output.defaultDescription;
}
export abstract class Embeddable<
TEmbeddableInput extends EmbeddableInput = EmbeddableInput,
TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput,
@ -61,6 +67,7 @@ export abstract class Embeddable<
this.output = {
title: getPanelTitle(input, output),
description: getPanelDescription(input, output),
...(this.reportsEmbeddableLoad()
? {}
: {
@ -184,7 +191,11 @@ export abstract class Embeddable<
}
public getTitle(): string {
return this.output.title || '';
return this.output.title ?? '';
}
public getDescription(): string {
return this.output.description ?? '';
}
/**
@ -283,6 +294,7 @@ export abstract class Embeddable<
this.inputSubject.next(newInput);
this.updateOutput({
title: getPanelTitle(this.input, this.output),
description: getPanelDescription(this.input, this.output),
} as Partial<TEmbeddableOutput>);
if (oldLastReloadRequestTime !== newInput.lastReloadRequestTime) {
this.reload();

View file

@ -26,7 +26,9 @@ export interface EmbeddableOutput {
editApp?: string;
editPath?: string;
defaultTitle?: string;
defaultDescription?: string;
title?: string;
description?: string;
editable?: boolean;
// Whether the embeddable can be edited inline by re-requesting the explicit input from the user
editableWithExplicitInput?: boolean;
@ -166,6 +168,11 @@ export interface IEmbeddable<
*/
getTitle(): string | undefined;
/**
* Returns the description of this embeddable.
*/
getDescription(): string | undefined;
/**
* Returns the top most parent embeddable, or itself if this embeddable
* is not within a parent.

View file

@ -525,7 +525,7 @@ test('Runs customize panel action on title click when in edit mode', async () =>
...s,
universalActions: {
...s.universalActions,
customizePanelTitle: { execute: titleExecute, isCompatible: jest.fn() },
customizePanel: { execute: titleExecute, isCompatible: jest.fn() },
},
}));

View file

@ -17,8 +17,7 @@ import classNames from 'classnames';
import React, { ReactNode } from 'react';
import { Subscription } from 'rxjs';
import deepEqual from 'fast-deep-equal';
import { CoreStart, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { CoreStart, ThemeServiceStart } from '@kbn/core/public';
import { isPromise } from '@kbn/std';
import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { MaybePromise } from '@kbn/utility-types';
@ -43,13 +42,12 @@ import { ViewMode } from '../types';
import { EmbeddablePanelError } from './embeddable_panel_error';
import { RemovePanelAction } from './panel_header/panel_actions';
import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action';
import { CustomizePanelTitleAction } from './panel_header/panel_actions/customize_title/customize_panel_action';
import { CustomizePanelAction } from './panel_header/panel_actions/customize_panel/customize_panel_action';
import { PanelHeader } from './panel_header/panel_header';
import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action';
import { EditPanelAction } from '../actions';
import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal';
import { EmbeddableStart } from '../../plugin';
import { EmbeddableStateTransfer, isSelfStyledEmbeddable } from '..';
import { EmbeddableStateTransfer, isSelfStyledEmbeddable, CommonlyUsedRange } from '..';
const sortByOrderField = (
{ order: orderA }: { order?: number },
@ -85,6 +83,8 @@ interface Props {
getActions?: UiActionsService['getTriggerCompatibleActions'];
getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'];
getAllEmbeddableFactories?: EmbeddableStart['getEmbeddableFactories'];
dateFormat?: string;
commonlyUsedRanges?: CommonlyUsedRange[];
overlays?: CoreStart['overlays'];
notifications?: CoreStart['notifications'];
application?: CoreStart['application'];
@ -121,7 +121,7 @@ interface InspectorPanelAction {
}
interface BasePanelActions {
customizePanelTitle: CustomizePanelTitleAction;
customizePanel: CustomizePanelAction;
addPanel: AddPanelAction;
inspectPanel: InspectPanelAction;
removePanel: RemovePanelAction;
@ -281,6 +281,7 @@ export class EmbeddablePanel extends React.Component<Props, State> {
if (this.state.error) contentAttrs['data-error'] = true;
const title = this.props.embeddable.getTitle();
const description = this.props.embeddable.getDescription();
const headerId = this.generateId();
const selfStyledOptions = isSelfStyledEmbeddable(this.props.embeddable)
@ -302,13 +303,14 @@ export class EmbeddablePanel extends React.Component<Props, State> {
getActionContextMenuPanel={this.getActionContextMenuPanel}
hidePanelTitle={this.state.hidePanelTitle || !!selfStyledOptions?.hideTitle}
isViewMode={viewOnlyMode}
customizeTitle={
'customizePanelTitle' in this.state.universalActions
? this.state.universalActions.customizePanelTitle
customizePanel={
'customizePanel' in this.state.universalActions
? this.state.universalActions.customizePanel
: undefined
}
closeContextMenu={this.state.closeContextMenu}
title={title}
description={description}
index={this.props.index}
badges={this.state.badges}
notifications={this.state.notifications}
@ -397,34 +399,16 @@ export class EmbeddablePanel extends React.Component<Props, State> {
) {
return actions;
}
const createGetUserData = (overlays: OverlayStart, theme: ThemeServiceStart) =>
async function getUserData(context: { embeddable: IEmbeddable }) {
return new Promise<{ title: string | undefined; hideTitle?: boolean }>((resolve) => {
const session = overlays.openModal(
toMountPoint(
<CustomizePanelModal
embeddable={context.embeddable}
updateTitle={(title, hideTitle) => {
session.close();
resolve({ title, hideTitle });
}}
cancel={() => session.close()}
/>,
{ theme$: theme.theme$ }
),
{
'data-test-subj': 'customizePanel',
}
);
});
};
// Universal actions are exposed on the context menu for every embeddable, they bypass the trigger
// registry.
return {
...actions,
customizePanelTitle: new CustomizePanelTitleAction(
createGetUserData(this.props.overlays, this.props.theme)
customizePanel: new CustomizePanelAction(
this.props.overlays,
this.props.theme,
this.props.commonlyUsedRanges,
this.props.dateFormat
),
addPanel: new AddPanelAction(
this.props.getEmbeddableFactory,

View file

@ -7,9 +7,12 @@
*/
import { canInheritTimeRange } from './can_inherit_time_range';
import { HelloWorldContainer } from '@kbn/embeddable-plugin/public/lib/test_samples';
import { HelloWorldEmbeddable } from '@kbn/embeddable-plugin/public/tests/fixtures';
import { TimeRangeEmbeddable, TimeRangeContainer } from './test_helpers';
import {
HelloWorldContainer,
TimeRangeContainer,
TimeRangeEmbeddable,
} from '../../../../test_samples';
import { HelloWorldEmbeddable } from '../../../../../tests/fixtures';
test('canInheritTimeRange returns false if embeddable is inside container without a time range', () => {
const embeddable = new TimeRangeEmbeddable(

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { Embeddable, IContainer, ContainerInput } from '@kbn/embeddable-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { TimeRangeInput } from './custom_time_range_action';
import { Embeddable, IContainer, ContainerInput } from '../../../../..';
import { TimeRangeInput } from './customize_panel_action';
interface ContainerTimeRangeInput extends ContainerInput<TimeRangeInput> {
timeRange: TimeRange;

View file

@ -0,0 +1,115 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import {
TimeRangeEmbeddable,
TimeRangeContainer,
TIME_RANGE_EMBEDDABLE,
} from '../../../../test_samples/embeddables';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
test(`badge is not compatible with embeddable that inherits from parent`, async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const compatible = await new CustomTimeRangeBadge(
overlayServiceMock.createStartContract(),
themeServiceMock.createStartContract(),
[],
'MM YYYY'
).isCompatible({
embeddable: child,
});
expect(compatible).toBe(false);
});
test(`badge is compatible with embeddable that has custom time range`, async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { to: '123', from: '456' },
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const compatible = await new CustomTimeRangeBadge(
overlayServiceMock.createStartContract(),
themeServiceMock.createStartContract(),
[],
'MM YYYY'
).isCompatible({
embeddable: child,
});
expect(compatible).toBe(true);
});
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const badge = await new CustomTimeRangeBadge(
overlayServiceMock.createStartContract(),
themeServiceMock.createStartContract(),
[],
'MM YYYY'
);
async function check() {
await badge.execute({ embeddable: child });
}
await expect(check()).rejects.toThrow(Error);
});

View file

@ -0,0 +1,48 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderToString } from 'react-dom/server';
import { PrettyDuration } from '@elastic/eui';
import { Action } from '@kbn/ui-actions-plugin/public';
import { doesInheritTimeRange } from './does_inherit_time_range';
import { Embeddable } from '../../../../..';
import { TimeRangeInput, hasTimeRange, CustomizePanelAction } from './customize_panel_action';
export const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE';
export interface TimeBadgeActionContext {
embeddable: Embeddable<TimeRangeInput>;
}
export class CustomTimeRangeBadge
extends CustomizePanelAction
implements Action<TimeBadgeActionContext>
{
public readonly type = CUSTOM_TIME_RANGE_BADGE;
public readonly id = CUSTOM_TIME_RANGE_BADGE;
public order = 7;
public getDisplayName({ embeddable }: TimeBadgeActionContext) {
return renderToString(
<PrettyDuration
timeFrom={embeddable.getInput().timeRange.from}
timeTo={embeddable.getInput().timeRange.to}
dateFormat={this.dateFormat ?? 'Browser'}
/>
);
}
public getIconType() {
return 'calendar';
}
public async isCompatible({ embeddable }: TimeBadgeActionContext) {
return Boolean(embeddable && hasTimeRange(embeddable) && !doesInheritTimeRange(embeddable));
}
}

View file

@ -0,0 +1,65 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { Container, isErrorEmbeddable } from '../../../..';
import { CustomizePanelAction } from './customize_panel_action';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container';
import { embeddablePluginMock } from '../../../../../mocks';
let container: Container;
let embeddable: ContactCardEmbeddable;
const overlays = overlayServiceMock.createStartContract();
const theme = themeServiceMock.createStartContract();
function createHelloWorldContainer(input = { id: '123', panels: {} }) {
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory((() => {}) as any, {} as any)
);
const getEmbeddableFactory = doStart().getEmbeddableFactory;
return new HelloWorldContainer(input, { getEmbeddableFactory } as any);
}
beforeAll(async () => {
container = createHelloWorldContainer();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
id: 'robert',
firstName: 'Robert',
lastName: 'Baratheon',
});
if (isErrorEmbeddable(contactCardEmbeddable)) {
throw new Error('Error creating new hello world embeddable');
} else {
embeddable = contactCardEmbeddable;
}
});
test('execute should open flyout', async () => {
const customizePanelAction = new CustomizePanelAction(overlays, theme);
const spy = jest.spyOn(overlays, 'openFlyout');
await customizePanelAction.execute({ embeddable });
expect(spy).toHaveBeenCalled();
});

View file

@ -0,0 +1,136 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { OverlayRef, OverlayStart, ThemeServiceStart } from '@kbn/core/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { ViewMode } from '../../../../types';
import {
IEmbeddable,
Embeddable,
EmbeddableInput,
CommonlyUsedRange,
EmbeddableOutput,
} from '../../../..';
import { CustomizePanelEditor } from './customize_panel_editor';
export const ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
const VISUALIZE_EMBEDDABLE_TYPE = 'visualization';
type VisualizeEmbeddable = IEmbeddable<{ id: string }, EmbeddableOutput & { visTypeName: string }>;
interface TracksOverlays {
openOverlay: (ref: OverlayRef) => void;
clearOverlays: () => void;
}
function tracksOverlays(root: unknown): root is TracksOverlays {
return Boolean((root as TracksOverlays).openOverlay && (root as TracksOverlays).clearOverlays);
}
function isVisualizeEmbeddable(
embeddable: IEmbeddable | VisualizeEmbeddable
): embeddable is VisualizeEmbeddable {
return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE;
}
export interface TimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
export function hasTimeRange(
embeddable: IEmbeddable | Embeddable<TimeRangeInput>
): embeddable is Embeddable<TimeRangeInput> {
return (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange !== undefined;
}
export interface CustomizePanelActionContext {
embeddable: IEmbeddable | Embeddable<TimeRangeInput>;
}
export class CustomizePanelAction implements Action<CustomizePanelActionContext> {
public type = ACTION_CUSTOMIZE_PANEL;
public id = ACTION_CUSTOMIZE_PANEL;
public order = 40;
constructor(
protected readonly overlays: OverlayStart,
protected readonly theme: ThemeServiceStart,
protected readonly commonlyUsedRanges?: CommonlyUsedRange[],
protected readonly dateFormat?: string
) {}
protected isTimeRangeCompatible({ embeddable }: CustomizePanelActionContext): boolean {
const isInputControl =
isVisualizeEmbeddable(embeddable) &&
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'input_control_vis';
const isMarkdown =
isVisualizeEmbeddable(embeddable) &&
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown';
const isImage = embeddable.type === 'image';
return Boolean(
embeddable && hasTimeRange(embeddable) && !isInputControl && !isMarkdown && !isImage
);
}
public getDisplayName({ embeddable }: CustomizePanelActionContext): string {
return i18n.translate('embeddableApi.customizePanel.action.displayName', {
defaultMessage: 'Edit panel settings',
});
}
public getIconType() {
return 'pencil';
}
public async isCompatible({ embeddable }: CustomizePanelActionContext) {
// It should be possible to customize just the time range in View mode
return (
embeddable.getInput().viewMode === ViewMode.EDIT || this.isTimeRangeCompatible({ embeddable })
);
}
public async execute({ embeddable }: CustomizePanelActionContext) {
const isCompatible = await this.isCompatible({ embeddable });
if (!isCompatible) {
throw new IncompatibleActionError();
}
// send the overlay ref to the root embeddable if it is capable of tracking overlays
const rootEmbeddable = embeddable.getRoot();
const overlayTracker = tracksOverlays(rootEmbeddable) ? rootEmbeddable : undefined;
const handle = this.overlays.openFlyout(
toMountPoint(
<CustomizePanelEditor
embeddable={embeddable}
timeRangeCompatible={this.isTimeRangeCompatible({ embeddable })}
dateFormat={this.dateFormat}
commonlyUsedRanges={this.commonlyUsedRanges}
onClose={() => {
if (overlayTracker) overlayTracker.clearOverlays();
handle.close();
}}
/>,
{ theme$: this.theme.theme$ }
),
{
size: 's',
'data-test-subj': 'customizePanel',
}
);
overlayTracker?.openOverlay(handle);
}
}

View file

@ -0,0 +1,295 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import {
EuiFormRow,
EuiFieldText,
EuiSwitch,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiForm,
EuiTextArea,
EuiFlyoutFooter,
EuiButtonEmpty,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSuperDatePicker,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { TimeRange } from '@kbn/es-query';
import { TimeRangeInput } from './customize_panel_action';
import { doesInheritTimeRange } from './does_inherit_time_range';
import { IEmbeddable, Embeddable, CommonlyUsedRange, ViewMode } from '../../../..';
import { canInheritTimeRange } from './can_inherit_time_range';
type PanelSettings = {
title?: string;
hidePanelTitles?: boolean;
description?: string;
timeRange?: TimeRange;
};
interface CustomizePanelProps {
embeddable: IEmbeddable;
timeRangeCompatible: boolean;
dateFormat?: string;
commonlyUsedRanges?: CommonlyUsedRange[];
onClose: () => void;
}
export const CustomizePanelEditor = (props: CustomizePanelProps) => {
const { onClose, embeddable, dateFormat, timeRangeCompatible } = props;
const editMode = embeddable.getInput().viewMode === ViewMode.EDIT;
const [hideTitle, setHideTitle] = useState(embeddable.getInput().hidePanelTitles);
const [panelDescription, setPanelDescription] = useState(
embeddable.getInput().description ?? embeddable.getOutput().defaultDescription
);
const [panelTitle, setPanelTitle] = useState(
embeddable.getInput().title ?? embeddable.getOutput().defaultTitle
);
const [inheritTimeRange, setInheritTimeRange] = useState(
timeRangeCompatible ? doesInheritTimeRange(embeddable as Embeddable<TimeRangeInput>) : false
);
const [panelTimeRange, setPanelTimeRange] = useState(
timeRangeCompatible
? (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange
: undefined
);
const commonlyUsedRangesForDatePicker = props.commonlyUsedRanges
? props.commonlyUsedRanges.map(
({ from, to, display }: { from: string; to: string; display: string }) => {
return {
start: from,
end: to,
label: display,
};
}
)
: undefined;
const save = () => {
const newPanelSettings: PanelSettings = {
hidePanelTitles: hideTitle,
title: panelTitle === embeddable.getOutput().defaultTitle ? undefined : panelTitle,
description:
panelDescription === embeddable.getOutput().defaultDescription
? undefined
: panelDescription,
};
if (Boolean(timeRangeCompatible))
newPanelSettings.timeRange = !inheritTimeRange ? panelTimeRange : undefined;
embeddable.updateInput(newPanelSettings);
onClose();
};
const renderCustomTitleComponent = () => {
if (!editMode) return null;
return (
<>
<EuiFormRow>
<EuiSwitch
checked={!hideTitle}
data-test-subj="customEmbeddablePanelHideTitleSwitch"
disabled={!editMode}
id="hideTitle"
label={
<FormattedMessage
defaultMessage="Show title"
id="embeddableApi.customizePanel.flyout.optionsMenuForm.showTitle"
/>
}
onChange={(e) => setHideTitle(!e.target.checked)}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="embeddableApi.customizePanel.flyout.optionsMenuForm.panelTitleFormRowLabel"
defaultMessage="Title"
/>
}
labelAppend={
<EuiButtonEmpty
size="xs"
data-test-subj="resetCustomEmbeddablePanelTitleButton"
onClick={() => setPanelTitle(embeddable.getOutput().defaultTitle)}
disabled={hideTitle || !editMode}
aria-label={i18n.translate(
'embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonAriaLabel',
{
defaultMessage: 'Reset title',
}
)}
>
<FormattedMessage
id="embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonLabel"
defaultMessage="Reset"
/>
</EuiButtonEmpty>
}
>
<EuiFieldText
id="panelTitleInput"
className="panelTitleInputText"
data-test-subj="customEmbeddablePanelTitleInput"
name="title"
type="text"
disabled={hideTitle || !editMode}
value={panelTitle ?? ''}
onChange={(e) => setPanelTitle(e.target.value)}
aria-label={i18n.translate(
'embeddableApi.customizePanel.flyout.optionsMenuForm.panelTitleInputAriaLabel',
{
defaultMessage: 'Enter a custom title for your panel',
}
)}
/>
</EuiFormRow>
<EuiFormRow
label={
<FormattedMessage
id="embeddableApi.customizePanel.flyout.optionsMenuForm.panelDescriptionFormRowLabel"
defaultMessage="Description"
/>
}
labelAppend={
<EuiButtonEmpty
size="xs"
data-test-subj="resetCustomEmbeddablePanelDescriptionButton"
onClick={() => {
setPanelDescription(embeddable.getOutput().defaultDescription);
}}
disabled={hideTitle || !editMode}
aria-label={i18n.translate(
'embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomDescriptionButtonAriaLabel',
{
defaultMessage: 'Reset description',
}
)}
>
<FormattedMessage
id="embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDescriptionButtonLabel"
defaultMessage="Reset"
/>
</EuiButtonEmpty>
}
>
<EuiTextArea
id="panelDescriptionInput"
className="panelDescriptionInputText"
data-test-subj="customEmbeddablePanelDescriptionInput"
disabled={hideTitle || !editMode}
name="description"
value={panelDescription ?? ''}
onChange={(e) => setPanelDescription(e.target.value)}
aria-label={i18n.translate(
'embeddableApi.customizePanel.flyout.optionsMenuForm.panelDescriptionAriaLabel',
{
defaultMessage: 'Enter a custom description for your panel',
}
)}
/>
</EuiFormRow>
</>
);
};
const renderCustomTimeRangeComponent = () => {
if (!timeRangeCompatible) return null;
return (
<>
{canInheritTimeRange(embeddable as Embeddable<TimeRangeInput>) ? (
<EuiFormRow>
<EuiSwitch
checked={!inheritTimeRange}
data-test-subj="customizePanelShowCustomTimeRange"
id="showCustomTimeRange"
label={
<FormattedMessage
defaultMessage="Apply custom time range"
id="embeddableApi.customizePanel.flyout.optionsMenuForm.showCustomTimeRangeSwitch"
/>
}
onChange={(e) => setInheritTimeRange(!e.target.checked)}
/>
</EuiFormRow>
) : null}
{!inheritTimeRange ? (
<EuiFormRow
label={
<FormattedMessage
id="embeddableApi.customizePanel.flyout.optionsMenuForm.panelTimeRangeFormRowLabel"
defaultMessage="Time range"
/>
}
>
<EuiSuperDatePicker
start={panelTimeRange?.from ?? undefined}
end={panelTimeRange?.to ?? undefined}
onTimeChange={({ start, end }) => setPanelTimeRange({ from: start, to: end })}
showUpdateButton={false}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRangesForDatePicker}
data-test-subj="customizePanelTimeRangeDatePicker"
/>
</EuiFormRow>
) : null}
</>
);
};
return (
<>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="embeddableApi.customizePanel.flyout.title"
defaultMessage="Panel settings"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiForm>
{renderCustomTitleComponent()}
{renderCustomTimeRangeComponent()}
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelCustomizePanelButton" onClick={onClose}>
<FormattedMessage
id="embeddableApi.customizePanel.flyout.cancelButtonTitle"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton data-test-subj="saveCustomizePanelButton" onClick={save} fill>
<FormattedMessage
id="embeddableApi.customizePanel.flyout.saveButtonTitle"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};

View file

@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
import { Embeddable, IContainer, ContainerInput } from '@kbn/embeddable-plugin/public';
import { TimeRangeInput } from './custom_time_range_action';
import { Embeddable, IContainer, ContainerInput } from '../../../..';
import { TimeRangeInput } from './customize_panel_action';
export function doesInheritTimeRange(embeddable: Embeddable<TimeRangeInput>) {
if (!embeddable.parent) {

View file

@ -1,102 +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 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 or the Server
* Side Public License, v 1.
*/
import { Container, isErrorEmbeddable } from '../../../..';
import { nextTick } from '@kbn/test-jest-helpers';
import { CustomizePanelTitleAction } from './customize_panel_action';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
} from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container';
import { embeddablePluginMock } from '../../../../../mocks';
let container: Container;
let embeddable: ContactCardEmbeddable;
function createHelloWorldContainer(input = { id: '123', panels: {} }) {
const { setup, doStart } = embeddablePluginMock.createInstance();
setup.registerEmbeddableFactory(
CONTACT_CARD_EMBEDDABLE,
new ContactCardEmbeddableFactory((() => {}) as any, {} as any)
);
const getEmbeddableFactory = doStart().getEmbeddableFactory;
return new HelloWorldContainer(input, { getEmbeddableFactory } as any);
}
beforeEach(async () => {
container = createHelloWorldContainer();
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
id: 'robert',
firstName: 'Robert',
lastName: 'Baratheon',
});
if (isErrorEmbeddable(contactCardEmbeddable)) {
throw new Error('Error creating new hello world embeddable');
} else {
embeddable = contactCardEmbeddable;
}
});
test('Updates the embeddable title when given', async () => {
const getUserData = () => Promise.resolve({ title: 'What is up?' });
const customizePanelAction = new CustomizePanelTitleAction(getUserData);
expect(embeddable.getInput().title).toBeUndefined();
expect(embeddable.getTitle()).toBe('Hello Robert Baratheon');
await customizePanelAction.execute({ embeddable });
await nextTick();
expect(embeddable.getTitle()).toBe('What is up?');
expect(embeddable.getInput().title).toBe('What is up?');
// Recreating the container should preserve the custom title.
const containerClone = createHelloWorldContainer(container.getInput());
// Need to wait for the container to tell us the embeddable has been loaded.
const subscription = await containerClone.getOutput$().subscribe(() => {
if (containerClone.getOutput().embeddableLoaded[embeddable.id]) {
expect(embeddable.getInput().title).toBe('What is up?');
subscription.unsubscribe();
}
});
});
test('Empty string results in an empty title', async () => {
const getUserData = () => Promise.resolve({ title: '' });
const customizePanelAction = new CustomizePanelTitleAction(getUserData);
expect(embeddable.getInput().title).toBeUndefined();
expect(embeddable.getTitle()).toBe('Hello Robert Baratheon');
await customizePanelAction.execute({ embeddable });
await nextTick();
expect(embeddable.getTitle()).toBe('');
});
test('Undefined title results in the original title', async () => {
const getUserData = () => Promise.resolve({ title: 'hi' });
const customizePanelAction = new CustomizePanelTitleAction(getUserData);
expect(embeddable.getInput().title).toBeUndefined();
expect(embeddable.getTitle()).toBe('Hello Robert Baratheon');
await customizePanelAction.execute({ embeddable });
await nextTick();
expect(embeddable.getTitle()).toBe('hi');
await new CustomizePanelTitleAction(() => Promise.resolve({ title: undefined })).execute({
embeddable,
});
await nextTick();
expect(embeddable.getTitle()).toBe('Hello Robert Baratheon');
});

View file

@ -1,50 +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 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 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { Action } from '@kbn/ui-actions-plugin/public';
import { ViewMode } from '../../../../types';
import { IEmbeddable } from '../../../../embeddables';
export const ACTION_CUSTOMIZE_PANEL = 'ACTION_CUSTOMIZE_PANEL';
type GetUserData = (
context: ActionContext
) => Promise<{ title: string | undefined; hideTitle?: boolean }>;
interface ActionContext {
embeddable: IEmbeddable;
}
export class CustomizePanelTitleAction implements Action<ActionContext> {
public readonly type = ACTION_CUSTOMIZE_PANEL;
public id = ACTION_CUSTOMIZE_PANEL;
public order = 40;
constructor(private readonly getDataFromUser: GetUserData) {}
public getDisplayName() {
return i18n.translate('embeddableApi.customizePanel.action.displayName', {
defaultMessage: 'Edit panel title',
});
}
public getIconType() {
return 'pencil';
}
public async isCompatible({ embeddable }: ActionContext) {
return embeddable.getInput().viewMode === ViewMode.EDIT ? true : false;
}
public async execute({ embeddable }: ActionContext) {
const data = await this.getDataFromUser({ embeddable });
const { title, hideTitle } = data;
embeddable.updateInput({ title, hidePanelTitles: hideTitle });
}
}

View file

@ -1,165 +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 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 or the Server
* Side Public License, v 1.
*/
import React, { Component, FormEvent } from 'react';
import {
EuiFormRow,
EuiFieldText,
EuiButton,
EuiSwitch,
EuiButtonEmpty,
EuiModalHeader,
EuiModalFooter,
EuiModalBody,
EuiModalHeaderTitle,
EuiFocusTrap,
EuiOutsideClickDetector,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { IEmbeddable } from '../../../..';
interface CustomizePanelProps {
embeddable: IEmbeddable;
updateTitle: (newTitle: string | undefined, hideTitle: boolean | undefined) => void;
cancel: () => void;
}
interface State {
title: string | undefined;
hideTitle: boolean | undefined;
}
export class CustomizePanelModal extends Component<CustomizePanelProps, State> {
constructor(props: CustomizePanelProps) {
super(props);
this.state = {
hideTitle: props.embeddable.getInput().hidePanelTitles,
title: props.embeddable.getInput().title ?? this.props.embeddable.getOutput().defaultTitle,
};
}
private reset = () => {
this.setState({
title: this.props.embeddable.getOutput().defaultTitle,
});
};
private onHideTitleToggle = () => {
this.setState((prevState) => ({
hideTitle: !prevState.hideTitle,
}));
};
private save = () => {
const newTitle =
this.state.title === this.props.embeddable.getOutput().defaultTitle
? undefined
: this.state.title;
this.props.updateTitle(newTitle, this.state.hideTitle);
};
public render() {
const titleId = 'customizePanelModalTitle';
return (
<EuiFocusTrap clickOutsideDisables={true} initialFocus={'.panelTitleInputText'}>
<EuiOutsideClickDetector onOutsideClick={this.props.cancel}>
<div role="dialog" aria-modal="true" aria-labelledby={titleId} className="euiModal__flex">
<form
onSubmit={(event: FormEvent) => {
event.preventDefault();
this.save();
}}
>
<EuiModalHeader>
<EuiModalHeaderTitle
data-test-subj="customizePanelTitle"
id={titleId}
component="h2"
>
Customize panel
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFormRow>
<EuiSwitch
checked={!this.state.hideTitle}
data-test-subj="customizePanelHideTitle"
id="hideTitle"
label={
<FormattedMessage
defaultMessage="Show panel title"
id="embeddableApi.customizePanel.modal.showTitle"
/>
}
onChange={this.onHideTitleToggle}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel',
{
defaultMessage: 'Panel title',
}
)}
>
<EuiFieldText
id="panelTitleInput"
className="panelTitleInputText"
data-test-subj="customEmbeddablePanelTitleInput"
name="min"
type="text"
disabled={this.state.hideTitle}
value={this.state.title || ''}
onChange={(e) => this.setState({ title: e.target.value })}
aria-label={i18n.translate(
'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel',
{
defaultMessage: 'Enter a custom title for your panel',
}
)}
append={
<EuiButtonEmpty
data-test-subj="resetCustomEmbeddablePanelTitle"
onClick={this.reset}
disabled={this.state.hideTitle}
>
<FormattedMessage
id="embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel"
defaultMessage="Reset"
/>
</EuiButtonEmpty>
}
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={() => this.props.cancel()}>
<FormattedMessage
id="embeddableApi.customizePanel.modal.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton data-test-subj="saveNewTitleButton" onClick={this.save} fill>
<FormattedMessage
id="embeddableApi.customizePanel.modal.saveButtonTitle"
defaultMessage="Save"
/>
</EuiButton>
</EuiModalFooter>
</form>
</div>
</EuiOutsideClickDetector>
</EuiFocusTrap>
);
}
}

View file

@ -1,64 +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 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 or the Server
* Side Public License, v 1.
*/
import React, { ChangeEvent } from 'react';
import { EuiButtonEmpty, EuiFieldText, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
export interface PanelOptionsMenuFormProps {
title?: string;
onReset: () => void;
onUpdatePanelTitle: (newPanelTitle: string) => void;
}
export function CustomizeTitleForm({
title,
onReset,
onUpdatePanelTitle,
}: PanelOptionsMenuFormProps) {
function onInputChange(event: ChangeEvent<HTMLInputElement>) {
onUpdatePanelTitle(event.target.value);
}
return (
<div className="embPanel__optionsMenuForm" data-test-subj="dashboardPanelTitleInputMenuItem">
<EuiFormRow
label={i18n.translate(
'embeddableApi.customizeTitle.optionsMenuForm.panelTitleFormRowLabel',
{
defaultMessage: 'Panel title',
}
)}
>
<EuiFieldText
id="panelTitleInput"
data-test-subj="customEmbeddablePanelTitleInput"
name="min"
type="text"
value={title}
onChange={onInputChange}
aria-label={i18n.translate(
'embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel',
{
defaultMessage: 'Changes to this input are applied immediately. Press enter to exit.',
}
)}
/>
</EuiFormRow>
<EuiButtonEmpty data-test-subj="resetCustomEmbeddablePanelTitle" onClick={onReset}>
<FormattedMessage
id="embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel"
defaultMessage="Reset title"
/>
</EuiButtonEmpty>
</div>
);
}

View file

@ -9,4 +9,4 @@
export * from './inspect_panel_action';
export * from './add_panel';
export * from './remove_panel_action';
export * from './customize_title';
export * from './customize_panel';

View file

@ -22,10 +22,11 @@ import { Action } from '@kbn/ui-actions-plugin/public';
import { PanelOptionsMenu } from './panel_options_menu';
import { IEmbeddable } from '../../embeddables';
import { EmbeddableContext, panelBadgeTrigger, panelNotificationTrigger } from '../../triggers';
import { CustomizePanelTitleAction } from '.';
import { CustomizePanelAction } from '.';
export interface PanelHeaderProps {
title?: string;
description?: string;
index?: number;
isViewMode: boolean;
hidePanelTitle: boolean;
@ -39,7 +40,7 @@ export interface PanelHeaderProps {
embeddable: IEmbeddable;
headerId?: string;
showPlaceholderTitle?: boolean;
customizeTitle?: CustomizePanelTitleAction;
customizePanel?: CustomizePanelAction;
}
function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmbeddable) {
@ -50,6 +51,7 @@ function renderBadges(badges: Array<Action<EmbeddableContext>>, embeddable: IEmb
iconType={badge.getIconType({ embeddable, trigger: panelBadgeTrigger })}
onClick={() => badge.execute({ embeddable, trigger: panelBadgeTrigger })}
onClickAriaLabel={badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
data-test-subj={`embeddablePanelBadge-${badge.id}`}
>
{badge.getDisplayName({ embeddable, trigger: panelBadgeTrigger })}
</EuiBadge>
@ -101,22 +103,9 @@ function renderNotifications(
});
}
type EmbeddableWithDescription = IEmbeddable & { getDescription: () => string };
function getViewDescription(embeddable: IEmbeddable | EmbeddableWithDescription) {
if ('getDescription' in embeddable) {
const description = embeddable.getDescription();
if (description) {
return description;
}
}
return '';
}
export function PanelHeader({
title,
description,
index,
isViewMode,
hidePanelTitle,
@ -126,9 +115,8 @@ export function PanelHeader({
notifications,
embeddable,
headerId,
customizeTitle,
customizePanel,
}: PanelHeaderProps) {
const description = getViewDescription(embeddable);
const showTitle = !hidePanelTitle && (!isViewMode || title);
const showPanelBar =
!isViewMode || badges.length > 0 || notifications.length > 0 || showTitle || description;
@ -181,7 +169,7 @@ export function PanelHeader({
>
{title || placeholderTitle}
</span>
) : customizeTitle ? (
) : customizePanel ? (
<EuiLink
color="text"
data-test-subj={'embeddablePanelTitleLink'}
@ -193,7 +181,7 @@ export function PanelHeader({
defaultMessage: 'Click to edit title: {title}',
values: { title: title || placeholderTitle },
})}
onClick={() => customizeTitle.execute({ embeddable })}
onClick={() => customizePanel.execute({ embeddable })}
>
{title || placeholderTitle}
</EuiLink>

View file

@ -21,7 +21,7 @@ export interface FilterableContainerInput extends ContainerInput {
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
export type InheritedChildrenInput = {
type InheritedChildrenInput = {
filters: MockFilter[];
id?: string;
};

View file

@ -14,3 +14,6 @@ export * from './filterable_embeddable';
export * from './filterable_embeddable_factory';
export * from './hello_world_container';
export * from './hello_world_container_component';
export * from './time_range_container';
export * from './time_range_embeddable_factory';
export * from './time_range_embeddable';

View file

@ -6,20 +6,15 @@
* Side Public License, v 1.
*/
import {
ContainerInput,
Container,
ContainerOutput,
EmbeddableStart,
} from '@kbn/embeddable-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { ContainerInput, Container, ContainerOutput, EmbeddableStart } from '../../..';
/**
* interfaces are not allowed to specify a sub-set of the required types until
* https://github.com/microsoft/TypeScript/issues/15300 is fixed so we use a type
* here instead
*/
export type InheritedChildrenInput = {
type InheritedChildrenInput = {
timeRange: TimeRange;
id?: string;
};

View file

@ -6,15 +6,10 @@
* Side Public License, v 1.
*/
import {
EmbeddableOutput,
Embeddable,
EmbeddableInput,
IContainer,
} from '@kbn/embeddable-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { EmbeddableOutput, Embeddable, EmbeddableInput, IContainer } from '../../..';
interface EmbeddableTimeRangeInput extends EmbeddableInput {
export interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
@ -24,7 +19,15 @@ export class TimeRangeEmbeddable extends Embeddable<EmbeddableTimeRangeInput, Em
public readonly type = TIME_RANGE_EMBEDDABLE;
constructor(initialInput: EmbeddableTimeRangeInput, parent?: IContainer) {
super(initialInput, {}, parent);
const { title: defaultTitle, description: defaultDescription } = initialInput;
super(
initialInput,
{
defaultTitle,
defaultDescription,
},
parent
);
}
public render() {}

View file

@ -6,17 +6,12 @@
* Side Public License, v 1.
*/
import { IContainer, EmbeddableFactoryDefinition } from '../../..';
import {
EmbeddableInput,
IContainer,
EmbeddableFactoryDefinition,
} from '@kbn/embeddable-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { TIME_RANGE_EMBEDDABLE, TimeRangeEmbeddable } from './time_range_embeddable';
interface EmbeddableTimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
TIME_RANGE_EMBEDDABLE,
TimeRangeEmbeddable,
EmbeddableTimeRangeInput,
} from './time_range_embeddable';
export class TimeRangeEmbeddableFactory
implements EmbeddableFactoryDefinition<EmbeddableTimeRangeInput>

View file

@ -23,3 +23,9 @@ export interface PropertySpec {
}
export { ViewMode } from '../../common/types';
export type { Adapters };
export interface CommonlyUsedRange {
from: string;
to: string;
display: string;
}

View file

@ -9,6 +9,7 @@
import React from 'react';
import { Subscription } from 'rxjs';
import { identity } from 'lodash';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import type { SerializableRecord } from '@kbn/utility-types';
import { getSavedObjectFinder } from '@kbn/saved-objects-plugin/public';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
@ -39,6 +40,7 @@ import {
EmbeddablePanel,
SavedObjectEmbeddableInput,
EmbeddableContainerContext,
PANEL_BADGE_TRIGGER,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
import { EmbeddableStateTransfer } from './lib/state_transfer';
@ -53,6 +55,7 @@ import {
} from '../common/lib';
import { getAllMigrations } from '../common/lib/get_all_migrations';
import { setTheme } from './services';
import { CustomTimeRangeBadge } from './lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge';
export interface EmbeddableSetupDependencies {
uiActions: UiActionsSetup;
@ -151,6 +154,20 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
);
});
const { overlays, theme, uiSettings } = core;
const dateFormat = uiSettings.get(UI_SETTINGS.DATE_FORMAT);
const commonlyUsedRanges = uiSettings.get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES);
const timeRangeBadge = new CustomTimeRangeBadge(
overlays,
theme,
commonlyUsedRanges,
dateFormat
);
uiActions.addTriggerAction(PANEL_BADGE_TRIGGER, timeRangeBadge);
this.appListSubscription = core.application.applications$.subscribe((appList) => {
this.appList = appList;
});
@ -184,13 +201,15 @@ export class EmbeddablePublicPlugin implements Plugin<EmbeddableSetup, Embeddabl
getActions={uiActions.getTriggerCompatibleActions}
getEmbeddableFactory={this.getEmbeddableFactory}
getAllEmbeddableFactories={this.getEmbeddableFactories}
overlays={core.overlays}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges}
overlays={overlays}
notifications={core.notifications}
application={core.application}
inspector={inspector}
SavedObjectFinder={getSavedObjectFinder(core.savedObjects, core.uiSettings)}
containerContext={containerContext}
theme={core.theme}
theme={theme}
/>
);

View file

@ -0,0 +1,201 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { findTestSubject } from '@elastic/eui/lib/test';
import * as React from 'react';
import { EmbeddableOutput, isErrorEmbeddable, ViewMode } from '../lib';
import { coreMock } from '@kbn/core/public/mocks';
import { testPlugin } from './test_plugin';
import { CustomizePanelEditor } from '../lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import {
EmbeddableTimeRangeInput,
TimeRangeContainer,
TimeRangeEmbeddable,
TimeRangeEmbeddableFactory,
TIME_RANGE_EMBEDDABLE,
} from '../lib/test_samples';
let container: TimeRangeContainer;
let embeddable: TimeRangeEmbeddable;
beforeEach(async () => {
const { doStart, setup } = testPlugin(coreMock.createSetup(), coreMock.createStart());
const timeRangeFactory = new TimeRangeEmbeddableFactory();
setup.registerEmbeddableFactory(timeRangeFactory.type, timeRangeFactory);
const { getEmbeddableFactory } = doStart();
container = new TimeRangeContainer(
{ id: '123', panels: {}, timeRange: { from: '-7d', to: 'now' } },
getEmbeddableFactory
);
const timeRangeEmbeddable = await container.addNewEmbeddable<
EmbeddableTimeRangeInput,
EmbeddableOutput,
TimeRangeEmbeddable
>(TIME_RANGE_EMBEDDABLE, {
id: '4321',
title: 'A time series',
description: 'This might be a neat line chart',
viewMode: ViewMode.EDIT,
});
if (isErrorEmbeddable(timeRangeEmbeddable)) {
throw new Error('Error creating new hello world embeddable');
} else {
embeddable = timeRangeEmbeddable;
}
});
test('Value is initialized with the embeddables title', async () => {
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const titleField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const descriptionField = findTestSubject(component, 'customEmbeddablePanelDescriptionInput').find(
'textarea'
);
expect(titleField.props().value).toBe(embeddable.getOutput().title);
expect(descriptionField.props().value).toBe(embeddable.getOutput().description);
});
test('Calls updateInput with a new title', async () => {
const updateInput = jest.spyOn(embeddable, 'updateInput');
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'new title' } };
inputField.simulate('change', event);
findTestSubject(component, 'saveCustomizePanelButton').simulate('click');
expect(updateInput).toBeCalledWith({
title: 'new title',
});
});
test('Input value shows custom title if one given', async () => {
embeddable.updateInput({ title: 'new title' });
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
expect(inputField.props().value).toBe('new title');
findTestSubject(component, 'saveCustomizePanelButton').simulate('click');
expect(inputField.props().value).toBe('new title');
});
test('Reset updates the input values with the default properties when the embeddable has overridden the properties', async () => {
embeddable.updateInput({ title: 'my custom title', description: 'my custom description' });
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const titleField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'another custom title' } };
titleField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitleButton').simulate('click');
const titleAfter = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
expect(titleAfter.props().value).toBe(embeddable.getOutput().defaultTitle);
findTestSubject(component, 'resetCustomEmbeddablePanelDescriptionButton').simulate('click');
const descriptionAfter = findTestSubject(component, 'customEmbeddablePanelDescriptionInput').find(
'textarea'
);
expect(descriptionAfter.props().value).toBe(embeddable.getOutput().defaultDescription);
});
test('Reset updates the input with the default properties when the embeddable has no property overrides', async () => {
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const titleField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const titleEvent = { target: { value: 'new title' } };
titleField.simulate('change', titleEvent);
const descriptionField = findTestSubject(component, 'customEmbeddablePanelDescriptionInput').find(
'textarea'
);
const descriptionEvent = { target: { value: 'new description' } };
titleField.simulate('change', descriptionEvent);
findTestSubject(component, 'resetCustomEmbeddablePanelTitleButton').simulate('click');
findTestSubject(component, 'resetCustomEmbeddablePanelDescriptionButton').simulate('click');
await component.update();
expect(titleField.props().value).toBe(embeddable.getOutput().defaultTitle);
expect(descriptionField.props().value).toBe(embeddable.getOutput().defaultDescription);
});
test('Reset title calls updateInput with undefined', async () => {
const updateInput = jest.spyOn(embeddable, 'updateInput');
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'new title' } };
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitleButton').simulate('click');
findTestSubject(component, 'saveCustomizePanelButton').simulate('click');
expect(updateInput).toBeCalledWith({
title: undefined,
});
});
test('Reset description calls updateInput with undefined', async () => {
const updateInput = jest.spyOn(embeddable, 'updateInput');
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelDescriptionInput').find(
'textarea'
);
const event = { target: { value: 'new title' } };
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelDescriptionButton').simulate('click');
findTestSubject(component, 'saveCustomizePanelButton').simulate('click');
expect(updateInput).toBeCalledWith({
description: undefined,
});
});
test('Can set title and description to an empty string', async () => {
const updateInput = jest.spyOn(embeddable, 'updateInput');
const component = mountWithIntl(
<CustomizePanelEditor embeddable={embeddable} timeRangeCompatible={true} onClose={() => {}} />
);
for (const subject of [
'customEmbeddablePanelTitleInput',
'customEmbeddablePanelDescriptionInput',
]) {
const inputField = findTestSubject(component, subject);
const event = { target: { value: '' } };
inputField.simulate('change', event);
}
findTestSubject(component, 'saveCustomizePanelButton').simulate('click');
const titleFieldAfter = findTestSubject(component, 'customEmbeddablePanelTitleInput');
const descriptionFieldAfter = findTestSubject(component, 'customEmbeddablePanelDescriptionInput');
expect(titleFieldAfter.props().value).toBe('');
expect(descriptionFieldAfter.props().value).toBe('');
expect(updateInput).toBeCalledWith({ description: '', title: '' });
});

View file

@ -1,178 +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 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 or the Server
* Side Public License, v 1.
*/
import { findTestSubject } from '@elastic/eui/lib/test';
import * as React from 'react';
import { Container, isErrorEmbeddable } from '../lib';
import {
ContactCardEmbeddable,
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
} from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable';
import {
CONTACT_CARD_EMBEDDABLE,
ContactCardEmbeddableFactory,
} from '../lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory';
import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world_container';
import { coreMock } from '@kbn/core/public/mocks';
import { testPlugin } from './test_plugin';
import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal';
import { EmbeddableStart } from '../plugin';
import { createEmbeddablePanelMock } from '../mocks';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { OverlayStart } from '@kbn/core/public';
let api: EmbeddableStart;
let container: Container;
let embeddable: ContactCardEmbeddable;
beforeEach(async () => {
const { doStart, coreStart, uiActions, setup } = testPlugin(
coreMock.createSetup(),
coreMock.createStart()
);
const contactCardFactory = new ContactCardEmbeddableFactory(
uiActions.executeTriggerActions,
{} as unknown as OverlayStart
);
setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory);
api = doStart();
const testPanel = createEmbeddablePanelMock({
getActions: uiActions.getTriggerCompatibleActions,
getEmbeddableFactory: api.getEmbeddableFactory,
getAllEmbeddableFactories: api.getEmbeddableFactories,
overlays: coreStart.overlays,
notifications: coreStart.notifications,
application: coreStart.application,
});
container = new HelloWorldContainer(
{ id: '123', panels: {} },
{
getEmbeddableFactory: api.getEmbeddableFactory,
panelComponent: testPanel,
}
);
const contactCardEmbeddable = await container.addNewEmbeddable<
ContactCardEmbeddableInput,
ContactCardEmbeddableOutput,
ContactCardEmbeddable
>(CONTACT_CARD_EMBEDDABLE, {
firstName: 'Joe',
});
if (isErrorEmbeddable(contactCardEmbeddable)) {
throw new Error('Error creating new hello world embeddable');
} else {
embeddable = contactCardEmbeddable;
}
});
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().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 = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'new title' } };
inputField.simulate('change', event);
findTestSubject(component, 'saveNewTitleButton').simulate('click');
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 = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
expect(inputField.props().value).toBe('new title');
findTestSubject(component, 'saveNewTitleButton').simulate('click');
expect(inputField.props().value).toBe('new title');
});
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 = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'another custom title' } };
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click');
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 no title override', async () => {
const updateTitle = jest.fn();
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'new title' } };
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click');
await component.update();
expect(inputField.props().value).toBe(embeddable.getOutput().defaultTitle);
});
test('Reset calls updateTitle with undefined', async () => {
const updateTitle = jest.fn();
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input');
const event = { target: { value: 'new title' } };
inputField.simulate('change', event);
findTestSubject(component, 'resetCustomEmbeddablePanelTitle').simulate('click');
findTestSubject(component, 'saveNewTitleButton').simulate('click');
expect(updateTitle).toBeCalledWith(undefined, undefined);
});
test('Can set title to an empty string', async () => {
const updateTitle = jest.fn();
const component = mountWithIntl(
<CustomizePanelModal embeddable={embeddable} updateTitle={updateTitle} cancel={() => {}} />
);
const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput');
const event = { target: { value: '' } };
inputField.simulate('change', event);
findTestSubject(component, 'saveNewTitleButton').simulate('click');
const inputFieldAfter = findTestSubject(component, 'customEmbeddablePanelTitleInput');
expect(inputFieldAfter.props().value).toBe('');
expect(updateTitle).toBeCalledWith('', undefined);
});

View file

@ -27,6 +27,9 @@
"@kbn/expressions-plugin",
"@kbn/usage-collection-plugin",
"@kbn/analytics",
"@kbn/data-plugin",
"@kbn/core-overlays-browser-mocks",
"@kbn/core-theme-browser-mocks",
],
"exclude": [
"target/**/*",

View file

@ -11,5 +11,5 @@
"ui": true,
"requiredPlugins": ["embeddable", "uiActions"],
"optionalPlugins": ["licensing"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "data"]
"requiredBundles": ["kibanaUtils", "kibanaReact"]
}

View file

@ -1,328 +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 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 or the Server
* Side Public License, v 1.
*/
import { findTestSubject } from '@elastic/eui/lib/test';
import { skip, take } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { CustomTimeRangeAction } from './custom_time_range_action';
import { HelloWorldContainer } from '@kbn/embeddable-plugin/public/lib/test_samples';
import {
HelloWorldEmbeddable,
HELLO_WORLD_EMBEDDABLE,
} from '@kbn/embeddable-plugin/public/tests/fixtures';
import { nextTick } from '@kbn/test-jest-helpers';
import { ReactElement } from 'react';
const createOpenModalMock = () => {
const mock = jest.fn();
mock.mockReturnValue({ close: jest.fn() });
return mock;
};
test('Custom time range action prevents embeddable from using container time', async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
expect(child1).toBeDefined();
expect(child1.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' });
const child2 = container.getChild<TimeRangeEmbeddable>('2');
expect(child2).toBeDefined();
expect(child2.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' });
const openModalMock = createOpenModalMock();
new CustomTimeRangeAction({
openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } });
findTestSubject(wrapper, 'addPerPanelTimeRangeButton').simulate('click');
const promise = Rx.merge(container.getOutput$(), container.getOutput$(), container.getInput$())
.pipe(skip(2), take(1))
.toPromise();
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
await promise;
expect(child1.getInput().timeRange).toEqual({ from: 'now-30days', to: 'now-29days' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-30m', to: 'now-1m' });
});
test('Removing custom time range action resets embeddable back to container time', async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('2');
const openModalMock = createOpenModalMock();
new CustomTimeRangeAction({
openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-30days', to: 'now-29days' } });
findTestSubject(wrapper, 'addPerPanelTimeRangeButton').simulate('click');
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
new CustomTimeRangeAction({
openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal2 = openModalMock.mock.calls[1][0];
const wrapper2 = mount(openModal2);
findTestSubject(wrapper2, 'removePerPanelTimeRangeButton').simulate('click');
const promise = Rx.merge(container.getOutput$(), container.getOutput$(), container.getInput$())
.pipe(skip(2), take(1))
.toPromise();
container.updateInput({ timeRange: { from: 'now-10m', to: 'now-5m' } });
await promise;
expect(child1.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
});
test('Cancelling custom time range action leaves state alone', async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { to: '2', from: '1' },
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('2');
const openModalMock = createOpenModalMock();
new CustomTimeRangeAction({
openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
wrapper.setState({ timeRange: { from: 'now-300m', to: 'now-400m' } });
findTestSubject(wrapper, 'cancelPerPanelTimeRangeButton').simulate('click');
const promise = Rx.merge(container.getOutput$(), container.getOutput$(), container.getInput$())
.pipe(skip(2), take(1))
.toPromise();
container.updateInput({ timeRange: { from: 'now-30m', to: 'now-1m' } });
await promise;
expect(child1.getInput().timeRange).toEqual({ from: '1', to: '2' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-30m', to: 'now-1m' });
});
test(`badge is compatible with embeddable that inherits from parent`, async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const openModalMock = createOpenModalMock();
const compatible = await new CustomTimeRangeAction({
openModal: openModalMock,
commonlyUsedRanges: [],
dateFormat: 'MM YYY',
}).isCompatible({
embeddable: child,
});
expect(compatible).toBe(true);
});
// TODO: uncomment when https://github.com/elastic/kibana/issues/43271 is fixed.
// test('Embeddable that does not use time range in a container that has time range is incompatible', async () => {
// const container = new TimeRangeContainer(
// {
// timeRange: { from: 'now-15m', to: 'now' },
// panels: {
// '1': {
// type: HELLO_WORLD_EMBEDDABLE,
// explicitInput: {
// id: '1',
// },
// },
// },
// id: '123',
// },
// () => undefined
// );
// await container.untilEmbeddableLoaded('1');
// const child = container.getChild<HelloWorldEmbeddable>('1');
// const start = coreMock.createStart();
// const action = await new CustomTimeRangeAction({
// openModal: start.overlays.openModal,
// dateFormat: 'MM YYYY',
// commonlyUsedRanges: [],
// });
// async function check() {
// await action.execute({ embeddable: child });
// }
// await expect(check()).rejects.toThrow(Error);
// });
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const container = new HelloWorldContainer(
{
panels: {
'1': {
type: HELLO_WORLD_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
{}
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<HelloWorldEmbeddable>('1');
const openModalMock = createOpenModalMock();
const action = await new CustomTimeRangeAction({
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});
async function check() {
// @ts-ignore
await action.execute({ embeddable: child });
}
await expect(check()).rejects.toThrow(Error);
});

View file

@ -1,124 +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 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 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
IEmbeddable,
Embeddable,
EmbeddableInput,
EmbeddableOutput,
} from '@kbn/embeddable-plugin/public';
import { TimeRange } from '@kbn/es-query';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { OpenModal, CommonlyUsedRange } from './types';
export const CUSTOM_TIME_RANGE = 'CUSTOM_TIME_RANGE';
export interface TimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
function hasTimeRange(
embeddable: IEmbeddable | Embeddable<TimeRangeInput>
): embeddable is Embeddable<TimeRangeInput> {
return (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange !== undefined;
}
const VISUALIZE_EMBEDDABLE_TYPE = 'visualization';
type VisualizeEmbeddable = IEmbeddable<{ id: string }, EmbeddableOutput & { visTypeName: string }>;
function isVisualizeEmbeddable(
embeddable: IEmbeddable | VisualizeEmbeddable
): embeddable is VisualizeEmbeddable {
return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE;
}
export interface TimeRangeActionContext {
embeddable: Embeddable<TimeRangeInput>;
}
export class CustomTimeRangeAction implements Action<TimeRangeActionContext> {
public readonly type = CUSTOM_TIME_RANGE;
private openModal: OpenModal;
private dateFormat?: string;
private commonlyUsedRanges: CommonlyUsedRange[];
public readonly id = CUSTOM_TIME_RANGE;
public order = 30;
constructor({
openModal,
dateFormat,
commonlyUsedRanges,
}: {
openModal: OpenModal;
dateFormat: string;
commonlyUsedRanges: CommonlyUsedRange[];
}) {
this.openModal = openModal;
this.dateFormat = dateFormat;
this.commonlyUsedRanges = commonlyUsedRanges;
}
public getDisplayName() {
return i18n.translate('uiActionsEnhanced.customizeTimeRangeMenuItem.displayName', {
defaultMessage: 'Customize time range',
});
}
public getIconType() {
return 'calendar';
}
public async isCompatible({ embeddable }: TimeRangeActionContext) {
const isInputControl =
isVisualizeEmbeddable(embeddable) &&
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'input_control_vis';
const isMarkdown =
isVisualizeEmbeddable(embeddable) &&
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown';
const isImage = embeddable.type === 'image';
return Boolean(
embeddable &&
embeddable.parent &&
hasTimeRange(embeddable) &&
!isInputControl &&
!isMarkdown &&
!isImage
);
}
public async execute({ embeddable }: TimeRangeActionContext) {
const isCompatible = await this.isCompatible({ embeddable });
if (!isCompatible) {
throw new IncompatibleActionError();
}
// Only here for typescript
if (hasTimeRange(embeddable)) {
const CustomizeTimeRangeModal = await import('./customize_time_range_modal').then(
(m) => m.CustomizeTimeRangeModal
);
const modalSession = this.openModal(
<CustomizeTimeRangeModal
onClose={() => modalSession.close()}
embeddable={embeddable}
dateFormat={this.dateFormat}
commonlyUsedRanges={this.commonlyUsedRanges}
/>,
{
'data-test-subj': 'customizeTimeRangeModal',
}
);
}
}
}

View file

@ -1,174 +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 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 or the Server
* Side Public License, v 1.
*/
import { findTestSubject } from '@elastic/eui/lib/test';
import { skip, take } from 'rxjs/operators';
import * as Rx from 'rxjs';
import { mount } from 'enzyme';
import { TimeRangeEmbeddable, TimeRangeContainer, TIME_RANGE_EMBEDDABLE } from './test_helpers';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
import { ReactElement } from 'react';
import { nextTick } from '@kbn/test-jest-helpers';
test('Removing custom time range from badge resets embeddable back to container time', async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { from: '1', to: '2' },
},
},
'2': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '2',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
await container.untilEmbeddableLoaded('2');
const child1 = container.getChild<TimeRangeEmbeddable>('1');
const child2 = container.getChild<TimeRangeEmbeddable>('2');
const openModalMock = jest.fn();
openModalMock.mockReturnValue({ close: jest.fn() });
new CustomTimeRangeBadge({
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).execute({
embeddable: child1,
});
await nextTick();
const openModal = openModalMock.mock.calls[0][0] as ReactElement;
const wrapper = mount(openModal);
findTestSubject(wrapper, 'removePerPanelTimeRangeButton').simulate('click');
const promise = Rx.merge(child1.getInput$(), container.getOutput$(), container.getInput$())
.pipe(skip(4), take(1))
.toPromise();
container.updateInput({ timeRange: { from: 'now-10m', to: 'now-5m' } });
await promise;
expect(child1.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
expect(child2.getInput().timeRange).toEqual({ from: 'now-10m', to: 'now-5m' });
});
test(`badge is not compatible with embeddable that inherits from parent`, async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const openModalMock = jest.fn();
const compatible = await new CustomTimeRangeBadge({
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).isCompatible({
embeddable: child,
});
expect(compatible).toBe(false);
});
test(`badge is compatible with embeddable that has custom time range`, async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
timeRange: { to: '123', from: '456' },
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const openModalMock = jest.fn();
const compatible = await new CustomTimeRangeBadge({
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
}).isCompatible({
embeddable: child,
});
expect(compatible).toBe(true);
});
test('Attempting to execute on incompatible embeddable throws an error', async () => {
const container = new TimeRangeContainer(
{
timeRange: { from: 'now-15m', to: 'now' },
panels: {
'1': {
type: TIME_RANGE_EMBEDDABLE,
explicitInput: {
id: '1',
},
},
},
id: '123',
},
() => undefined
);
await container.untilEmbeddableLoaded('1');
const child = container.getChild<TimeRangeEmbeddable>('1');
const openModalMock = jest.fn();
const badge = await new CustomTimeRangeBadge({
openModal: openModalMock,
dateFormat: 'MM YYYY',
commonlyUsedRanges: [],
});
async function check() {
await badge.execute({ embeddable: child });
}
await expect(check()).rejects.toThrow(Error);
});

View file

@ -1,98 +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 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 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { renderToString } from 'react-dom/server';
import { PrettyDuration } from '@elastic/eui';
import { IEmbeddable, Embeddable, EmbeddableInput } from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { doesInheritTimeRange } from './does_inherit_time_range';
import { OpenModal, CommonlyUsedRange } from './types';
export const CUSTOM_TIME_RANGE_BADGE = 'CUSTOM_TIME_RANGE_BADGE';
export interface TimeRangeInput extends EmbeddableInput {
timeRange: TimeRange;
}
function hasTimeRange(
embeddable: IEmbeddable | Embeddable<TimeRangeInput>
): embeddable is Embeddable<TimeRangeInput> {
return (embeddable as Embeddable<TimeRangeInput>).getInput().timeRange !== undefined;
}
export interface TimeBadgeActionContext {
embeddable: Embeddable<TimeRangeInput>;
}
export class CustomTimeRangeBadge implements Action<TimeBadgeActionContext> {
public readonly type = CUSTOM_TIME_RANGE_BADGE;
public readonly id = CUSTOM_TIME_RANGE_BADGE;
public order = 7;
private openModal: OpenModal;
private dateFormat: string;
private commonlyUsedRanges: CommonlyUsedRange[];
constructor({
openModal,
dateFormat,
commonlyUsedRanges,
}: {
openModal: OpenModal;
dateFormat: string;
commonlyUsedRanges: CommonlyUsedRange[];
}) {
this.openModal = openModal;
this.dateFormat = dateFormat;
this.commonlyUsedRanges = commonlyUsedRanges;
}
public getDisplayName({ embeddable }: TimeBadgeActionContext) {
return renderToString(
<PrettyDuration
timeFrom={embeddable.getInput().timeRange.from}
timeTo={embeddable.getInput().timeRange.to}
dateFormat={this.dateFormat}
/>
);
}
public getIconType() {
return 'calendar';
}
public async isCompatible({ embeddable }: TimeBadgeActionContext) {
return Boolean(embeddable && hasTimeRange(embeddable) && !doesInheritTimeRange(embeddable));
}
public async execute({ embeddable }: TimeBadgeActionContext) {
const isCompatible = await this.isCompatible({ embeddable });
if (!isCompatible) {
throw new IncompatibleActionError();
}
// Only here for typescript
if (hasTimeRange(embeddable)) {
const CustomizeTimeRangeModal = await import('./customize_time_range_modal').then(
(m) => m.CustomizeTimeRangeModal
);
const modalSession = this.openModal(
<CustomizeTimeRangeModal
onClose={() => modalSession.close()}
embeddable={embeddable}
dateFormat={this.dateFormat}
commonlyUsedRanges={this.commonlyUsedRanges}
/>,
{
'data-test-subj': 'customizeTimeRangeModal',
}
);
}
}
}

View file

@ -1,30 +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 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 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { render, screen } from '@testing-library/react';
import { CustomizeTimeRangeModal } from './customize_time_range_modal';
import { Embeddable } from '@kbn/embeddable-plugin/public';
import { TimeRangeInput } from './custom_time_range_action';
test("Doesn't display refresh interval options", () => {
render(
<CustomizeTimeRangeModal
embeddable={
{
getInput: () => ({ timerange: { from: 'now-7d', to: 'now' } }),
} as unknown as Embeddable<TimeRangeInput>
}
onClose={() => {}}
commonlyUsedRanges={[]}
/>
);
expect(screen.getByTestId('superDatePickerToggleQuickMenuButton')).toBeInTheDocument();
expect(screen.queryByTitle(/auto refresh/gi)).not.toBeInTheDocument();
});

View file

@ -1,186 +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 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 or the Server
* Side Public License, v 1.
*/
import React, { Component } from 'react';
import {
EuiFormRow,
EuiButton,
EuiButtonEmpty,
EuiModalHeader,
EuiModalFooter,
EuiModalBody,
EuiModalHeaderTitle,
EuiSuperDatePicker,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type { TimeRange } from '@kbn/es-query';
import { Embeddable, IContainer, ContainerInput } from '@kbn/embeddable-plugin/public';
import { TimeRangeInput } from './custom_time_range_action';
import { doesInheritTimeRange } from './does_inherit_time_range';
import { CommonlyUsedRange } from './types';
interface CustomizeTimeRangeProps {
embeddable: Embeddable<TimeRangeInput>;
onClose: () => void;
dateFormat?: string;
commonlyUsedRanges: CommonlyUsedRange[];
}
interface State {
timeRange?: TimeRange;
inheritTimeRange: boolean;
}
export class CustomizeTimeRangeModal extends Component<CustomizeTimeRangeProps, State> {
constructor(props: CustomizeTimeRangeProps) {
super(props);
this.state = {
timeRange: props.embeddable.getInput().timeRange,
inheritTimeRange: doesInheritTimeRange(props.embeddable),
};
}
onTimeChange = ({ start, end }: { start: string; end: string }) => {
this.setState({ timeRange: { from: start, to: end } });
};
cancel = () => {
this.props.onClose();
};
onInheritToggle = () => {
this.setState((prevState) => ({
inheritTimeRange: !prevState.inheritTimeRange,
}));
};
addToPanel = () => {
const { embeddable } = this.props;
embeddable.updateInput({ timeRange: this.state.timeRange });
this.props.onClose();
};
inheritFromParent = () => {
const { embeddable } = this.props;
const parent = embeddable.parent as IContainer<{}, ContainerInput<TimeRangeInput>>;
const parentPanels = parent!.getInput().panels;
// Remove explicit input to this child from the parent.
parent!.updateInput({
panels: {
...parentPanels,
[embeddable.id]: {
...parentPanels[embeddable.id],
explicitInput: {
...parentPanels[embeddable.id].explicitInput,
timeRange: undefined,
},
},
},
});
this.props.onClose();
};
public render() {
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle data-test-subj="customizePanelTitle">
{i18n.translate('uiActionsEnhanced.customizeTimeRange.modal.headerTitle', {
defaultMessage: 'Customize panel time range',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody data-test-subj="customizePanelBody">
<EuiFormRow
label={i18n.translate(
'uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel',
{
defaultMessage: 'Time range',
}
)}
>
<EuiSuperDatePicker
start={this.state.timeRange ? this.state.timeRange.from : undefined}
end={this.state.timeRange ? this.state.timeRange.to : undefined}
onTimeChange={this.onTimeChange}
showUpdateButton={false}
dateFormat={this.props.dateFormat}
commonlyUsedRanges={this.props.commonlyUsedRanges.map(
({ from, to, display }: { from: string; to: string; display: string }) => {
return {
start: from,
end: to,
label: display,
};
}
)}
data-test-subj="customizePanelTimeRangeDatePicker"
/>
</EuiFormRow>
</EuiModalBody>
<EuiModalFooter data-test-subj="customizePanelFooter">
<EuiFlexGroup gutterSize="s" responsive={false} justifyContent="spaceBetween">
<EuiFlexItem grow={true}>
<div>
<EuiButtonEmpty
onClick={this.inheritFromParent}
color="danger"
data-test-subj="removePerPanelTimeRangeButton"
disabled={!this.props.embeddable.parent || this.state.inheritTimeRange}
flush="left"
>
{i18n.translate(
'uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle',
{
defaultMessage: 'Remove',
}
)}
</EuiButtonEmpty>
</div>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={this.cancel} data-test-subj="cancelPerPanelTimeRangeButton">
{i18n.translate(
'uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle',
{
defaultMessage: 'Cancel',
}
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton data-test-subj="addPerPanelTimeRangeButton" onClick={this.addToPanel} fill>
{this.state.inheritTimeRange
? i18n.translate(
'uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle',
{
defaultMessage: 'Add to panel',
}
)
: i18n.translate(
'uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle',
{
defaultMessage: 'Update',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</React.Fragment>
);
}
}

View file

@ -8,20 +8,10 @@
import { BehaviorSubject, Subscription } from 'rxjs';
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { createReactOverlays } from '@kbn/kibana-react-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/public';
import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import {
CONTEXT_MENU_TRIGGER,
PANEL_BADGE_TRIGGER,
EmbeddableSetup,
EmbeddableStart,
} from '@kbn/embeddable-plugin/public';
import { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { ILicense, LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { createStartServicesGetter, Storage } from '@kbn/kibana-utils-plugin/public';
import { CustomTimeRangeAction } from './custom_time_range_action';
import { CustomTimeRangeBadge } from './custom_time_range_badge';
import { CommonlyUsedRange } from './types';
import { UiActionsServiceEnhancements } from './services';
import { createPublicDrilldownManager, PublicDrilldownManagerComponent } from './drilldowns';
import { dynamicActionEnhancement } from './dynamic_actions/dynamic_action_enhancement';
@ -93,25 +83,6 @@ export class AdvancedUiActionsPublicPlugin
public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract {
if (licensing) this.subs.push(licensing.license$.subscribe(this.licenseInfo));
const dateFormat = core.uiSettings.get('dateFormat') as string;
const commonlyUsedRanges = core.uiSettings.get(
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
) as CommonlyUsedRange[];
const { openModal } = createReactOverlays(core);
const timeRangeAction = new CustomTimeRangeAction({
openModal,
dateFormat,
commonlyUsedRanges,
});
uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, timeRangeAction);
const timeRangeBadge = new CustomTimeRangeBadge({
openModal,
dateFormat,
commonlyUsedRanges,
});
uiActions.addTriggerAction(PANEL_BADGE_TRIGGER, timeRangeBadge);
return {
...uiActions,
...this.enhancements!,

View file

@ -1,10 +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 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 or the Server
* Side Public License, v 1.
*/
export { TimeRangeEmbeddable, TIME_RANGE_EMBEDDABLE } from './time_range_embeddable';
export { TimeRangeContainer } from './time_range_container';

View file

@ -17,8 +17,6 @@
"@kbn/kibana-utils-plugin",
"@kbn/ui-actions-plugin",
"@kbn/licensing-plugin",
"@kbn/es-query",
"@kbn/test-jest-helpers",
"@kbn/i18n",
"@kbn/utility-types",
"@kbn/i18n-react",

View file

@ -146,6 +146,7 @@ export class VisualizeEmbeddable
initialInput,
{
defaultTitle: vis.title,
defaultDescription: vis.description,
editPath,
editApp: 'visualize',
editUrl,
@ -196,10 +197,6 @@ export class VisualizeEmbeddable
return true;
}
public getDescription() {
return this.vis.description;
}
public getVis() {
return this.vis;
}

View file

@ -14,6 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const security = getService('security');
const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']);
@ -74,7 +75,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('data-shared-item title should update a viz when using a custom panel title', async () => {
await PageObjects.dashboard.switchToEditMode();
const CUSTOM_VIS_TITLE = 'ima custom title for a vis!';
await dashboardPanelActions.setCustomPanelTitle(CUSTOM_VIS_TITLE);
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutOpen();
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_VIS_TITLE);
await dashboardCustomizePanel.clickSaveButton();
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutClosed();
await retry.try(async () => {
const sharedData = await PageObjects.dashboard.getPanelSharedItemData();
const foundSharedItemTitle = !!sharedData.find((item) => {
@ -85,7 +91,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('data-shared-item title is cleared with an empty panel title string', async () => {
await dashboardPanelActions.toggleHidePanelTitle();
const toggleHideTitle = async () => {
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutOpen();
await dashboardCustomizePanel.clickToggleHidePanelTitle();
await dashboardCustomizePanel.clickSaveButton();
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutClosed();
};
await toggleHideTitle();
await retry.try(async () => {
const sharedData = await PageObjects.dashboard.getPanelSharedItemData();
const foundSharedItemTitle = !!sharedData.find((item) => {
@ -93,11 +107,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
expect(foundSharedItemTitle).to.be(true);
});
await dashboardPanelActions.toggleHidePanelTitle();
await toggleHideTitle();
});
it('data-shared-item title can be reset', async () => {
await dashboardPanelActions.resetCustomPanelTitle();
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.resetCustomPanelTitle();
await dashboardCustomizePanel.clickSaveButton();
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutClosed();
await retry.try(async () => {
const sharedData = await PageObjects.dashboard.getPanelSharedItemData();
const foundOriginalSharedItemTitle = !!sharedData.find((item) => {
@ -108,11 +127,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('data-shared-item title should update a saved search when using a custom panel title', async () => {
await PageObjects.dashboard.switchToEditMode();
const CUSTOM_SEARCH_TITLE = 'ima custom title for a search!';
await dashboardPanelActions.setCustomPanelTitle(
CUSTOM_SEARCH_TITLE,
'Rendering Test: saved search'
);
const el = await dashboardPanelActions.getPanelHeading('Rendering Test: saved search');
await dashboardPanelActions.customizePanel(el);
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutOpen();
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_SEARCH_TITLE);
await dashboardCustomizePanel.clickSaveButton();
await dashboardCustomizePanel.expectCustomizePanelSettingsFlyoutClosed();
await retry.try(async () => {
const sharedData = await PageObjects.dashboard.getPanelSharedItemData();
const foundSharedItemTitle = !!sharedData.find((item) => {

View file

@ -40,6 +40,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const PageObjects = getPageObjects(['dashboard', 'common', 'share', 'timePicker']);
@ -128,7 +129,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should have "panels" in app state when a panel has been modified', async () => {
await dashboardPanelActions.setCustomPanelTitle('Test New Title');
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle('Test New Title');
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.waitForRenderComplete();
await testSubjects.existOrFail('dashboardUnsavedChangesBadge');

View file

@ -11,5 +11,7 @@ export { DashboardExpectService } from './expectations';
export { DashboardAddPanelService } from './add_panel';
export { DashboardReplacePanelService } from './replace_panel';
export { DashboardPanelActionsService } from './panel_actions';
export { DashboardCustomizePanelProvider } from './panel_settings';
export { DashboardBadgeActionsProvider } from './panel_badge_actions';
export { DashboardDrilldownPanelActionsProvider } from './panel_drilldown_actions';
export { DashboardDrilldownsManageProvider } from './drilldowns_manage';

View file

@ -303,49 +303,6 @@ export class DashboardPanelActionsService extends FtrService {
return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`);
}
async clickHidePanelTitleToggle() {
await this.testSubjects.click('customizePanelHideTitle');
}
async toggleHidePanelTitle(originalTitle?: string) {
this.log.debug(`hidePanelTitle(${originalTitle})`);
if (originalTitle) {
const panelOptions = await this.getPanelHeading(originalTitle);
await this.customizePanel(panelOptions);
} else {
await this.customizePanel();
}
await this.clickHidePanelTitleToggle();
await this.testSubjects.click('saveNewTitleButton');
}
/**
*
* @param customTitle
* @param originalTitle - optional to specify which panel to change the title on.
* @return {Promise<void>}
*/
async setCustomPanelTitle(customTitle: string, originalTitle?: string) {
this.log.debug(`setCustomPanelTitle(${customTitle}, ${originalTitle})`);
if (originalTitle) {
const panelOptions = await this.getPanelHeading(originalTitle);
await this.customizePanel(panelOptions);
} else {
await this.customizePanel();
}
await this.testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle, {
clearWithKeyboard: customTitle === '', // if clearing the title using the empty string as the new value, 'clearWithKeyboard' must be true; otherwise, false
});
await this.testSubjects.click('saveNewTitleButton');
}
async resetCustomPanelTitle(panel?: WebElementWrapper) {
this.log.debug('resetCustomPanelTitle');
await this.customizePanel(panel);
await this.testSubjects.click('resetCustomEmbeddablePanelTitle');
await this.testSubjects.click('saveNewTitleButton');
}
async getActionWebElementByText(text: string): Promise<WebElementWrapper> {
this.log.debug(`getActionWebElement: "${text}"`);
const menu = await this.testSubjects.find('multipleActionsContextMenu');

View file

@ -0,0 +1,34 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
const CUSTOM_TIME_RANGE_BADGE_DATA_TEST_SUBJ = 'embeddablePanelBadge-CUSTOM_TIME_RANGE_BADGE';
export function DashboardBadgeActionsProvider({ getService }: FtrProviderContext) {
const log = getService('log');
const testSubjects = getService('testSubjects');
return new (class DashboardBadgeActions {
async expectExistsTimeRangeBadgeAction() {
log.debug('expectExistsTimeRangeBadgeAction');
await testSubjects.existOrFail(CUSTOM_TIME_RANGE_BADGE_DATA_TEST_SUBJ);
}
async expectMissingTimeRangeBadgeAction() {
log.debug('expectMissingTimeRangeBadgeAction');
await testSubjects.missingOrFail(CUSTOM_TIME_RANGE_BADGE_DATA_TEST_SUBJ);
}
async clickTimeRangeBadgeAction() {
log.debug('clickTimeRangeBadgeAction');
await this.expectExistsTimeRangeBadgeAction();
await testSubjects.click(CUSTOM_TIME_RANGE_BADGE_DATA_TEST_SUBJ);
}
})();
}

View file

@ -0,0 +1,111 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { CommonlyUsed } from '../../page_objects/time_picker';
export function DashboardCustomizePanelProvider({ getService }: FtrProviderContext) {
const log = getService('log');
const testSubjects = getService('testSubjects');
return new (class DashboardCustomizePanel {
public readonly FLYOUT_TEST_SUBJ = 'customizePanel';
public readonly TOGGLE_TIME_RANGE_TEST_SUBJ = 'customizePanelShowCustomTimeRange';
async expectCustomizePanelSettingsFlyoutOpen() {
log.debug('expectCustomizePanelSettingsFlyoutOpen');
await testSubjects.existOrFail(this.FLYOUT_TEST_SUBJ);
}
async expectCustomizePanelSettingsFlyoutClosed() {
log.debug('expectCustomizePanelSettingsFlyoutClosed');
await testSubjects.missingOrFail(this.FLYOUT_TEST_SUBJ);
}
async expectExistsCustomTimeRange() {
log.debug('expectExistsCustomTimeRange');
await testSubjects.existOrFail(this.TOGGLE_TIME_RANGE_TEST_SUBJ);
}
async expectMissingCustomTimeRange() {
log.debug('expectMissingCustomTimeRange');
await testSubjects.missingOrFail(this.TOGGLE_TIME_RANGE_TEST_SUBJ);
}
public async findFlyout() {
log.debug('findFlyout');
return await testSubjects.find(this.FLYOUT_TEST_SUBJ);
}
public async findFlyoutTestSubject(testSubject: string) {
log.debug('findFlyoutTestSubject');
const flyout = await this.findFlyout();
return await flyout.findByCssSelector(`[data-test-subj="${testSubject}"]`);
}
public async findToggleQuickMenuButton() {
log.debug('findToggleQuickMenuButton');
return await this.findFlyoutTestSubject('superDatePickerToggleQuickMenuButton');
}
public async clickToggleQuickMenuButton() {
log.debug('clickToggleQuickMenuButton');
const button = await this.findToggleQuickMenuButton();
await button.click();
}
public async clickCommonlyUsedTimeRange(time: CommonlyUsed) {
log.debug('clickCommonlyUsedTimeRange', time);
await testSubjects.click(`superDatePickerCommonlyUsed_${time}`);
}
public async clickToggleHidePanelTitle() {
log.debug('clickToggleHidePanelTitle');
await testSubjects.click('customEmbeddablePanelHideTitleSwitch');
}
public async setCustomPanelTitle(customTitle: string) {
log.debug('setCustomPanelTitle');
await testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle, {
clearWithKeyboard: customTitle === '', // if clearing the title using the empty string as the new value, 'clearWithKeyboard' must be true; otherwise, false
});
}
public async resetCustomPanelTitle() {
log.debug('resetCustomPanelTitle');
await testSubjects.click('resetCustomEmbeddablePanelTitleButton');
}
public async setCustomPanelDescription(customDescription: string) {
log.debug('setCustomPanelDescription');
await testSubjects.setValue('customEmbeddablePanelDescriptionInput', customDescription, {
clearWithKeyboard: customDescription === '', // if clearing the description using the empty string as the new value, 'clearWithKeyboard' must be true; otherwise, false
});
}
public async resetCustomPanelDescription() {
log.debug('resetCustomPanelDescription');
await testSubjects.click('resetCustomEmbeddablePanelDescriptionButton');
}
public async clickSaveButton() {
log.debug('clickSaveButton');
await testSubjects.click('saveCustomizePanelButton');
}
public async clickCancelButton() {
log.debug('clickCancelButton');
await testSubjects.click('cancelCustomizePanelButton');
}
public async clickToggleShowCustomTimeRange() {
log.debug('clickToggleShowCustomTimeRange');
await testSubjects.click(this.TOGGLE_TIME_RANGE_TEST_SUBJ);
}
})();
}

View file

@ -25,6 +25,8 @@ import {
DashboardReplacePanelService,
DashboardExpectService,
DashboardPanelActionsService,
DashboardCustomizePanelProvider,
DashboardBadgeActionsProvider,
DashboardVisualizationsService,
DashboardDrilldownPanelActionsProvider,
DashboardDrilldownsManageProvider,
@ -73,6 +75,8 @@ export const services = {
dashboardAddPanel: DashboardAddPanelService,
dashboardReplacePanel: DashboardReplacePanelService,
dashboardPanelActions: DashboardPanelActionsService,
dashboardCustomizePanel: DashboardCustomizePanelProvider,
dashboardBadgeActions: DashboardBadgeActionsProvider,
dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider,
dashboardDrilldownsManage: DashboardDrilldownsManageProvider,
flyout: FlyoutService,

View file

@ -1170,11 +1170,14 @@ export class Embeddable
}
const title = input.hidePanelTitles ? '' : input.title ?? this.savedVis.title;
const description = input.hidePanelTitles ? '' : input.description ?? this.savedVis.description;
const savedObjectId = (input as LensByReferenceInput).savedObjectId;
this.updateOutput({
defaultTitle: this.savedVis.title,
defaultDescription: this.savedVis.description,
editable: this.getIsEditable(),
title,
description,
editPath: getEditPath(savedObjectId),
editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`),
indexPatterns: this.dataViews,
@ -1205,12 +1208,6 @@ export class Embeddable
return this.deps.attributeService.getInputAsValueType(this.getExplicitInput());
};
// same API as Visualize
public getDescription() {
// mind that savedViz is loaded in async way here
return this.savedVis && this.savedVis.description;
}
/**
* Gets the Lens embeddable's local filters
* @returns Local/panel-level array of filters for Lens embeddable

View file

@ -218,15 +218,15 @@ export class MapEmbeddable
}
private async _initializeOutput() {
const savedMapTitle = this._savedMap.getAttributes()?.title
? this._savedMap.getAttributes().title
: '';
const { title: savedMapTitle, description: savedMapDescription } =
this._savedMap.getAttributes();
const input = this.getInput();
const title = input.hidePanelTitles ? '' : input.title ?? savedMapTitle;
const savedObjectId = 'savedObjectId' in input ? input.savedObjectId : undefined;
this.updateOutput({
...this.getOutput(),
defaultTitle: savedMapTitle,
defaultDescription: savedMapDescription,
title,
editPath: getEditPath(savedObjectId),
editUrl: getHttp().basePath.prepend(getFullPath(savedObjectId)),
@ -267,10 +267,6 @@ export class MapEmbeddable
return getLayerList(this._savedMap.getStore().getState());
}
public getDescription() {
return this._isInitialized ? this._savedMap.getAttributes().description : '';
}
public async getFilters() {
const embeddableSearchContext = getEmbeddableSearchContext(
this._savedMap.getStore().getState()

View file

@ -48,6 +48,7 @@ export class AnomalyChartsEmbeddable extends Embeddable<
initialInput,
{
defaultTitle: initialInput.title,
defaultDescription: initialInput.description,
},
parent
);

View file

@ -49,6 +49,7 @@ export class AnomalySwimlaneEmbeddable extends Embeddable<
initialInput,
{
defaultTitle: initialInput.title,
defaultDescription: initialInput.description,
},
parent
);

View file

@ -2383,15 +2383,6 @@
"embeddableApi.addPanel.Title": "Ajouter depuis la bibliothèque",
"embeddableApi.contextMenuTrigger.title": "Menu contextuel",
"embeddableApi.customizePanel.action.displayName": "Modifier le titre du panneau",
"embeddableApi.customizePanel.modal.cancel": "Annuler",
"embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel": "Titre du panneau",
"embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel": "Entrez un titre personnalisé pour le panneau.",
"embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel": "Réinitialiser",
"embeddableApi.customizePanel.modal.saveButtonTitle": "Enregistrer",
"embeddableApi.customizePanel.modal.showTitle": "Afficher le titre du panneau",
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleFormRowLabel": "Titre du panneau",
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel": "Les modifications apportées à cette entrée sont appliquées immédiatement. Appuyez sur Entrée pour quitter.",
"embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel": "Réinitialiser le titre",
"embeddableApi.errors.paneldoesNotExist": "Panneau introuvable",
"embeddableApi.helloworld.displayName": "bonjour",
"embeddableApi.panel.dashboardPanelAriaLabel": "Panneau du tableau de bord",
@ -5032,13 +5023,6 @@
"uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "Ce type de déclenchement n'est pas pris en charge par ce panneau",
"uiActionsEnhanced.components.TriggerPickerItem.unknown": "Inconnu",
"uiActionsEnhanced.CustomActions": "Actions personnalisées",
"uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "Ajouter au panneau",
"uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "Annuler",
"uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "Plage temporelle",
"uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "Retirer",
"uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "Mettre à jour",
"uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "Personnaliser la plage temporelle du panneau",
"uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "Personnaliser la plage temporelle",
"uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "Copier la recherche existante",
"uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "Les recherches vous permettent de définir de nouveaux comportements pour l'interaction avec les panneaux. Vous pouvez ajouter plusieurs actions et remplacer le filtre par défaut.",
"uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "Masquer",

View file

@ -2381,15 +2381,6 @@
"embeddableApi.addPanel.Title": "ライブラリから追加",
"embeddableApi.contextMenuTrigger.title": "コンテキストメニュー",
"embeddableApi.customizePanel.action.displayName": "パネルタイトルを編集",
"embeddableApi.customizePanel.modal.cancel": "キャンセル",
"embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel": "パネルタイトル",
"embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel": "パネルのカスタムタイトルを入力してください",
"embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel": "リセット",
"embeddableApi.customizePanel.modal.saveButtonTitle": "保存",
"embeddableApi.customizePanel.modal.showTitle": "パネルタイトルを表示",
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleFormRowLabel": "パネルタイトル",
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel": "このインプットへの変更は直ちに適用されます。Enter を押して閉じます。",
"embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel": "タイトルをリセット",
"embeddableApi.errors.paneldoesNotExist": "パネルが見つかりません",
"embeddableApi.helloworld.displayName": "こんにちは",
"embeddableApi.panel.dashboardPanelAriaLabel": "ダッシュボードパネル",
@ -5029,13 +5020,6 @@
"uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "このトリガータイプはこのパネルでサポートされていません",
"uiActionsEnhanced.components.TriggerPickerItem.unknown": "不明",
"uiActionsEnhanced.CustomActions": "カスタムアクション",
"uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "パネルに追加",
"uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "キャンセル",
"uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "時間範囲",
"uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "削除",
"uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新",
"uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "パネルの時間範囲のカスタマイズ",
"uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "時間範囲のカスタマイズ",
"uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "既存のドリルダウンをコピー",
"uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "ドリルダウンにより、パネルと連携する新しい動作を定義できます。複数のアクションを追加し、デフォルトフィルターを無効化できます。",
"uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "非表示",

View file

@ -2385,15 +2385,6 @@
"embeddableApi.addPanel.Title": "从库中添加",
"embeddableApi.contextMenuTrigger.title": "上下文菜单",
"embeddableApi.customizePanel.action.displayName": "编辑面板标题",
"embeddableApi.customizePanel.modal.cancel": "取消",
"embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel": "面板标题",
"embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel": "为面板输入定制标题",
"embeddableApi.customizePanel.modal.optionsMenuForm.resetCustomDashboardButtonLabel": "重置",
"embeddableApi.customizePanel.modal.saveButtonTitle": "保存",
"embeddableApi.customizePanel.modal.showTitle": "显示面板标题",
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleFormRowLabel": "面板标题",
"embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel": "对此输入的更改将立即应用。按 Enter 键可退出。",
"embeddableApi.customizeTitle.optionsMenuForm.resetCustomDashboardButtonLabel": "重置标题",
"embeddableApi.errors.paneldoesNotExist": "未找到面板",
"embeddableApi.helloworld.displayName": "hello world",
"embeddableApi.panel.dashboardPanelAriaLabel": "仪表板面板",
@ -5035,13 +5026,6 @@
"uiActionsEnhanced.components.TriggerLineItem.incompatibleTooltip": "此触发类型不受此面板支持",
"uiActionsEnhanced.components.TriggerPickerItem.unknown": "未知",
"uiActionsEnhanced.CustomActions": "定制操作",
"uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle": "添加到面板",
"uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle": "取消",
"uiActionsEnhanced.customizePanelTimeRange.modal.optionsMenuForm.panelTitleFormRowLabel": "时间范围",
"uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle": "移除",
"uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle": "更新",
"uiActionsEnhanced.customizeTimeRange.modal.headerTitle": "定制面板时间范围",
"uiActionsEnhanced.customizeTimeRangeMenuItem.displayName": "定制时间范围",
"uiActionsEnhanced.drilldownManager.containers.TemplatePicker.label": "复制现有向下钻取",
"uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.helpText": "向下钻取允许您定义与面板交互的新行为。您可以添加多个操作并覆盖默认筛选。",
"uiActionsEnhanced.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel": "隐藏",

View file

@ -113,10 +113,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardPanelActions.toggleContextMenu(header);
await dashboardPanelActions.customizePanel();
await a11y.testAppSnapshot();
await testSubjects.click('customizePanelHideTitle');
await testSubjects.click('customEmbeddablePanelHideTitleSwitch');
await a11y.testAppSnapshot();
await testSubjects.click('customizePanelHideTitle');
await testSubjects.click('saveNewTitleButton');
await testSubjects.click('customEmbeddablePanelHideTitleSwitch');
await testSubjects.click('saveCustomizePanelButton');
});
it('dashboard panel - Create drilldown panel', async () => {

View file

@ -14,6 +14,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_lens_by_value'));
loadTestFile(require.resolve('./dashboard_maps_by_value'));
loadTestFile(require.resolve('./panel_titles'));
loadTestFile(require.resolve('./panel_time_range'));
loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test'));
loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test'));

View file

@ -0,0 +1,105 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardBadgeActions = getService('dashboardBadgeActions');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const dashboardAddPanel = getService('dashboardAddPanel');
const PageObjects = getPageObjects([
'common',
'dashboard',
'visualize',
'visEditor',
'timePicker',
'lens',
]);
const DASHBOARD_NAME = 'Custom panel time range test';
describe('custom time range', () => {
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
await kibanaServer.importExport.load(
'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json'
);
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.preserveCrossAppState();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.saveDashboard(DASHBOARD_NAME);
});
describe('by value', () => {
it('can add a custom time range to a panel', async () => {
await PageObjects.lens.createAndAddLensFromDashboard({});
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickToggleQuickMenuButton();
await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_30 days');
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.waitForRenderComplete();
await dashboardBadgeActions.expectExistsTimeRangeBadgeAction();
expect(await testSubjects.exists('emptyPlaceholder'));
await PageObjects.dashboard.clickQuickSave();
});
it('can remove a custom time range from a panel', async () => {
await dashboardBadgeActions.clickTimeRangeBadgeAction();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.waitForRenderComplete();
await dashboardBadgeActions.expectMissingTimeRangeBadgeAction();
expect(await testSubjects.exists('xyVisChart'));
});
});
describe('by reference', () => {
it('can add a custom time range to panel', async () => {
await dashboardPanelActions.saveToLibrary('My by reference visualization');
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickToggleQuickMenuButton();
await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_30 days');
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.waitForRenderComplete();
await dashboardBadgeActions.expectExistsTimeRangeBadgeAction();
expect(await testSubjects.exists('emptyPlaceholder'));
await PageObjects.dashboard.clickQuickSave();
});
it('can remove a custom time range from a panel', async () => {
await dashboardBadgeActions.clickTimeRangeBadgeAction();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.waitForRenderComplete();
await dashboardBadgeActions.expectMissingTimeRangeBadgeAction();
expect(await testSubjects.exists('xyVisChart'));
});
});
describe('embeddable that does not support time', () => {
it('should not show custom time picker in flyout', async () => {
await dashboardPanelActions.removePanel();
await PageObjects.dashboard.waitForRenderComplete();
await dashboardAddPanel.clickMarkdownQuickButton();
await PageObjects.visEditor.setMarkdownTxt('I am timeless!');
await PageObjects.visEditor.clickGo();
await PageObjects.visualize.saveVisualizationAndReturn();
await PageObjects.dashboard.clickQuickSave();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.expectMissingCustomTimeRange();
});
});
});
}

View file

@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const dashboardPanelActions = getService('dashboardPanelActions');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const PageObjects = getPageObjects([
'common',
'dashboard',
@ -49,12 +50,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('saving new panel with blank title clears "unsaved changes" badge', async () => {
await dashboardPanelActions.setCustomPanelTitle('');
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle('');
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.clearUnsavedChanges();
});
it('custom title causes unsaved changes and saving clears it', async () => {
await dashboardPanelActions.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardCustomizePanel.clickSaveButton();
const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
expect(panelTitle).to.equal(CUSTOM_TITLE);
await PageObjects.dashboard.clearUnsavedChanges();
@ -62,9 +67,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('resetting title on a by value panel sets it to the empty string', async () => {
const BY_VALUE_TITLE = 'Reset Title - By Value';
await dashboardPanelActions.setCustomPanelTitle(BY_VALUE_TITLE);
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle(BY_VALUE_TITLE);
await dashboardCustomizePanel.clickSaveButton();
await dashboardPanelActions.resetCustomPanelTitle();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.resetCustomPanelTitle();
await dashboardCustomizePanel.clickSaveButton();
const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
expect(panelTitle).to.equal(EMPTY_TITLE);
await PageObjects.dashboard.clearUnsavedChanges();
@ -79,7 +88,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('custom titles are visible in view mode', async () => {
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.clickQuickSave();
await PageObjects.dashboard.clickCancelOutOfEditMode();
@ -89,7 +100,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('hiding an individual panel title hides it in view mode', async () => {
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.toggleHidePanelTitle();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.clickToggleHidePanelTitle();
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.clickQuickSave();
await PageObjects.dashboard.clickCancelOutOfEditMode();
@ -98,14 +111,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// undo the previous hide panel toggle (i.e. make the panel visible) to keep state consistent
await PageObjects.dashboard.switchToEditMode();
await dashboardPanelActions.toggleHidePanelTitle();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.clickToggleHidePanelTitle();
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.dashboard.clickQuickSave();
});
});
describe('by reference', () => {
it('linking a by value panel with a custom title to the library will overwrite the custom title with the library title', async () => {
await dashboardPanelActions.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardCustomizePanel.clickSaveButton();
await dashboardPanelActions.saveToLibrary(LIBRARY_TITLE_FOR_CUSTOM_TESTS);
await retry.try(async () => {
// need to surround in 'retry' due to delays in HTML updates causing the title read to be behind
@ -115,21 +132,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('resetting title on a by reference panel sets it to the library title', async () => {
await dashboardPanelActions.setCustomPanelTitle('This should go away');
await dashboardPanelActions.resetCustomPanelTitle();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle('This should go away');
await dashboardCustomizePanel.clickSaveButton();
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.resetCustomPanelTitle();
await dashboardCustomizePanel.clickSaveButton();
const resetPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
expect(resetPanelTitle).to.equal(LIBRARY_TITLE_FOR_CUSTOM_TESTS);
});
it('unlinking a by reference panel with a custom title will keep the current title', async () => {
await dashboardPanelActions.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle(CUSTOM_TITLE);
await dashboardCustomizePanel.clickSaveButton();
await dashboardPanelActions.unlinkFromLibary();
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0];
expect(newPanelTitle).to.equal(CUSTOM_TITLE);
});
it("linking a by value panel with a blank title to the library will set the panel's title to the library title", async () => {
await dashboardPanelActions.setCustomPanelTitle('');
await dashboardPanelActions.customizePanel();
await dashboardCustomizePanel.setCustomPanelTitle('');
await dashboardCustomizePanel.clickSaveButton();
await dashboardPanelActions.saveToLibrary(LIBRARY_TITLE_FOR_EMPTY_TESTS);
await retry.try(async () => {
// need to surround in 'retry' due to delays in HTML updates causing the title read to be behind

View file

@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'timePicker',
]);
const panelActions = getService('dashboardPanelActions');
const panelActionsTimeRange = getService('dashboardPanelTimeRange');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
@ -44,9 +44,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
after('clean-up custom time range on panel', async () => {
await common.navigateToApp('dashboard');
await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
await panelActions.openContextMenuMorePanel();
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton();
await panelActions.customizePanel();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickSaveButton();
await dashboard.saveDashboard('Dashboard with Pie Chart');
});
@ -78,11 +79,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME);
await panelActions.openContextMenuMorePanel();
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
await panelActionsTimeRange.clickToggleQuickMenuButton();
await panelActionsTimeRange.clickCommonlyUsedTimeRange('Last_90 days');
await panelActionsTimeRange.clickModalPrimaryButton();
await panelActions.customizePanel();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickToggleQuickMenuButton();
await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_90 days');
await dashboardCustomizePanel.clickSaveButton();
await dashboard.saveDashboard('Dashboard with Pie Chart');

View file

@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const dashboardAddPanel = getService('dashboardAddPanel');
const dataGrid = getService('dataGrid');
const panelActions = getService('dashboardPanelActions');
const panelActionsTimeRange = getService('dashboardPanelTimeRange');
const dashboardCustomizePanel = getService('dashboardCustomizePanel');
const queryBar = getService('queryBar');
const filterBar = getService('filterBar');
const ecommerceSOPath = 'x-pack/test/functional/fixtures/kbn_archiver/reporting/ecommerce.json';
@ -51,11 +51,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardAddPanel.clickOpenAddPanel();
await dashboardAddPanel.addSavedSearch('Ecommerce Data');
expect(await dataGrid.getDocCount()).to.be(500);
await panelActions.openContextMenuMorePanel();
await panelActionsTimeRange.clickTimeRangeActionInContextMenu();
await panelActionsTimeRange.clickToggleQuickMenuButton();
await panelActionsTimeRange.clickCommonlyUsedTimeRange('Last_90 days');
await panelActionsTimeRange.clickModalPrimaryButton();
await panelActions.customizePanel();
await dashboardCustomizePanel.clickToggleShowCustomTimeRange();
await dashboardCustomizePanel.clickToggleQuickMenuButton();
await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_90 days');
await dashboardCustomizePanel.clickSaveButton();
await PageObjects.header.waitUntilLoadingHasFinished();
expect(await dataGrid.hasNoResults()).to.be(true);
});

View file

@ -1,8 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { DashboardPanelTimeRangeProvider } from './panel_time_range';

View file

@ -1,63 +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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { CommonlyUsed } from '../../../../../test/functional/page_objects/time_picker';
export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderContext) {
const log = getService('log');
const testSubjects = getService('testSubjects');
return new (class DashboardPanelTimeRange {
public readonly MODAL_TEST_SUBJ = 'customizeTimeRangeModal';
public readonly CUSTOM_TIME_RANGE_ACTION = 'CUSTOM_TIME_RANGE';
public async clickTimeRangeActionInContextMenu() {
log.debug('clickTimeRangeActionInContextMenu');
await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE');
}
public async findModal() {
log.debug('findModal');
return await testSubjects.find(this.MODAL_TEST_SUBJ);
}
public async findModalTestSubject(testSubject: string) {
log.debug('findModalElement');
const modal = await this.findModal();
return await modal.findByCssSelector(`[data-test-subj="${testSubject}"]`);
}
public async findToggleQuickMenuButton() {
log.debug('findToggleQuickMenuButton');
return await this.findModalTestSubject('superDatePickerToggleQuickMenuButton');
}
public async clickToggleQuickMenuButton() {
log.debug('clickToggleQuickMenuButton');
const button = await this.findToggleQuickMenuButton();
await button.click();
}
public async clickCommonlyUsedTimeRange(time: CommonlyUsed) {
log.debug('clickCommonlyUsedTimeRange', time);
await testSubjects.click(`superDatePickerCommonlyUsed_${time}`);
}
public async clickModalPrimaryButton() {
log.debug('clickModalPrimaryButton');
const button = await this.findModalTestSubject('addPerPanelTimeRangeButton');
await button.click();
}
public async clickRemovePerPanelTimeRangeButton() {
log.debug('clickRemovePerPanelTimeRangeButton');
const button = await this.findModalTestSubject('removePerPanelTimeRangeButton');
await button.click();
}
})();
}

View file

@ -61,7 +61,6 @@ import { InfraSourceConfigurationFormProvider } from './infra_source_configurati
import { LogsUiProvider } from './logs_ui';
import { MachineLearningProvider } from './ml';
import { TransformProvider } from './transform';
import { DashboardPanelTimeRangeProvider } from './dashboard';
import { SearchSessionsService } from './search_sessions';
import { ObservabilityProvider } from './observability';
import { CasesServiceProvider } from './cases';
@ -121,7 +120,6 @@ export const services = {
logsUi: LogsUiProvider,
ml: MachineLearningProvider,
transform: TransformProvider,
dashboardPanelTimeRange: DashboardPanelTimeRangeProvider,
reporting: ReportingFunctionalProvider,
searchSessions: SearchSessionsService,
observability: ObservabilityProvider,