diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 77466de4e8cc3..be69c0236cd50 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -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. diff --git a/docs/user/dashboard/aggregation-based.asciidoc b/docs/user/dashboard/aggregation-based.asciidoc index 7d5e4f93bba88..9098ea6265291 100644 --- a/docs/user/dashboard/aggregation-based.asciidoc +++ b/docs/user/dashboard/aggregation-based.asciidoc @@ -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*. diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 5f147f48d7659..a6a540acfc973 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -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 < 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]] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 3467276ce236d..ff6ffe230f7e7 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -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]] diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 7e5b753973ea2..f491167acda0b 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -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()]. diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index d725a82d74b65..f266e9a9cc331 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -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 <> 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. -==== \ No newline at end of file +==== diff --git a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index 95f937bb65443..0c183b91fc495 100644 --- a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -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*. \ No newline at end of file +. Click *Save*. diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index b107a82bdaa33..2c9d5db938962 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -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: { diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index f26e5e9e2e9dd..8e06f6865749b 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -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 */ diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index fd4a94419613a..199756cca6ee2 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -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 diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index d4c56c78f5107..ceff49dc80f13 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -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"] } diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index d1871ce2ffc98..75c666d4e2903 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -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); if (oldLastReloadRequestTime !== newInput.lastReloadRequestTime) { this.reload(); diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 2370b62743466..4f82e21440163 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -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. diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 35d3ecdf7c6e6..8752bcedfe00f 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -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() }, }, })); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index f6a17c09f3aae..c6bef2db73ac9 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -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 { 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 { 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 { ) { 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( - { - 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, diff --git a/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.test.ts similarity index 85% rename from src/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts rename to src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.test.ts index 1221b44aefa79..0bec8ab73d911 100644 --- a/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.test.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.test.ts @@ -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( diff --git a/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.ts similarity index 83% rename from src/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts rename to src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.ts index e3b2122398632..598a921f30016 100644 --- a/src/plugins/ui_actions_enhanced/public/can_inherit_time_range.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/can_inherit_time_range.ts @@ -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 { timeRange: TimeRange; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.test.ts new file mode 100644 index 0000000000000..dc2c02fb4462e --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.test.ts @@ -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('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('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('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); +}); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.tsx new file mode 100644 index 0000000000000..93c64122351f9 --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/custom_time_range_badge.tsx @@ -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; +} + +export class CustomTimeRangeBadge + extends CustomizePanelAction + implements Action +{ + public readonly type = CUSTOM_TIME_RANGE_BADGE; + public readonly id = CUSTOM_TIME_RANGE_BADGE; + public order = 7; + + public getDisplayName({ embeddable }: TimeBadgeActionContext) { + return renderToString( + + ); + } + + public getIconType() { + return 'calendar'; + } + + public async isCompatible({ embeddable }: TimeBadgeActionContext) { + return Boolean(embeddable && hasTimeRange(embeddable) && !doesInheritTimeRange(embeddable)); + } +} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.test.ts new file mode 100644 index 0000000000000..0d67a41c7cd19 --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.test.ts @@ -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(); +}); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx new file mode 100644 index 0000000000000..ec6c2011ca53d --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_action.tsx @@ -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 +): embeddable is Embeddable { + return (embeddable as Embeddable).getInput().timeRange !== undefined; +} + +export interface CustomizePanelActionContext { + embeddable: IEmbeddable | Embeddable; +} + +export class CustomizePanelAction implements Action { + 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( + { + if (overlayTracker) overlayTracker.clearOverlays(); + handle.close(); + }} + />, + { theme$: this.theme.theme$ } + ), + { + size: 's', + 'data-test-subj': 'customizePanel', + } + ); + overlayTracker?.openOverlay(handle); + } +} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor.tsx new file mode 100644 index 0000000000000..0c19452867038 --- /dev/null +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/customize_panel_editor.tsx @@ -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) : false + ); + const [panelTimeRange, setPanelTimeRange] = useState( + timeRangeCompatible + ? (embeddable as Embeddable).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 ( + <> + + + } + onChange={(e) => setHideTitle(!e.target.checked)} + /> + + + } + labelAppend={ + setPanelTitle(embeddable.getOutput().defaultTitle)} + disabled={hideTitle || !editMode} + aria-label={i18n.translate( + 'embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonAriaLabel', + { + defaultMessage: 'Reset title', + } + )} + > + + + } + > + setPanelTitle(e.target.value)} + aria-label={i18n.translate( + 'embeddableApi.customizePanel.flyout.optionsMenuForm.panelTitleInputAriaLabel', + { + defaultMessage: 'Enter a custom title for your panel', + } + )} + /> + + + } + labelAppend={ + { + setPanelDescription(embeddable.getOutput().defaultDescription); + }} + disabled={hideTitle || !editMode} + aria-label={i18n.translate( + 'embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomDescriptionButtonAriaLabel', + { + defaultMessage: 'Reset description', + } + )} + > + + + } + > + setPanelDescription(e.target.value)} + aria-label={i18n.translate( + 'embeddableApi.customizePanel.flyout.optionsMenuForm.panelDescriptionAriaLabel', + { + defaultMessage: 'Enter a custom description for your panel', + } + )} + /> + + + ); + }; + + const renderCustomTimeRangeComponent = () => { + if (!timeRangeCompatible) return null; + + return ( + <> + {canInheritTimeRange(embeddable as Embeddable) ? ( + + + } + onChange={(e) => setInheritTimeRange(!e.target.checked)} + /> + + ) : null} + {!inheritTimeRange ? ( + + } + > + setPanelTimeRange({ from: start, to: end })} + showUpdateButton={false} + dateFormat={dateFormat} + commonlyUsedRanges={commonlyUsedRangesForDatePicker} + data-test-subj="customizePanelTimeRangeDatePicker" + /> + + ) : null} + + ); + }; + + return ( + <> + + +

+ +

+
+
+ + + {renderCustomTitleComponent()} + {renderCustomTimeRangeComponent()} + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/does_inherit_time_range.ts similarity index 86% rename from src/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts rename to src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/does_inherit_time_range.ts index 52b3abc6ebbdd..3fa68bd3d5829 100644 --- a/src/plugins/ui_actions_enhanced/public/does_inherit_time_range.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/does_inherit_time_range.ts @@ -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) { if (!embeddable.parent) { diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/index.ts similarity index 100% rename from src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/index.ts rename to src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_panel/index.ts diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts deleted file mode 100644 index 58f15c326be77..0000000000000 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ /dev/null @@ -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'); -}); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts deleted file mode 100644 index d2aab8eedea2c..0000000000000 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts +++ /dev/null @@ -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 { - 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 }); - } -} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx deleted file mode 100644 index 4360f74310f9f..0000000000000 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx +++ /dev/null @@ -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 { - 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 ( - - -
-
{ - event.preventDefault(); - this.save(); - }} - > - - - Customize panel - - - - - - - } - onChange={this.onHideTitleToggle} - /> - - - this.setState({ title: e.target.value })} - aria-label={i18n.translate( - 'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel', - { - defaultMessage: 'Enter a custom title for your panel', - } - )} - append={ - - - - } - /> - - - - this.props.cancel()}> - - - - - - - -
-
-
-
- ); - } -} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx deleted file mode 100644 index f54eb4125326f..0000000000000 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx +++ /dev/null @@ -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) { - onUpdatePanelTitle(event.target.value); - } - - return ( -
- - - - - - - -
- ); -} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts index c6ce5a073b53e..c3b8f0d75ef16 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/index.ts @@ -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'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index f7ef8c0ccb502..00fa8db54552c 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -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>, embeddable: IEmbeddable) { @@ -50,6 +51,7 @@ function renderBadges(badges: Array>, 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 })} @@ -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} - ) : customizeTitle ? ( + ) : customizePanel ? ( customizeTitle.execute({ embeddable })} + onClick={() => customizePanel.execute({ embeddable })} > {title || placeholderTitle} diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx index 1fbc9a459e572..098ec0d9a0bdd 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/filterable_container.tsx @@ -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; }; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/index.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/index.ts index b1c5db03cc1d3..6d24be64f29c8 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/index.ts @@ -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'; diff --git a/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/time_range_container.ts similarity index 89% rename from src/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts rename to src/plugins/embeddable/public/lib/test_samples/embeddables/time_range_container.ts index 0e6044ba9291e..5b9697934bec9 100644 --- a/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_container.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/time_range_container.ts @@ -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; }; diff --git a/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts b/src/plugins/embeddable/public/lib/test_samples/embeddables/time_range_embeddable.ts similarity index 68% rename from src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts rename to src/plugins/embeddable/public/lib/test_samples/embeddables/time_range_embeddable.ts index 7829c432d3c6c..31da543477189 100644 --- a/src/plugins/ui_actions_enhanced/public/test_helpers/time_range_embeddable.ts +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/time_range_embeddable.ts @@ -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 diff --git a/src/plugins/embeddable/public/lib/types.ts b/src/plugins/embeddable/public/lib/types.ts index d22bcbd12bef9..a8a5892b07aa6 100644 --- a/src/plugins/embeddable/public/lib/types.ts +++ b/src/plugins/embeddable/public/lib/types.ts @@ -23,3 +23,9 @@ export interface PropertySpec { } export { ViewMode } from '../../common/types'; export type { Adapters }; + +export interface CommonlyUsedRange { + from: string; + to: string; + display: string; +} diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 698649c2cbd2a..937ea9a28639d 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -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 { this.appList = appList; }); @@ -184,13 +201,15 @@ export class EmbeddablePublicPlugin implements Plugin ); diff --git a/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx new file mode 100644 index 0000000000000..4a4e7733ba40d --- /dev/null +++ b/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx @@ -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( + {}} /> + ); + + 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( + {}} /> + ); + + 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( + {}} /> + ); + + 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( + {}} /> + ); + + 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( + {}} /> + ); + + 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( + {}} /> + ); + + 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( + {}} /> + ); + + 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( + {}} /> + ); + + 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: '' }); +}); diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx deleted file mode 100644 index 25280c3ee8092..0000000000000 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ /dev/null @@ -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( - {}} 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( - {}} /> - ); - - 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( - {}} /> - ); - - 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( - {}} /> - ); - - 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( - {}} /> - ); - - 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( - {}} /> - ); - - 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( - {}} /> - ); - - 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); -}); diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json index 645b3ae3dfe08..67102414915d9 100644 --- a/src/plugins/embeddable/tsconfig.json +++ b/src/plugins/embeddable/tsconfig.json @@ -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/**/*", diff --git a/src/plugins/ui_actions_enhanced/kibana.json b/src/plugins/ui_actions_enhanced/kibana.json index 0f050db399d38..95f78979c1f62 100644 --- a/src/plugins/ui_actions_enhanced/kibana.json +++ b/src/plugins/ui_actions_enhanced/kibana.json @@ -11,5 +11,5 @@ "ui": true, "requiredPlugins": ["embeddable", "uiActions"], "optionalPlugins": ["licensing"], - "requiredBundles": ["kibanaUtils", "kibanaReact", "data"] + "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts deleted file mode 100644 index 78be66fa84fb4..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/custom_time_range_action.test.ts +++ /dev/null @@ -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('1'); - expect(child1).toBeDefined(); - expect(child1.getInput().timeRange).toEqual({ from: 'now-15m', to: 'now' }); - - const child2 = container.getChild('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('1'); - const child2 = container.getChild('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('1'); - const child2 = container.getChild('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('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('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('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); -}); diff --git a/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx b/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx deleted file mode 100644 index 8638a877b9c12..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/custom_time_range_action.tsx +++ /dev/null @@ -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 -): embeddable is Embeddable { - return (embeddable as Embeddable).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; -} - -export class CustomTimeRangeAction implements Action { - 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( - modalSession.close()} - embeddable={embeddable} - dateFormat={this.dateFormat} - commonlyUsedRanges={this.commonlyUsedRanges} - />, - { - 'data-test-subj': 'customizeTimeRangeModal', - } - ); - } - } -} diff --git a/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts b/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts deleted file mode 100644 index 3cc4c3b44cdc7..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.test.ts +++ /dev/null @@ -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('1'); - const child2 = container.getChild('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('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('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('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); -}); diff --git a/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx b/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx deleted file mode 100644 index ffed76649d8ca..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/custom_time_range_badge.tsx +++ /dev/null @@ -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 -): embeddable is Embeddable { - return (embeddable as Embeddable).getInput().timeRange !== undefined; -} - -export interface TimeBadgeActionContext { - embeddable: Embeddable; -} - -export class CustomTimeRangeBadge implements Action { - 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( - - ); - } - - 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( - modalSession.close()} - embeddable={embeddable} - dateFormat={this.dateFormat} - commonlyUsedRanges={this.commonlyUsedRanges} - />, - { - 'data-test-subj': 'customizeTimeRangeModal', - } - ); - } - } -} diff --git a/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx b/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx deleted file mode 100644 index d73b3ea59b708..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.test.tsx +++ /dev/null @@ -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( - ({ timerange: { from: 'now-7d', to: 'now' } }), - } as unknown as Embeddable - } - onClose={() => {}} - commonlyUsedRanges={[]} - /> - ); - - expect(screen.getByTestId('superDatePickerToggleQuickMenuButton')).toBeInTheDocument(); - expect(screen.queryByTitle(/auto refresh/gi)).not.toBeInTheDocument(); -}); diff --git a/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx b/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx deleted file mode 100644 index 0bcd36741d8c6..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/customize_time_range_modal.tsx +++ /dev/null @@ -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; - onClose: () => void; - dateFormat?: string; - commonlyUsedRanges: CommonlyUsedRange[]; -} - -interface State { - timeRange?: TimeRange; - inheritTimeRange: boolean; -} - -export class CustomizeTimeRangeModal extends Component { - 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>; - 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 ( - - - - {i18n.translate('uiActionsEnhanced.customizeTimeRange.modal.headerTitle', { - defaultMessage: 'Customize panel time range', - })} - - - - - - { - return { - start: from, - end: to, - label: display, - }; - } - )} - data-test-subj="customizePanelTimeRangeDatePicker" - /> - - - - - -
- - {i18n.translate( - 'uiActionsEnhanced.customizePanelTimeRange.modal.removeButtonTitle', - { - defaultMessage: 'Remove', - } - )} - -
-
- - - {i18n.translate( - 'uiActionsEnhanced.customizePanelTimeRange.modal.cancelButtonTitle', - { - defaultMessage: 'Cancel', - } - )} - - - - - {this.state.inheritTimeRange - ? i18n.translate( - 'uiActionsEnhanced.customizePanelTimeRange.modal.addToPanelButtonTitle', - { - defaultMessage: 'Add to panel', - } - ) - : i18n.translate( - 'uiActionsEnhanced.customizePanelTimeRange.modal.updatePanelTimeRangeButtonTitle', - { - defaultMessage: 'Update', - } - )} - - -
-
-
- ); - } -} diff --git a/src/plugins/ui_actions_enhanced/public/plugin.ts b/src/plugins/ui_actions_enhanced/public/plugin.ts index 1065b30c8e4dc..dc3836f2d693e 100644 --- a/src/plugins/ui_actions_enhanced/public/plugin.ts +++ b/src/plugins/ui_actions_enhanced/public/plugin.ts @@ -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!, diff --git a/src/plugins/ui_actions_enhanced/public/test_helpers/index.ts b/src/plugins/ui_actions_enhanced/public/test_helpers/index.ts deleted file mode 100644 index 26220094e4981..0000000000000 --- a/src/plugins/ui_actions_enhanced/public/test_helpers/index.ts +++ /dev/null @@ -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'; diff --git a/src/plugins/ui_actions_enhanced/tsconfig.json b/src/plugins/ui_actions_enhanced/tsconfig.json index ccb424b83a1b0..cd3dafd8789a0 100644 --- a/src/plugins/ui_actions_enhanced/tsconfig.json +++ b/src/plugins/ui_actions_enhanced/tsconfig.json @@ -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", diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 6fbb5917b2ada..144fbf2d40952 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -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; } diff --git a/test/functional/apps/dashboard/group5/data_shared_attributes.ts b/test/functional/apps/dashboard/group5/data_shared_attributes.ts index d4070c700a925..71d8a16b2f7d8 100644 --- a/test/functional/apps/dashboard/group5/data_shared_attributes.ts +++ b/test/functional/apps/dashboard/group5/data_shared_attributes.ts @@ -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) => { diff --git a/test/functional/apps/dashboard/group5/share.ts b/test/functional/apps/dashboard/group5/share.ts index da66dd538653f..b6a2e9811e51f 100644 --- a/test/functional/apps/dashboard/group5/share.ts +++ b/test/functional/apps/dashboard/group5/share.ts @@ -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'); diff --git a/test/functional/services/dashboard/index.ts b/test/functional/services/dashboard/index.ts index 01262efd2a16f..02fd7d95fd026 100644 --- a/test/functional/services/dashboard/index.ts +++ b/test/functional/services/dashboard/index.ts @@ -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'; diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index d3d9338c4f1a2..2ec60fef36462 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -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} - */ - 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 { this.log.debug(`getActionWebElement: "${text}"`); const menu = await this.testSubjects.find('multipleActionsContextMenu'); diff --git a/test/functional/services/dashboard/panel_badge_actions.ts b/test/functional/services/dashboard/panel_badge_actions.ts new file mode 100644 index 0000000000000..461cc8e70aa2d --- /dev/null +++ b/test/functional/services/dashboard/panel_badge_actions.ts @@ -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); + } + })(); +} diff --git a/test/functional/services/dashboard/panel_settings.ts b/test/functional/services/dashboard/panel_settings.ts new file mode 100644 index 0000000000000..d82dabf097eb7 --- /dev/null +++ b/test/functional/services/dashboard/panel_settings.ts @@ -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); + } + })(); +} diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 4f74899e826f0..80b80ada9a979 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -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, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index c0ad0a8417e77..fd38ac8fabcbe 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -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 diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 9ac16988bdf73..51b806cec5dce 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -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() diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx index 137b9ec260b92..556a9c8419d7b 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_embeddable.tsx @@ -48,6 +48,7 @@ export class AnomalyChartsEmbeddable extends Embeddable< initialInput, { defaultTitle: initialInput.title, + defaultDescription: initialInput.description, }, parent ); diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index c024341da42ae..4d28dbd4f0e6f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -49,6 +49,7 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< initialInput, { defaultTitle: initialInput.title, + defaultDescription: initialInput.description, }, parent ); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4e4e22cefafec..617d8c03280f6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -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", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2b089e8c055b8..0a150f7b97c74 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "非表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 849f629a69eea..54c91f554cc22 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "隐藏", diff --git a/x-pack/test/accessibility/apps/dashboard_panel_options.ts b/x-pack/test/accessibility/apps/dashboard_panel_options.ts index da66f03054b08..4e4dc3b218d79 100644 --- a/x-pack/test/accessibility/apps/dashboard_panel_options.ts +++ b/x-pack/test/accessibility/apps/dashboard_panel_options.ts @@ -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 () => { diff --git a/x-pack/test/functional/apps/dashboard/group2/index.ts b/x-pack/test/functional/apps/dashboard/group2/index.ts index bc6a13af29c5f..8b45cda030252 100644 --- a/x-pack/test/functional/apps/dashboard/group2/index.ts +++ b/x-pack/test/functional/apps/dashboard/group2/index.ts @@ -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')); diff --git a/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts b/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts new file mode 100644 index 0000000000000..ee07446783603 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group2/panel_time_range.ts @@ -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(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts index 17ec741a73c30..14970ba7764ab 100644 --- a/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts +++ b/x-pack/test/functional/apps/dashboard/group2/panel_titles.ts @@ -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 diff --git a/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts index 5028fa056ba76..8e943c2b3104d 100644 --- a/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts @@ -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'); diff --git a/x-pack/test/functional/apps/discover/saved_searches.ts b/x-pack/test/functional/apps/discover/saved_searches.ts index 91eee92683654..f043c1985e702 100644 --- a/x-pack/test/functional/apps/discover/saved_searches.ts +++ b/x-pack/test/functional/apps/discover/saved_searches.ts @@ -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); }); diff --git a/x-pack/test/functional/services/dashboard/index.ts b/x-pack/test/functional/services/dashboard/index.ts deleted file mode 100644 index 8a97912563aaf..0000000000000 --- a/x-pack/test/functional/services/dashboard/index.ts +++ /dev/null @@ -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'; diff --git a/x-pack/test/functional/services/dashboard/panel_time_range.ts b/x-pack/test/functional/services/dashboard/panel_time_range.ts deleted file mode 100644 index 6f92c1abe2d39..0000000000000 --- a/x-pack/test/functional/services/dashboard/panel_time_range.ts +++ /dev/null @@ -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(); - } - })(); -} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index aef5bb6523ac6..9e77c3594c96e 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -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,