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 index 172c05d330a7d..e232d2e4e54ef 100644 --- 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 @@ -8,18 +8,33 @@ import React from 'react'; import { Subject } from 'rxjs'; -import { distinctUntilChanged, filter, map, take, takeUntil } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import { 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 } from '../../../..'; +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 }>; + +function isVisualizeEmbeddable( + embeddable: IEmbeddable | VisualizeEmbeddable +): embeddable is VisualizeEmbeddable { + return embeddable.type === VISUALIZE_EMBEDDABLE_TYPE; +} + export interface TimeRangeInput extends EmbeddableInput { timeRange: TimeRange; } @@ -46,6 +61,22 @@ export class CustomizePanelAction implements Action 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', @@ -57,7 +88,10 @@ export class CustomizePanelAction implements Action } public async isCompatible({ embeddable }: CustomizePanelActionContext) { - return embeddable.getInput().viewMode === ViewMode.EDIT ? true : false; + // 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) { @@ -76,6 +110,7 @@ export class CustomizePanelAction implements Action toMountPoint( 'data-test-subj': 'customizePanel', } ); - - // Close flyout on dashboard switch to "view" mode or on embeddable destroy. - embeddable - .getInput$() - .pipe( - takeUntil(closed$), - map((input) => input.viewMode), - distinctUntilChanged(), - filter((mode) => mode !== ViewMode.EDIT), - take(1) - ) - .subscribe({ next: close, complete: close }); } } 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 index eecbc677f9041..94a2c0ead1bb1 100644 --- 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 @@ -27,9 +27,9 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { TimeRange } from '@kbn/es-query'; -import { hasTimeRange, TimeRangeInput } from './customize_panel_action'; +import { TimeRangeInput } from './customize_panel_action'; import { doesInheritTimeRange } from './does_inherit_time_range'; -import { IEmbeddable, Embeddable, EmbeddableOutput, CommonlyUsedRange } from '../../../..'; +import { IEmbeddable, Embeddable, CommonlyUsedRange, ViewMode } from '../../../..'; import { canInheritTimeRange } from './can_inherit_time_range'; type PanelSettings = { @@ -41,40 +41,15 @@ type PanelSettings = { interface CustomizePanelProps { embeddable: IEmbeddable; + timeRangeCompatible: boolean; dateFormat?: string; commonlyUsedRanges?: CommonlyUsedRange[]; onClose: () => void; } -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; -} - -function isTimeRangeCompatible(embeddable: IEmbeddable) { - 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 - ); -} - export const CustomizePanelEditor = (props: CustomizePanelProps) => { - const { onClose, embeddable, dateFormat } = props; - const timeRangeCompatible = isTimeRangeCompatible(embeddable); + 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 @@ -119,6 +94,119 @@ export const CustomizePanelEditor = (props: CustomizePanelProps) => { 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 panel 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 panel 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; @@ -178,109 +266,7 @@ export const CustomizePanelEditor = (props: CustomizePanelProps) => { - - - } - onChange={(e) => setHideTitle(!e.target.checked)} - /> - - - } - labelAppend={ - setPanelTitle(embeddable.getOutput().defaultTitle)} - disabled={hideTitle} - aria-label={i18n.translate( - 'embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomTitleButtonAriaLabel', - { - defaultMessage: 'Reset panel 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} - aria-label={i18n.translate( - 'embeddableApi.customizePanel.flyout.optionsMenuForm.resetCustomDescriptionButtonAriaLabel', - { - defaultMessage: 'Reset panel description', - } - )} - > - - - } - > - setPanelDescription(e.target.value)} - aria-label={i18n.translate( - 'embeddableApi.customizePanel.flyout.optionsMenuForm.panelDescriptionAriaLabel', - { - defaultMessage: 'Enter a custom description for your panel', - } - )} - /> - + {renderCustomTitleComponent()} {renderCustomTimeRangeComponent()} diff --git a/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx index a57a4dda210df..4a4e7733ba40d 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_editor.test.tsx @@ -8,7 +8,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import * as React from 'react'; -import { EmbeddableOutput, isErrorEmbeddable } from '../lib'; +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'; @@ -44,6 +44,7 @@ beforeEach(async () => { 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'); @@ -54,7 +55,7 @@ beforeEach(async () => { test('Value is initialized with the embeddables title', async () => { const component = mountWithIntl( - {}} /> + {}} /> ); const titleField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); @@ -68,7 +69,7 @@ test('Value is initialized with the embeddables title', async () => { test('Calls updateInput with a new title', async () => { const updateInput = jest.spyOn(embeddable, 'updateInput'); const component = mountWithIntl( - {}} /> + {}} /> ); const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); @@ -85,7 +86,7 @@ test('Calls updateInput with a new title', async () => { 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'); @@ -97,7 +98,7 @@ test('Input value shows custom title if one given', async () => { 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'); @@ -117,7 +118,7 @@ test('Reset updates the input values with the default properties when the embedd 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'); @@ -141,7 +142,7 @@ test('Reset updates the input with the default properties when the embeddable ha test('Reset title calls updateInput with undefined', async () => { const updateInput = jest.spyOn(embeddable, 'updateInput'); const component = mountWithIntl( - {}} /> + {}} /> ); const inputField = findTestSubject(component, 'customEmbeddablePanelTitleInput').find('input'); @@ -159,7 +160,7 @@ test('Reset title calls updateInput with undefined', async () => { test('Reset description calls updateInput with undefined', async () => { const updateInput = jest.spyOn(embeddable, 'updateInput'); const component = mountWithIntl( - {}} /> + {}} /> ); const inputField = findTestSubject(component, 'customEmbeddablePanelDescriptionInput').find( @@ -179,7 +180,7 @@ test('Reset description calls updateInput with undefined', async () => { test('Can set title and description to an empty string', async () => { const updateInput = jest.spyOn(embeddable, 'updateInput'); const component = mountWithIntl( - {}} /> + {}} /> ); for (const subject of [