mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Dashboard Usability] Unified panel options pane (#148301)
This commit is contained in:
parent
f90bf811f8
commit
ace2c30c29
74 changed files with 1370 additions and 1891 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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*.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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()].
|
||||
|
|
|
@ -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.
|
||||
====
|
||||
====
|
||||
|
|
|
@ -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*.
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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() },
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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) {
|
|
@ -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');
|
||||
});
|
|
@ -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 });
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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() {}
|
|
@ -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>
|
|
@ -23,3 +23,9 @@ export interface PropertySpec {
|
|||
}
|
||||
export { ViewMode } from '../../common/types';
|
||||
export type { Adapters };
|
||||
|
||||
export interface CommonlyUsedRange {
|
||||
from: string;
|
||||
to: string;
|
||||
display: string;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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: '' });
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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/**/*",
|
||||
|
|
|
@ -11,5 +11,5 @@
|
|||
"ui": true,
|
||||
"requiredPlugins": ["embeddable", "uiActions"],
|
||||
"optionalPlugins": ["licensing"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "data"]
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact"]
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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!,
|
||||
|
|
|
@ -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';
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
|
|
34
test/functional/services/dashboard/panel_badge_actions.ts
Normal file
34
test/functional/services/dashboard/panel_badge_actions.ts
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
111
test/functional/services/dashboard/panel_settings.ts
Normal file
111
test/functional/services/dashboard/panel_settings.ts
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -48,6 +48,7 @@ export class AnomalyChartsEmbeddable extends Embeddable<
|
|||
initialInput,
|
||||
{
|
||||
defaultTitle: initialInput.title,
|
||||
defaultDescription: initialInput.description,
|
||||
},
|
||||
parent
|
||||
);
|
||||
|
|
|
@ -49,6 +49,7 @@ export class AnomalySwimlaneEmbeddable extends Embeddable<
|
|||
initialInput,
|
||||
{
|
||||
defaultTitle: initialInput.title,
|
||||
defaultDescription: initialInput.description,
|
||||
},
|
||||
parent
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "非表示",
|
||||
|
|
|
@ -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": "隐藏",
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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'));
|
||||
|
|
105
x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts
Normal file
105
x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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';
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue