From 690690ea21689ce34cdd6b85662e956096caa2d5 Mon Sep 17 00:00:00 2001 From: "Eyo O. Eyo" <7893459+eokoneyo@users.noreply.github.com> Date: Wed, 29 May 2024 11:46:23 +0200 Subject: [PATCH] Simplify workflow for dashboard copy creation in both view and edit interaction modes (#180938) ## Summary Closes https://github.com/elastic/kibana/issues/161047 - Removes the `save as` top nav menu button - Also renames nav menu item `clone` to `duplicate` and make it available in edit mode. - The save dashboard modal no longer displays and open to save the dashboard in context as new, given that we've chosen to explicitly create a copy of the dashboard in context when either of the the `duplicate` or `saveas` menu option is selected. - includes bug fix for an issue where clicking the dashboard modal scrolled the user to the content bottom, see https://github.com/elastic/kibana/pull/180938#issuecomment-2117586572 ## Before ### View mode Screenshot 2024-04-16 at 15 59 10 ### Edit mode Screenshot 2024-04-16 at 15 59 00 ## After #### Managed Dashboard https://github.com/elastic/kibana/assets/7893459/5072a501-8d16-4f25-9575-6f11fed6e580 #### View mode https://github.com/elastic/kibana/assets/7893459/610d0952-97f0-46b8-a0ea-1546a799d387 #### Edit mode https://github.com/elastic/kibana/assets/7893459/4f596c07-7bd1-4c5a-9131-0c78731cb113 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../dashboard_app/_dashboard_app_strings.ts | 14 +- .../top_nav/use_dashboard_menu_items.tsx | 89 ++-- .../component/settings/settings_flyout.tsx | 6 +- .../embeddable/api/index.ts | 2 +- .../api/lib/extract_title_and_count.test.ts | 18 +- .../api/lib/extract_title_and_count.ts | 2 +- .../__snapshots__/save_modal.test.js.snap | 53 ++- .../embeddable/api/overlays/save_modal.tsx | 186 ++++----- .../embeddable/api/run_save_functions.tsx | 382 +++++++++--------- .../embeddable/dashboard_container.tsx | 6 +- .../saved_object_save_modal.test.tsx.snap | 24 +- .../save_modal/saved_object_save_modal.tsx | 12 +- .../show_saved_object_save_modal.tsx | 63 +-- src/plugins/saved_objects/tsconfig.json | 1 - test/accessibility/apps/dashboard.ts | 4 +- .../dashboard/group1/embeddable_rendering.ts | 1 + .../dashboard/group2/dashboard_filter_bar.ts | 4 +- .../apps/dashboard/group3/bwc_shared_urls.ts | 1 + .../apps/dashboard/group3/dashboard_state.ts | 2 +- .../apps/dashboard/group4/dashboard_clone.ts | 2 +- .../dashboard/group4/dashboard_listing.ts | 5 +- .../apps/dashboard/group4/dashboard_save.ts | 196 +++++---- .../apps/dashboard/group4/dashboard_time.ts | 10 +- .../apps/dashboard/group5/legacy_urls.ts | 5 +- .../apps/dashboard/group6/view_edit.ts | 35 +- .../common/control_group_apply_button.ts | 5 +- .../controls/common/range_slider.ts | 5 +- .../controls/common/replace_controls.ts | 5 +- .../controls/common/time_slider.ts | 5 +- .../controls/options_list/index.ts | 1 + .../links/links_create_edit.ts | 5 +- .../embeddable/_saved_search_embeddable.ts | 3 +- .../functional/page_objects/dashboard_page.ts | 110 +++-- .../journeys_e2e/dashboard_listing_page.ts | 4 +- .../translations/translations/fr-FR.json | 8 +- .../translations/translations/ja-JP.json | 8 +- .../translations/translations/zh-CN.json | 8 +- .../apps/group1/dashboard_links.ts | 5 +- .../apps/dashboard/group1/created_by.ts | 1 + .../drilldowns/explore_data_panel_action.ts | 10 +- .../apps/discover/saved_search_embeddable.ts | 1 + .../group4/show_underlying_data_dashboard.ts | 2 + .../apps/managed_content/managed_content.ts | 2 +- ...index_data_visualizer_grid_in_dashboard.ts | 2 +- .../functional/tests/dashboard_integration.ts | 4 +- .../embeddable/_saved_search_embeddable.ts | 3 +- 46 files changed, 743 insertions(+), 577 deletions(-) diff --git a/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts b/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts index a513d725f1f31..c58fa0cf828df 100644 --- a/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts +++ b/src/plugins/dashboard/public/dashboard_app/_dashboard_app_strings.ts @@ -189,11 +189,11 @@ export const topNavStrings = { defaultMessage: 'Quick save your dashboard without any prompts', }), }, - saveAs: { - label: i18n.translate('dashboard.topNave.saveAsButtonAriaLabel', { + editModeInteractiveSave: { + label: i18n.translate('dashboard.topNave.editModeInteractiveSaveButtonAriaLabel', { defaultMessage: 'save as', }), - description: i18n.translate('dashboard.topNave.saveAsConfigDescription', { + description: i18n.translate('dashboard.topNave.editModeInteractiveSaveConfigDescription', { defaultMessage: 'Save as a new dashboard', }), }, @@ -229,11 +229,11 @@ export const topNavStrings = { defaultMessage: 'Open dashboard settings', }), }, - clone: { - label: i18n.translate('dashboard.topNave.cloneButtonAriaLabel', { - defaultMessage: 'clone', + viewModeInteractiveSave: { + label: i18n.translate('dashboard.topNave.viewModeInteractiveSaveButtonAriaLabel', { + defaultMessage: 'duplicate', }), - description: i18n.translate('dashboard.topNave.cloneConfigDescription', { + description: i18n.translate('dashboard.topNave.viewModeInteractiveSaveConfigDescription', { defaultMessage: 'Create a copy of your dashboard', }), }, 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 9c5aedb76c147..85ebe38e8eecd 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 @@ -104,23 +104,11 @@ export const useDashboardMenuItems = ({ }, [dashboard]); /** - * Show the dashboard's save modal + * initiate interactive dashboard copy action */ - const saveDashboardAs = useCallback(() => { - dashboard.runSaveAs().then((result) => maybeRedirect(result)); - }, [maybeRedirect, dashboard]); - - /** - * Clone the dashboard - */ - const clone = useCallback(() => { - setIsSaveInProgress(true); - - dashboard.runClone().then((result) => { - setIsSaveInProgress(false); - maybeRedirect(result); - }); - }, [maybeRedirect, dashboard]); + const dashboardInteractiveSave = useCallback(() => { + dashboard.runInteractiveSave(viewMode).then((result) => maybeRedirect(result)); + }, [maybeRedirect, dashboard, viewMode]); /** * Show the dashboard's "Confirm reset changes" modal. If confirmed: @@ -197,15 +185,22 @@ export const useDashboardMenuItems = ({ run: () => quickSaveDashboard(), } as TopNavMenuData, - saveAs: { - description: topNavStrings.saveAs.description, + interactiveSave: { disableButton: disableTopNav, - id: 'save', emphasize: !Boolean(lastSavedId), - testId: 'dashboardSaveMenuItem', - iconType: Boolean(lastSavedId) ? undefined : 'save', - label: Boolean(lastSavedId) ? topNavStrings.saveAs.label : topNavStrings.quickSave.label, - run: () => saveDashboardAs(), + id: 'interactive-save', + testId: 'dashboardInteractiveSaveMenuItem', + run: dashboardInteractiveSave, + label: + viewMode === ViewMode.VIEW + ? topNavStrings.viewModeInteractiveSave.label + : Boolean(lastSavedId) + ? topNavStrings.editModeInteractiveSave.label + : topNavStrings.quickSave.label, + description: + viewMode === ViewMode.VIEW + ? topNavStrings.viewModeInteractiveSave.description + : topNavStrings.editModeInteractiveSave.description, } as TopNavMenuData, switchToViewMode: { @@ -230,31 +225,23 @@ export const useDashboardMenuItems = ({ testId: 'dashboardSettingsButton', disableButton: disableTopNav, run: () => dashboard.showSettings(), - } as TopNavMenuData, - - clone: { - ...topNavStrings.clone, - id: 'clone', - testId: 'dashboardClone', - disableButton: disableTopNav, - run: () => clone(), - } as TopNavMenuData, + }, }; }, [ - quickSaveDashboard, + disableTopNav, isSaveInProgress, hasRunMigrations, hasUnsavedChanges, - dashboardBackup, - saveDashboardAs, - setIsLabsShown, - disableTopNav, - resetChanges, - isLabsShown, lastSavedId, + dashboardInteractiveSave, + viewMode, showShare, dashboard, - clone, + setIsLabsShown, + isLabsShown, + dashboardBackup, + quickSaveDashboard, + resetChanges, ]); const resetChangesMenuItem = useMemo(() => { @@ -276,7 +263,7 @@ export const useDashboardMenuItems = ({ const viewModeTopNavConfig = useMemo(() => { const labsMenuItem = isLabsEnabled ? [menuItems.labs] : []; const shareMenuItem = share ? [menuItems.share] : []; - const cloneMenuItem = showWriteControls ? [menuItems.clone] : []; + const duplicateMenuItem = showWriteControls ? [menuItems.interactiveSave] : []; const editMenuItem = showWriteControls && !managed ? [menuItems.edit] : []; const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : []; @@ -284,7 +271,7 @@ export const useDashboardMenuItems = ({ ...labsMenuItem, menuItems.fullScreen, ...shareMenuItem, - ...cloneMenuItem, + ...duplicateMenuItem, ...mayberesetChangesMenuItem, ...editMenuItem, ]; @@ -304,7 +291,7 @@ export const useDashboardMenuItems = ({ const editModeItems: TopNavMenuData[] = []; if (lastSavedId) { - editModeItems.push(menuItems.saveAs, menuItems.switchToViewMode); + editModeItems.push(menuItems.interactiveSave, menuItems.switchToViewMode); if (showResetChange) { editModeItems.push(resetChangesMenuItem); @@ -312,22 +299,10 @@ export const useDashboardMenuItems = ({ editModeItems.push(menuItems.quickSave); } else { - editModeItems.push(menuItems.switchToViewMode, menuItems.saveAs); + editModeItems.push(menuItems.switchToViewMode, menuItems.interactiveSave); } return [...labsMenuItem, menuItems.settings, ...shareMenuItem, ...editModeItems]; - }, [ - isLabsEnabled, - menuItems.labs, - menuItems.share, - menuItems.settings, - menuItems.saveAs, - menuItems.switchToViewMode, - menuItems.quickSave, - share, - lastSavedId, - showResetChange, - resetChangesMenuItem, - ]); + }, [isLabsEnabled, menuItems, share, lastSavedId, showResetChange, resetChangesMenuItem]); return { viewModeTopNavConfig, editModeTopNavConfig }; }; diff --git a/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx b/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx index 51fd1e0e1ed9f..2a6a25a7f0385 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/settings/settings_flyout.tsx @@ -323,7 +323,11 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => { - + { ]); }); - it('defaults to the count to 1 and returns the original title when the provided title does not contain a valid count', () => { - expect(extractTitleAndCount('Test dashboard')).toEqual(['Test dashboard', 1]); - expect(extractTitleAndCount('Test dashboard 2')).toEqual(['Test dashboard 2', 1]); - expect(extractTitleAndCount('Test dashboard (-1)')).toEqual(['Test dashboard (-1)', 1]); - expect(extractTitleAndCount('Test dashboard (0)')).toEqual(['Test dashboard (0)', 1]); - expect(extractTitleAndCount('Test dashboard (3.0)')).toEqual(['Test dashboard (3.0)', 1]); - expect(extractTitleAndCount('Test dashboard (8.4)')).toEqual(['Test dashboard (8.4)', 1]); - expect(extractTitleAndCount('Test dashboard (foo3.0)')).toEqual(['Test dashboard (foo3.0)', 1]); - expect(extractTitleAndCount('Test dashboard (bar7)')).toEqual(['Test dashboard (bar7)', 1]); + it('defaults to the count to 0 and returns the original title when the provided title does not contain a valid count', () => { + expect(extractTitleAndCount('Test dashboard')).toEqual(['Test dashboard', 0]); + expect(extractTitleAndCount('Test dashboard 2')).toEqual(['Test dashboard 2', 0]); + expect(extractTitleAndCount('Test dashboard (-1)')).toEqual(['Test dashboard (-1)', 0]); + expect(extractTitleAndCount('Test dashboard (0)')).toEqual(['Test dashboard (0)', 0]); + expect(extractTitleAndCount('Test dashboard (3.0)')).toEqual(['Test dashboard (3.0)', 0]); + expect(extractTitleAndCount('Test dashboard (8.4)')).toEqual(['Test dashboard (8.4)', 0]); + expect(extractTitleAndCount('Test dashboard (foo3.0)')).toEqual(['Test dashboard (foo3.0)', 0]); + expect(extractTitleAndCount('Test dashboard (bar7)')).toEqual(['Test dashboard (bar7)', 0]); }); }); diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/lib/extract_title_and_count.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/lib/extract_title_and_count.ts index 7100fab8af96d..6f1fac112c9c3 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/lib/extract_title_and_count.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/lib/extract_title_and_count.ts @@ -15,5 +15,5 @@ export const extractTitleAndCount = (title: string): [string, number] => { return [baseTitle, Number(count)]; } } - return [title, 1]; + return [title, 0]; }; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap index 05a8cc2964003..8b551d8afaf21 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/__snapshots__/save_modal.test.js.snap @@ -14,27 +14,44 @@ exports[`renders DashboardSaveModal 1`] = ` display="row" hasChildLabel={true} hasEmptyLabelSpace={false} - helpText={ - - } labelType="label" > - + + + } + onChange={[Function]} /> - } - onChange={[Function]} - /> + + + + } + position="top" + /> + + } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx index 737b8eac640f4..456328f694215 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/overlays/save_modal.tsx @@ -7,20 +7,20 @@ */ import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; +import React, { Fragment, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { SavedObjectSaveModal } from '@kbn/saved-objects-plugin/public'; import type { DashboardSaveOptions } from '../../../types'; import { pluginServices } from '../../../../services/plugin_services'; /** - * TODO: Portable Dashboard followup, convert this to a functional component & use redux for the state. + * TODO: Portable Dashboard followup, use redux for the state. * https://github.com/elastic/kibana/issues/147490 */ -interface Props { +interface DashboardSaveModalProps { onSave: ({ newTitle, newDescription, @@ -36,65 +36,57 @@ interface Props { tags?: string[]; timeRestore: boolean; showCopyOnSave: boolean; + showStoreTimeOnSave?: boolean; + customModalTitle?: string; } -interface State { - tags: string[]; - timeRestore: boolean; -} - -export class DashboardSaveModal extends React.Component { - state: State = { - timeRestore: this.props.timeRestore, - tags: this.props.tags ?? [], - }; - - constructor(props: Props) { - super(props); - } +type SaveDashboardHandler = (args: { + newTitle: string; + newDescription: string; + newCopyOnSave: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; +}) => ReturnType; - saveDashboard = ({ - newTitle, - newDescription, - newCopyOnSave, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }: { - newTitle: string; - newDescription: string; - newCopyOnSave: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; - }) => { - this.props.onSave({ - newTitle, - newDescription, - newCopyOnSave, - newTimeRestore: this.state.timeRestore, - isTitleDuplicateConfirmed, - onTitleDuplicate, - newTags: this.state.tags, - }); - }; +export const DashboardSaveModal: React.FC = ({ + customModalTitle, + description, + onClose, + onSave, + showCopyOnSave, + showStoreTimeOnSave = true, + tags, + title, + timeRestore, +}) => { + const [selectedTags, setSelectedTags] = React.useState(tags ?? []); + const [persistSelectedTimeInterval, setPersistSelectedTimeInterval] = React.useState(timeRestore); - onTimeRestoreChange = (event: any) => { - this.setState({ - timeRestore: event.target.checked, - }); - }; + const saveDashboard = React.useCallback( + ({ newTitle, newDescription, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => { + onSave({ + newTitle, + newDescription, + newCopyOnSave, + newTimeRestore: persistSelectedTimeInterval, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newTags: selectedTags, + }); + }, + [onSave, persistSelectedTimeInterval, selectedTags] + ); - renderDashboardSaveOptions() { + const renderDashboardSaveOptions = useCallback(() => { const { savedObjectsTagging: { components }, } = pluginServices.getServices(); const tagSelector = components ? ( { - this.setState({ - tags, - }); + initialSelection={selectedTags} + onTagsSelected={(selectedTagIds) => { + setSelectedTags(selectedTagIds); }} markOptional /> @@ -103,46 +95,56 @@ export class DashboardSaveModal extends React.Component { return ( {tagSelector} - - - } - > - - } - /> - + {showStoreTimeOnSave ? ( + + + + { + setPersistSelectedTimeInterval(event.target.checked); + }} + label={ + + } + /> + + + + } + position="top" + /> + + + + ) : null} ); - } + }, [persistSelectedTimeInterval, selectedTags, showStoreTimeOnSave]); - render() { - return ( - - ); - } -} + return ( + + ); +}; diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx index 13e2d492fb568..c1ef52ec72e2c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/run_save_functions.tsx @@ -9,12 +9,17 @@ import { Reference } from '@kbn/content-management-utils'; import type { PersistableControlGroupInput } from '@kbn/controls-plugin/common'; import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; -import { EmbeddableInput, isReferenceOrValueEmbeddable } from '@kbn/embeddable-plugin/public'; +import { + EmbeddableInput, + isReferenceOrValueEmbeddable, + ViewMode, +} from '@kbn/embeddable-plugin/public'; import { apiHasSerializableState, SerializedPanelState } from '@kbn/presentation-containers'; import { showSaveModal } from '@kbn/saved-objects-plugin/public'; import { cloneDeep } from 'lodash'; import React from 'react'; import { batch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; import { DashboardContainerInput, DashboardPanelMap } from '../../../../common'; import { prefixReferencesFromPanel } from '../../../../common/dashboard_container/persistable_state/dashboard_container_references'; import { DASHBOARD_CONTENT_ID, SAVED_OBJECT_POST_TIME } from '../../../dashboard_constants'; @@ -63,124 +68,6 @@ const serializeAllPanelState = async ( return { panels, references }; }; -export function runSaveAs(this: DashboardContainer) { - const { - data: { - query: { - timefilter: { timefilter }, - }, - }, - savedObjectsTagging: { hasApi: hasSavedObjectsTagging }, - dashboardContentManagement: { checkForDuplicateDashboardTitle, saveDashboardState }, - } = pluginServices.getServices(); - - const { - explicitInput: currentState, - componentState: { lastSavedId, managed }, - } = this.getState(); - - return new Promise((resolve) => { - if (managed) resolve(undefined); - const onSave = async ({ - newTags, - newTitle, - newDescription, - newCopyOnSave, - newTimeRestore, - onTitleDuplicate, - isTitleDuplicateConfirmed, - }: DashboardSaveOptions): Promise => { - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - saveAsCopy: newCopyOnSave, - }; - const stateFromSaveModal: DashboardStateFromSaveModal = { - title: newTitle, - tags: [] as string[], - description: newDescription, - timeRestore: newTimeRestore, - timeRange: newTimeRestore ? timefilter.getTime() : undefined, - refreshInterval: newTimeRestore ? timefilter.getRefreshInterval() : undefined, - }; - if (hasSavedObjectsTagging && newTags) { - // remove `hasSavedObjectsTagging` once the savedObjectsTagging service is optional - stateFromSaveModal.tags = newTags; - } - if ( - !(await checkForDuplicateDashboardTitle({ - title: newTitle, - onTitleDuplicate, - lastSavedTitle: currentState.title, - copyOnSave: newCopyOnSave, - isTitleDuplicateConfirmed, - })) - ) { - // do not save if title is duplicate and is unconfirmed - return {}; - } - const { panels: nextPanels, references } = await serializeAllPanelState(this); - const dashboardStateToSave: DashboardContainerInput = { - ...currentState, - panels: nextPanels, - ...stateFromSaveModal, - }; - let stateToSave: SavedDashboardInput = dashboardStateToSave; - let persistableControlGroupInput: PersistableControlGroupInput | undefined; - if (this.controlGroup) { - persistableControlGroupInput = this.controlGroup.getPersistableInput(); - stateToSave = { ...stateToSave, controlGroupInput: persistableControlGroupInput }; - } - const beforeAddTime = window.performance.now(); - - const saveResult = await saveDashboardState({ - panelReferences: references, - currentState: stateToSave, - saveOptions, - lastSavedId, - }); - const addDuration = window.performance.now() - beforeAddTime; - reportPerformanceMetricEvent(pluginServices.getServices().analytics, { - eventName: SAVED_OBJECT_POST_TIME, - duration: addDuration, - meta: { - saved_object_type: DASHBOARD_CONTENT_ID, - }, - }); - - stateFromSaveModal.lastSavedId = saveResult.id; - if (saveResult.id) { - batch(() => { - this.dispatch.setStateFromSaveModal(stateFromSaveModal); - this.dispatch.setLastSavedInput(dashboardStateToSave); - if (this.controlGroup && persistableControlGroupInput) { - this.controlGroup.setSavedState(persistableControlGroupInput); - } - }); - } - this.savedObjectReferences = saveResult.references ?? []; - this.saveNotification$.next(); - resolve(saveResult); - return saveResult; - }; - - const dashboardSaveModal = ( - resolve(undefined)} - timeRestore={currentState.timeRestore} - description={currentState.description ?? ''} - showCopyOnSave={lastSavedId ? true : false} - onSave={onSave} - /> - ); - this.clearOverlays(); - showSaveModal(dashboardSaveModal); - }); -} - /** * Save the current state of this dashboard to a saved object without showing any save modal. */ @@ -222,86 +109,203 @@ export async function runQuickSave(this: DashboardContainer) { return saveResult; } -export async function runClone(this: DashboardContainer) { +/** + * @description exclusively for user directed dashboard save actions, also + * accounts for scenarios of cloning elastic managed dashboard into user managed dashboards + */ +export async function runInteractiveSave(this: DashboardContainer, interactionMode: ViewMode) { const { - dashboardContentManagement: { saveDashboardState, checkForDuplicateDashboardTitle }, + data: { + query: { + timefilter: { timefilter }, + }, + }, + savedObjectsTagging: { hasApi: hasSavedObjectsTagging }, + dashboardContentManagement: { checkForDuplicateDashboardTitle, saveDashboardState }, } = pluginServices.getServices(); - const { explicitInput: currentState } = this.getState(); + const { + explicitInput: currentState, + componentState: { lastSavedId, managed }, + } = this.getState(); + + return new Promise((resolve, reject) => { + if (interactionMode === ViewMode.EDIT && managed) { + resolve(undefined); + } - return new Promise(async (resolve, reject) => { - try { - const [baseTitle, baseCount] = extractTitleAndCount(currentState.title); - let copyCount = baseCount; - let newTitle = `${baseTitle} (${copyCount})`; - while ( - !(await checkForDuplicateDashboardTitle({ + const onSaveAttempt = async ({ + newTags, + newTitle, + newDescription, + newCopyOnSave, + newTimeRestore, + onTitleDuplicate, + isTitleDuplicateConfirmed, + }: DashboardSaveOptions): Promise => { + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + saveAsCopy: lastSavedId ? true : newCopyOnSave, + }; + + try { + if ( + !(await checkForDuplicateDashboardTitle({ + title: newTitle, + onTitleDuplicate, + lastSavedTitle: currentState.title, + copyOnSave: saveOptions.saveAsCopy, + isTitleDuplicateConfirmed, + })) + ) { + return {}; + } + + const stateFromSaveModal: DashboardStateFromSaveModal = { title: newTitle, - lastSavedTitle: currentState.title, - copyOnSave: true, - isTitleDuplicateConfirmed: false, - })) - ) { - copyCount++; - newTitle = `${baseTitle} (${copyCount})`; - } + tags: [] as string[], + description: newDescription, + timeRestore: newTimeRestore, + timeRange: newTimeRestore ? timefilter.getTime() : undefined, + refreshInterval: newTimeRestore ? timefilter.getRefreshInterval() : undefined, + }; + + if (hasSavedObjectsTagging && newTags) { + // remove `hasSavedObjectsTagging` once the savedObjectsTagging service is optional + stateFromSaveModal.tags = newTags; + } - let stateToSave: DashboardContainerInput & { - controlGroupInput?: PersistableControlGroupInput; - } = currentState; - if (this.controlGroup) { - stateToSave = { - ...stateToSave, - controlGroupInput: this.controlGroup.getPersistableInput(), + let dashboardStateToSave: DashboardContainerInput & { + controlGroupInput?: PersistableControlGroupInput; + } = { + ...currentState, + ...stateFromSaveModal, }; - } - const isManaged = this.getState().componentState.managed; - const newPanels = await (async () => { - if (!isManaged) return currentState.panels; - - // this is a managed dashboard - unlink all by reference embeddables on clone - const unlinkedPanels: DashboardPanelMap = {}; - for (const [panelId, panel] of Object.entries(currentState.panels)) { - const child = this.getChild(panelId); - if ( - child && - isReferenceOrValueEmbeddable(child) && - child.inputIsRefType(child.getInput() as EmbeddableInput) - ) { - const valueTypeInput = await child.getInputAsValueType(); - unlinkedPanels[panelId] = { - ...panel, - explicitInput: valueTypeInput, - }; - continue; - } - unlinkedPanels[panelId] = panel; + let persistableControlGroupInput: PersistableControlGroupInput | undefined; + if (this.controlGroup) { + persistableControlGroupInput = this.controlGroup.getPersistableInput(); + dashboardStateToSave = { + ...dashboardStateToSave, + controlGroupInput: persistableControlGroupInput, + }; } - return unlinkedPanels; - })(); - - const saveResult = await saveDashboardState({ - saveOptions: { - saveAsCopy: true, - }, - currentState: { - ...stateToSave, - panels: newPanels, - title: newTitle, - }, - }); - this.savedObjectReferences = saveResult.references ?? []; - resolve(saveResult); - return saveResult.id - ? { - id: saveResult.id, + + const { panels: nextPanels, references } = await serializeAllPanelState(this); + + const newPanels = await (async () => { + if (!managed) return nextPanels; + + // this is a managed dashboard - unlink all by reference embeddables on clone + const unlinkedPanels: DashboardPanelMap = {}; + for (const [panelId, panel] of Object.entries(nextPanels)) { + const child = this.getChild(panelId); + if ( + child && + isReferenceOrValueEmbeddable(child) && + child.inputIsRefType(child.getInput() as EmbeddableInput) + ) { + const valueTypeInput = await child.getInputAsValueType(); + unlinkedPanels[panelId] = { + ...panel, + explicitInput: valueTypeInput, + }; + continue; + } + unlinkedPanels[panelId] = panel; } - : { - error: saveResult.error, - }; - } catch (error) { - reject(error); + return unlinkedPanels; + })(); + + const beforeAddTime = window.performance.now(); + + const saveResult = await saveDashboardState({ + panelReferences: references, + saveOptions, + currentState: { + ...dashboardStateToSave, + panels: newPanels, + title: newTitle, + }, + lastSavedId, + }); + + const addDuration = window.performance.now() - beforeAddTime; + + reportPerformanceMetricEvent(pluginServices.getServices().analytics, { + eventName: SAVED_OBJECT_POST_TIME, + duration: addDuration, + meta: { + saved_object_type: DASHBOARD_CONTENT_ID, + }, + }); + + stateFromSaveModal.lastSavedId = saveResult.id; + + if (saveResult.id) { + batch(() => { + this.dispatch.setStateFromSaveModal(stateFromSaveModal); + this.dispatch.setLastSavedInput(dashboardStateToSave); + if (this.controlGroup && persistableControlGroupInput) { + this.controlGroup.setSavedState(persistableControlGroupInput); + } + }); + } + + this.savedObjectReferences = saveResult.references ?? []; + this.saveNotification$.next(); + + resolve(saveResult); + return saveResult; + } catch (error) { + reject(error); + return error; + } + }; + + let customModalTitle; + let newTitle = currentState.title; + + if (lastSavedId) { + const [baseTitle, baseCount] = extractTitleAndCount(currentState.title); + newTitle = `${baseTitle} (${baseCount + 1})`; + + switch (interactionMode) { + case ViewMode.EDIT: { + customModalTitle = i18n.translate('dashboard.topNav.editModeInteractiveSave.modalTitle', { + defaultMessage: 'Save as new dashboard', + }); + break; + } + case ViewMode.VIEW: { + customModalTitle = i18n.translate('dashboard.topNav.viewModeInteractiveSave.modalTitle', { + defaultMessage: 'Duplicate dashboard', + }); + break; + } + default: { + customModalTitle = undefined; + } + } } + + const dashboardDuplicateModal = ( + resolve(undefined)} + timeRestore={currentState.timeRestore} + showStoreTimeOnSave={!lastSavedId} + description={currentState.description ?? ''} + showCopyOnSave={false} + onSave={onSaveAttempt} + customModalTitle={customModalTitle} + /> + ); + + this.clearOverlays(); + showSaveModal(dashboardDuplicateModal); }); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 258a80ed834eb..8d9c41be0094b 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -82,9 +82,8 @@ import { import { addFromLibrary, addOrUpdateEmbeddable, - runClone, runQuickSave, - runSaveAs, + runInteractiveSave, showSettings, } from './api'; import { duplicateDashboardPanel } from './api/duplicate_dashboard_panel'; @@ -455,8 +454,7 @@ export class DashboardContainer // Dashboard API // ------------------------------------------------------------------------------------------------------ - public runClone = runClone; - public runSaveAs = runSaveAs; + public runInteractiveSave = runInteractiveSave; public runQuickSave = runQuickSave; public showSettings = showSettings; diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index c32c8d6d40331..6c7c5fef20daf 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -43,9 +43,13 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` labelType="label" > { private warning = React.createRef(); private formId = generateId('form'); + private savedObjectTitleInputRef = React.createRef(); public readonly state = { title: this.props.title, @@ -89,6 +90,13 @@ export class SavedObjectSaveModal extends React.Component hasAttemptedSubmit: false, }; + public componentDidMount() { + setTimeout(() => { + // defer so input focus ref value has been populated + this.savedObjectTitleInputRef.current?.focus(); + }, 0); + } + public render() { const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, hasAttemptedSubmit } = this.state; const duplicateWarningId = generateId(); @@ -111,7 +119,7 @@ export class SavedObjectSaveModal extends React.Component > /> } color="warning" - data-test-subj="titleDupicateWarnMsg" + data-test-subj="titleDuplicateWarnMsg" id={duplicateWarningId} >

diff --git a/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx index 5d04e5b10ed85..9013cec29997a 100644 --- a/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/show_saved_object_save_modal.tsx @@ -7,9 +7,8 @@ */ import React, { FC, PropsWithChildren } from 'react'; -import ReactDOM from 'react-dom'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { getAnalytics, getI18n, getTheme } from '../kibana_services'; /** @@ -34,34 +33,42 @@ export function showSaveModal( saveModal: React.ReactElement, Wrapper?: FC> ) { - const container = document.createElement('div'); - const closeModal = () => { - ReactDOM.unmountComponentAtNode(container); - document.body.removeChild(container); - saveModal.props.onClose?.(); - }; + // initialize variable that will hold reference for unmount + // eslint-disable-next-line prefer-const + let unmount: ReturnType>; - const onSave = saveModal.props.onSave; + const mount = toMountPoint( + React.createElement(function createSavedObjectModal() { + const closeModal = () => { + unmount(); + // revert control back to caller after cleaning up modal + setTimeout(() => { + saveModal.props.onClose?.(); + }, 0); + }; - const onSaveConfirmed: MinimalSaveModalProps['onSave'] = async (...args) => { - const response = await onSave(...args); - // close modal if we either hit an error or the saved object got an id - if (Boolean(isSuccess(response) ? response.id : response.error)) { - closeModal(); - } - return response; - }; - document.body.appendChild(container); - const element = React.cloneElement(saveModal, { - onSave: onSaveConfirmed, - onClose: closeModal, - }); + const onSave = saveModal.props.onSave; - const I18nContext = getI18n().Context; - ReactDOM.render( - - {Wrapper ? {element} : element} - , - container + const onSaveConfirmed: MinimalSaveModalProps['onSave'] = async (...args) => { + const response = await onSave(...args); + // close modal if we either hit an error or the saved object got an id + if (Boolean(isSuccess(response) ? response.id : response.error)) { + closeModal(); + } + return response; + }; + + const augmentedElement = React.cloneElement(saveModal, { + onSave: onSaveConfirmed, + onClose: closeModal, + }); + + return React.createElement(Wrapper ?? React.Fragment, { + children: augmentedElement, + }); + }), + { analytics: getAnalytics(), theme: getTheme(), i18n: getI18n() } ); + + unmount = mount(document.createElement('div')); } diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index 39557cef8c5b1..4119011216816 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -13,7 +13,6 @@ "@kbn/i18n-react", "@kbn/utility-types", "@kbn/ui-theme", - "@kbn/react-kibana-context-render", "@kbn/react-kibana-mount", ], "exclude": [ diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts index 32d198c7d5e02..268bf06bc4b17 100644 --- a/test/accessibility/apps/dashboard.ts +++ b/test/accessibility/apps/dashboard.ts @@ -57,7 +57,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('save the dashboard', async () => { - await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName, { saveAsNew: false }); await a11y.testAppSnapshot(); }); @@ -128,7 +128,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Make a clone of the dashboard', async () => { - await PageObjects.dashboard.clickClone(); + await PageObjects.dashboard.duplicateDashboard(); await a11y.testAppSnapshot(); }); diff --git a/test/functional/apps/dashboard/group1/embeddable_rendering.ts b/test/functional/apps/dashboard/group1/embeddable_rendering.ts index 45408a8846c17..3ebef1fda00cc 100644 --- a/test/functional/apps/dashboard/group1/embeddable_rendering.ts +++ b/test/functional/apps/dashboard/group1/embeddable_rendering.ts @@ -154,6 +154,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); await PageObjects.dashboard.saveDashboard('embeddable rendering test', { + saveAsNew: true, storeTimeWithDashboard: true, }); }); diff --git a/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts index 276a3f29e8fd5..c580d69fa0453 100644 --- a/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts +++ b/test/functional/apps/dashboard/group2/dashboard_filter_bar.ts @@ -167,9 +167,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterKey = 'bytes'; await filterBar.toggleFilterPinned(filterKey); await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.saveDashboard('saved with pinned filters', { - saveAsNew: true, - }); + await PageObjects.dashboard.saveDashboard('saved with pinned filters'); expect(await filterBar.isFilterPinned(filterKey)).to.be(true); await pieChart.expectPieSliceCount(1); }); diff --git a/test/functional/apps/dashboard/group3/bwc_shared_urls.ts b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts index 8f89ed302c67c..fece4367abf04 100644 --- a/test/functional/apps/dashboard/group3/bwc_shared_urls.ts +++ b/test/functional/apps/dashboard/group3/bwc_shared_urls.ts @@ -103,6 +103,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('loads a saved dashboard', async function () { await PageObjects.dashboard.saveDashboard('saved with colors', { + saveAsNew: true, storeTimeWithDashboard: true, }); diff --git a/test/functional/apps/dashboard/group3/dashboard_state.ts b/test/functional/apps/dashboard/group3/dashboard_state.ts index 423adf781835c..df80d35ce2a64 100644 --- a/test/functional/apps/dashboard/group3/dashboard_state.ts +++ b/test/functional/apps/dashboard/group3/dashboard_state.ts @@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const overwriteColor = '#d36086'; await PageObjects.visChart.selectNewLegendColorChoice(overwriteColor); - await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName, { saveAsNew: false }); await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.loadSavedDashboard(dashboardName); diff --git a/test/functional/apps/dashboard/group4/dashboard_clone.ts b/test/functional/apps/dashboard/group4/dashboard_clone.ts index c62c1a4195900..438f01061dda8 100644 --- a/test/functional/apps/dashboard/group4/dashboard_clone.ts +++ b/test/functional/apps/dashboard/group4/dashboard_clone.ts @@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); await PageObjects.dashboard.saveDashboard(dashboardName); - await PageObjects.dashboard.clickClone(); + await PageObjects.dashboard.duplicateDashboard(); await PageObjects.dashboard.gotoDashboardLandingPage(); await listingTable.searchAndExpectItemsCount('dashboard', clonedDashboardName, 1); }); diff --git a/test/functional/apps/dashboard/group4/dashboard_listing.ts b/test/functional/apps/dashboard/group4/dashboard_listing.ts index ed8cc60cb5884..36e99d0e5c8c1 100644 --- a/test/functional/apps/dashboard/group4/dashboard_listing.ts +++ b/test/functional/apps/dashboard/group4/dashboard_listing.ts @@ -164,7 +164,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('stays on listing page if title matches two dashboards', async function () { await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.saveDashboard('two words', { needsConfirm: true }); + await PageObjects.dashboard.saveDashboard('two words', { + saveAsNew: true, + needsConfirm: true, + }); await PageObjects.dashboard.gotoDashboardLandingPage(); const currentUrl = await browser.getCurrentUrl(); const newUrl = currentUrl + '&title=two%20words'; diff --git a/test/functional/apps/dashboard/group4/dashboard_save.ts b/test/functional/apps/dashboard/group4/dashboard_save.ts index 4bbf697974953..9b7bcc77406c9 100644 --- a/test/functional/apps/dashboard/group4/dashboard_save.ts +++ b/test/functional/apps/dashboard/group4/dashboard_save.ts @@ -25,126 +25,144 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.initTests(); }); - it('warns on duplicate name for new dashboard', async function () { - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.saveDashboard(dashboardName); + describe('create new', () => { + it('warns on duplicate name for new dashboard', async function () { + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.saveDashboard(dashboardName); + + await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: false }); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.enterDashboardSaveModalApplyUpdatesAndClickSave(dashboardName, { + waitDialogIsClosed: false, + }); + await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); + }); - await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: false }); + it('does not save on reject confirmation', async function () { + await PageObjects.dashboard.cancelSave(); + await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { - waitDialogIsClosed: false, + await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1); }); - await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); - }); - it('does not save on reject confirmation', async function () { - await PageObjects.dashboard.cancelSave(); - await PageObjects.dashboard.gotoDashboardLandingPage(); + it('Saves on confirm duplicate title warning', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.enterDashboardSaveModalApplyUpdatesAndClickSave(dashboardName, { + waitDialogIsClosed: false, + }); - await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1); - }); + await PageObjects.dashboard.ensureDuplicateTitleCallout(); + await PageObjects.dashboard.clickSave(); - it('Saves on confirm duplicate title warning', async function () { - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { - waitDialogIsClosed: false, - }); + // This is important since saving a new dashboard will cause a refresh of the page. We have to + // wait till it finishes reloading or it might reload the url after simulating the + // dashboard landing page click. + await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.ensureDuplicateTitleCallout(); - await PageObjects.dashboard.clickSave(); + // after saving a new dashboard, the app state must be removed + await await PageObjects.dashboard.expectAppStateRemovedFromURL(); - // This is important since saving a new dashboard will cause a refresh of the page. We have to - // wait till it finishes reloading or it might reload the url after simulating the - // dashboard landing page click. - await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.gotoDashboardLandingPage(); - // after saving a new dashboard, the app state must be removed - await await PageObjects.dashboard.expectAppStateRemovedFromURL(); + await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 2); + }); - await PageObjects.dashboard.gotoDashboardLandingPage(); + it('Saves new Dashboard using the Enter key', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.enterDashboardTitleAndPressEnter(dashboardNameEnterKey); - await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 2); - }); + // This is important since saving a new dashboard will cause a refresh of the page. We have to + // wait till it finishes reloading or it might reload the url after simulating the + // dashboard landing page click. + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.gotoDashboardLandingPage(); - it('Does not warn when you save an existing dashboard with the title it already has, and that title is a duplicate', async function () { - await listingTable.clickItemLink('dashboard', dashboardName); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.saveDashboard(dashboardName); - - await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: false }); + await listingTable.searchAndExpectItemsCount('dashboard', dashboardNameEnterKey, 1); + }); }); - it('Warns you when you Save as New Dashboard, and the title is a duplicate', async function () { - await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { - saveAsNew: true, + describe('quick save', () => { + it('Does not show quick save menu item on a new dashboard', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.expectMissingQuickSaveOption(); }); - await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); + it('Does not show dashboard save modal when on quick save', async function () { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.saveDashboard('test quick save'); - await PageObjects.dashboard.cancelSave(); - }); + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.expectExistsQuickSaveOption(); + await dashboardAddPanel.clickMarkdownQuickButton(); + await PageObjects.visualize.saveVisualizationAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.clickQuickSave(); - it('Does not warn when only the prefix matches', async function () { - await PageObjects.dashboard.saveDashboard(dashboardName.split(' ')[0]); + await testSubjects.existOrFail('saveDashboardSuccess'); + }); - await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: false }); + it('Stays in edit mode after performing a quick save', async function () { + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); + }); }); - it('Warns when case is different', async function () { - await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName.toUpperCase(), { - waitDialogIsClosed: false, + describe('duplication (edit mode)', () => { + it('Warns you when you Save as New Dashboard, and the title is a duplicate', async function () { + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.enterDashboardSaveModalApplyUpdatesAndClickSave(dashboardName, { + waitDialogIsClosed: false, + }); + + await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); + + await PageObjects.dashboard.cancelSave(); }); - await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); + it('Does not warn when only the prefix matches', async function () { + await PageObjects.dashboard.saveDashboard(dashboardName.split(' ')[0]); - await PageObjects.dashboard.cancelSave(); - }); + await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: false }); + }); - it('Saves new Dashboard using the Enter key', async function () { - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.enterDashboardTitleAndPressEnter(dashboardNameEnterKey); + it('Warns when case is different', async function () { + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.enterDashboardSaveModalApplyUpdatesAndClickSave( + dashboardName.toUpperCase(), + { + waitDialogIsClosed: false, + } + ); - // This is important since saving a new dashboard will cause a refresh of the page. We have to - // wait till it finishes reloading or it might reload the url after simulating the - // dashboard landing page click. - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true }); - await listingTable.searchAndExpectItemsCount('dashboard', dashboardNameEnterKey, 1); + await PageObjects.dashboard.cancelSave(); + }); }); - it('Does not show quick save menu item on a new dashboard', async function () { - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.expectMissingQuickSaveOption(); - }); + describe('flyout settings', () => { + const dashboardNameFlyout = 'Dashboard Save Test with Flyout'; - it('Does not show dashboard save modal when on quick save', async function () { - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); - await PageObjects.dashboard.gotoDashboardLandingPage(); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.dashboard.saveDashboard('test quick save'); - - await PageObjects.dashboard.switchToEditMode(); - await PageObjects.dashboard.expectExistsQuickSaveOption(); - await dashboardAddPanel.clickMarkdownQuickButton(); - await PageObjects.visualize.saveVisualizationAndReturn(); - await PageObjects.dashboard.waitForRenderComplete(); - await PageObjects.dashboard.clickQuickSave(); - - await testSubjects.existOrFail('saveDashboardSuccess'); - }); + it('Does not warn when you save an existing dashboard with the title it already has', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.enterDashboardTitleAndPressEnter(dashboardNameFlyout); - it('Stays in edit mode after performing a quick save', async function () { - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('dashboardQuickSaveMenuItem'); + // This is important since saving a new dashboard will cause a refresh of the page. We have to + // wait till it finishes reloading or it might reload the url after simulating the + // dashboard landing page click. + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.modifyExistingDashboardDetails(dashboardNameFlyout); + }); }); }); } diff --git a/test/functional/apps/dashboard/group4/dashboard_time.ts b/test/functional/apps/dashboard/group4/dashboard_time.ts index 2b35c5e78f331..eb689353bde41 100644 --- a/test/functional/apps/dashboard/group4/dashboard_time.ts +++ b/test/functional/apps/dashboard/group4/dashboard_time.ts @@ -33,7 +33,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.addVisualizations([ PageObjects.dashboard.getTestVisualizationNames()[0], ]); - await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: false }); + await PageObjects.dashboard.saveDashboard(dashboardName, { + storeTimeWithDashboard: false, + saveAsNew: true, + }); }); it('Does not set the time picker on open', async () => { @@ -51,7 +54,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('is saved with time', async function () { await PageObjects.dashboard.switchToEditMode(); await PageObjects.timePicker.setDefaultAbsoluteRange(); - await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); + await PageObjects.dashboard.saveDashboard(dashboardName, { + storeTimeWithDashboard: true, + saveAsNew: false, + }); }); it('sets time on open', async function () { diff --git a/test/functional/apps/dashboard/group5/legacy_urls.ts b/test/functional/apps/dashboard/group5/legacy_urls.ts index 03dabfe87ba2f..0d09ba7a7ca79 100644 --- a/test/functional/apps/dashboard/group5/legacy_urls.ts +++ b/test/functional/apps/dashboard/group5/legacy_urls.ts @@ -41,7 +41,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.navigateToApp(); await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie'); - await PageObjects.dashboard.saveDashboard('legacyTest', { waitDialogIsClosed: true }); + await PageObjects.dashboard.saveDashboard('legacyTest', { + waitDialogIsClosed: true, + saveAsNew: true, + }); await PageObjects.header.waitUntilLoadingHasFinished(); const currentUrl = await browser.getCurrentUrl(); await log.debug(`Current url is ${currentUrl}`); diff --git a/test/functional/apps/dashboard/group6/view_edit.ts b/test/functional/apps/dashboard/group6/view_edit.ts index 1d4f2482cc9c0..93748e07e2536 100644 --- a/test/functional/apps/dashboard/group6/view_edit.ts +++ b/test/functional/apps/dashboard/group6/view_edit.ts @@ -63,12 +63,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(isInViewMode).to.be(false); }); + describe('save as new', () => { + it('keeps duplicated dashboard in edit mode', async () => { + await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); + await PageObjects.dashboard.duplicateDashboard('edit'); + const isViewMode = await PageObjects.dashboard.getIsInViewMode(); + expect(isViewMode).to.equal(false); + }); + }); + describe('save', function () { - it('auto exits out of edit mode', async function () { + it('keeps dashboard in edit mode', async function () { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); - await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName, { + storeTimeWithDashboard: true, + saveAsNew: false, + }); const isViewMode = await PageObjects.dashboard.getIsInViewMode(); - expect(isViewMode).to.equal(true); + expect(isViewMode).to.equal(false); }); }); @@ -85,6 +97,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true, + saveAsNew: false, }); await PageObjects.timePicker.setAbsoluteRange( @@ -170,7 +183,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Sep 19, 2013 @ 06:31:44.000', 'Sep 19, 2013 @ 06:31:44.000' ); - await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName, { saveAsNew: false }); await PageObjects.dashboard.switchToEditMode(); await PageObjects.timePicker.setAbsoluteRange( 'Sep 19, 2015 @ 06:31:44.000', @@ -180,6 +193,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.clickCancelOnModal(); await PageObjects.dashboard.saveDashboard(dashboardName, { + saveAsNew: false, storeTimeWithDashboard: true, }); @@ -197,8 +211,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('when time changed is stored with dashboard', async function () { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); await PageObjects.timePicker.setDefaultDataRange(); - await PageObjects.dashboard.saveDashboard(dashboardName); - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.dashboard.saveDashboard(dashboardName, { saveAsNew: false }); await PageObjects.timePicker.setAbsoluteRange( 'Sep 19, 2013 @ 06:31:44.000', 'Sep 19, 2013 @ 06:31:44.000' @@ -208,7 +221,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.clickCancelOutOfEditMode(false); await PageObjects.common.clickCancelOnModal(); - await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: true }); + await PageObjects.dashboard.saveDashboard(dashboardName, { + storeTimeWithDashboard: true, + saveAsNew: false, + }); await PageObjects.dashboard.loadSavedDashboard(dashboardName); @@ -222,7 +238,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Does not show lose changes warning', function () { it('when time changed is not stored with dashboard', async function () { await PageObjects.dashboard.gotoDashboardEditMode(dashboardName); - await PageObjects.dashboard.saveDashboard(dashboardName, { storeTimeWithDashboard: false }); + await PageObjects.dashboard.saveDashboard(dashboardName, { + storeTimeWithDashboard: false, + saveAsNew: false, + }); await PageObjects.timePicker.setAbsoluteRange( 'Oct 19, 2014 @ 06:31:44.000', 'Dec 19, 2014 @ 06:31:44.000' diff --git a/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts b/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts index 40e75ac70ff09..9f5862c5fbbcd 100644 --- a/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts +++ b/test/functional/apps/dashboard_elements/controls/common/control_group_apply_button.ts @@ -37,7 +37,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie'); // save the dashboard before adding controls - await dashboard.saveDashboard('Test Control Group Apply Button', { exitFromEditMode: false }); + await dashboard.saveDashboard('Test Control Group Apply Button', { + exitFromEditMode: false, + saveAsNew: true, + }); await header.waitUntilLoadingHasFinished(); await dashboard.waitForRenderComplete(); await dashboard.expectMissingUnsavedChangesBadge(); diff --git a/test/functional/apps/dashboard_elements/controls/common/range_slider.ts b/test/functional/apps/dashboard_elements/controls/common/range_slider.ts index 8819f143a3dc7..06e67b9d8df91 100644 --- a/test/functional/apps/dashboard_elements/controls/common/range_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/common/range_slider.ts @@ -57,7 +57,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.preserveCrossAppState(); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); - await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); + await dashboard.saveDashboard(DASHBOARD_NAME, { + exitFromEditMode: false, + saveAsNew: true, + }); }); after(async () => { diff --git a/test/functional/apps/dashboard_elements/controls/common/replace_controls.ts b/test/functional/apps/dashboard_elements/controls/common/replace_controls.ts index 974f5e942d42a..e29b53c95d4d0 100644 --- a/test/functional/apps/dashboard_elements/controls/common/replace_controls.ts +++ b/test/functional/apps/dashboard_elements/controls/common/replace_controls.ts @@ -56,7 +56,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); await timePicker.setDefaultDataRange(); - await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); + await dashboard.saveDashboard(DASHBOARD_NAME, { + exitFromEditMode: false, + saveAsNew: true, + }); }); after(async () => { diff --git a/test/functional/apps/dashboard_elements/controls/common/time_slider.ts b/test/functional/apps/dashboard_elements/controls/common/time_slider.ts index c860af183d64e..32f6e39e6b3c2 100644 --- a/test/functional/apps/dashboard_elements/controls/common/time_slider.ts +++ b/test/functional/apps/dashboard_elements/controls/common/time_slider.ts @@ -62,7 +62,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Oct 22, 2018 @ 00:00:00.000', 'Dec 3, 2018 @ 00:00:00.000' ); - await dashboard.saveDashboard('test time slider control', { exitFromEditMode: false }); + await dashboard.saveDashboard('test time slider control', { + exitFromEditMode: false, + saveAsNew: true, + }); }); it('can create a new time slider control from a blank state', async () => { diff --git a/test/functional/apps/dashboard_elements/controls/options_list/index.ts b/test/functional/apps/dashboard_elements/controls/options_list/index.ts index 966dbd91b7705..5f589129ed279 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list/index.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list/index.ts @@ -35,6 +35,7 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid await timePicker.setDefaultDataRange(); await elasticChart.setNewChartUiDebugFlag(); await dashboard.saveDashboard(OPTIONS_LIST_DASHBOARD_NAME, { + saveAsNew: true, exitFromEditMode: false, storeTimeWithDashboard: true, }); diff --git a/test/functional/apps/dashboard_elements/links/links_create_edit.ts b/test/functional/apps/dashboard_elements/links/links_create_edit.ts index ac68b2c00be8c..97e78ca2fb0a4 100644 --- a/test/functional/apps/dashboard_elements/links/links_create_edit.ts +++ b/test/functional/apps/dashboard_elements/links/links_create_edit.ts @@ -44,7 +44,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.preserveCrossAppState(); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); - await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); + await dashboard.saveDashboard(DASHBOARD_NAME, { + exitFromEditMode: false, + saveAsNew: true, + }); await dashboard.loadSavedDashboard(DASHBOARD_NAME); await dashboard.switchToEditMode(); }); diff --git a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts index b370141f69728..befce148bdf81 100644 --- a/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts +++ b/test/functional/apps/discover/embeddable/_saved_search_embeddable.ts @@ -68,6 +68,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.checkCurrentRowsPerPageToBe(100); await PageObjects.dashboard.saveDashboard(dashboardName, { + saveAsNew: true, waitDialogIsClosed: true, exitFromEditMode: false, }); @@ -78,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.changeRowsPerPageTo(10); - await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName, { saveAsNew: false }); await refreshDashboardPage(); await dataGrid.checkCurrentRowsPerPageToBe(10); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 66ce19c3d6d4f..408054a5c6114 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -229,9 +229,20 @@ export class DashboardPageObject extends FtrService { await this.expectExistsDashboardLandingPage(); } - public async clickClone() { - this.log.debug('Clicking clone'); - await this.testSubjects.click('dashboardClone'); + public async duplicateDashboard(dashboardNameOverride?: string) { + this.log.debug('Clicking duplicate'); + + await this.testSubjects.click('dashboardInteractiveSaveMenuItem'); + + if (dashboardNameOverride) { + this.log.debug('entering dashboard duplicate override title'); + await this.testSubjects.setValue('savedObjectTitle', dashboardNameOverride); + } + + await this.clickSave(); + + // Confirm that the Dashboard has actually been saved + await this.testSubjects.existOrFail('saveDashboardSuccess'); } /** @@ -240,9 +251,9 @@ export class DashboardPageObject extends FtrService { */ public async expectDuplicateTitleWarningDisplayed({ displayed = true }) { if (displayed) { - await this.testSubjects.existOrFail('titleDupicateWarnMsg'); + await this.testSubjects.existOrFail('titleDuplicateWarnMsg'); } else { - await this.testSubjects.missingOrFail('titleDupicateWarnMsg'); + await this.testSubjects.missingOrFail('titleDuplicateWarnMsg'); } } @@ -460,19 +471,62 @@ export class DashboardPageObject extends FtrService { } /** - * Save the current dashboard with the specified name and options and + * @description opens the dashboard settings flyout to modify an existing dashboard + */ + public async modifyExistingDashboardDetails( + dashboard: string, + saveOptions: Pick = {} + ) { + await this.openSettingsFlyout(); + + await this.retry.try(async () => { + this.log.debug('entering new title'); + await this.testSubjects.setValue('dashboardTitleInput', dashboard); + + if (saveOptions.storeTimeWithDashboard !== undefined) { + await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard); + } + + if (saveOptions.tags) { + const tagsComboBox = await this.testSubjects.find('comboBoxInput'); + for (const tagName of saveOptions.tags) { + await this.comboBox.setElement(tagsComboBox, tagName); + } + } + + this.log.debug('DashboardPage.applyCustomization'); + await this.testSubjects.click('applyCustomizeDashboardButton'); + + if (saveOptions.needsConfirm) { + await this.ensureDuplicateTitleCallout(); + await this.testSubjects.click('applyCustomizeDashboardButton'); + } + + this.log.debug('isCustomizeDashboardLoadingIndicatorVisible'); + return await this.testSubjects.exists('dashboardUnsavedChangesBadge', { timeout: 1500 }); + }); + } + + /** + * @description Save the current dashboard with the specified name and options and * verify that the save was successful, close the toast and return the * toast message - * - * @param dashboardName {String} - * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, needsConfirm: false, waitDialogIsClosed: boolean }} */ public async saveDashboard( dashboardName: string, - saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true, exitFromEditMode: true } + saveOptions: SaveDashboardOptions = { + waitDialogIsClosed: true, + exitFromEditMode: true, + saveAsNew: true, + } ) { await this.retry.try(async () => { - await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions); + if (saveOptions.saveAsNew) { + await this.enterDashboardSaveModalApplyUpdatesAndClickSave(dashboardName, saveOptions); + } else { + await this.modifyExistingDashboardDetails(dashboardName, saveOptions); + await this.clickQuickSave(); + } if (saveOptions.needsConfirm) { await this.ensureDuplicateTitleCallout(); @@ -482,9 +536,14 @@ export class DashboardPageObject extends FtrService { // Confirm that the Dashboard has actually been saved await this.testSubjects.existOrFail('saveDashboardSuccess'); }); - const message = await this.toasts.getTitleAndDismiss(); - await this.header.waitUntilLoadingHasFinished(); - await this.common.waitForSaveModalToClose(); + + let message; + + if (saveOptions.saveAsNew) { + message = await this.toasts.getTitleAndDismiss(); + await this.header.waitUntilLoadingHasFinished(); + await this.common.waitForSaveModalToClose(); + } const isInViewMode = await this.testSubjects.exists('dashboardEditMode'); if (saveOptions.exitFromEditMode && !isInViewMode) { @@ -506,20 +565,20 @@ export class DashboardPageObject extends FtrService { } /** - * - * @param dashboardTitle {String} - * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, waitDialogIsClosed: boolean}} + * @description populates the duplicate dashboard modal */ - public async enterDashboardTitleAndClickSave( + public async enterDashboardSaveModalApplyUpdatesAndClickSave( dashboardTitle: string, - saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true } + saveOptions: Omit = { waitDialogIsClosed: true } ) { const isSaveModalOpen = await this.testSubjects.exists('savedObjectSaveModal', { timeout: 2000, }); + if (!isSaveModalOpen) { - await this.testSubjects.click('dashboardSaveMenuItem'); + await this.testSubjects.click('dashboardInteractiveSaveMenuItem'); } + const modalDialog = await this.testSubjects.find('savedObjectSaveModal'); this.log.debug('entering new title'); @@ -529,11 +588,6 @@ export class DashboardPageObject extends FtrService { await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard); } - const saveAsNewCheckboxExists = await this.testSubjects.exists('saveAsNewCheckbox'); - if (saveAsNewCheckboxExists) { - await this.setSaveAsNewCheckBox(Boolean(saveOptions.saveAsNew)); - } - if (saveOptions.tags) { await this.selectDashboardTags(saveOptions.tags); } @@ -545,7 +599,7 @@ export class DashboardPageObject extends FtrService { } public async ensureDuplicateTitleCallout() { - await this.testSubjects.existOrFail('titleDupicateWarnMsg'); + await this.testSubjects.existOrFail('titleDuplicateWarnMsg'); } public async selectDashboardTags(tagNames: string[]) { @@ -560,7 +614,7 @@ export class DashboardPageObject extends FtrService { * @param dashboardTitle {String} */ public async enterDashboardTitleAndPressEnter(dashboardTitle: string) { - await this.testSubjects.click('dashboardSaveMenuItem'); + await this.testSubjects.click('dashboardInteractiveSaveMenuItem'); const modalDialog = await this.testSubjects.find('savedObjectSaveModal'); this.log.debug('entering new title'); @@ -745,7 +799,7 @@ export class DashboardPageObject extends FtrService { } public async expectMissingSaveOption() { - await this.testSubjects.missingOrFail('dashboardSaveMenuItem'); + await this.testSubjects.missingOrFail('dashboardInteractiveSaveMenuItem'); } public async expectMissingQuickSaveOption() { diff --git a/x-pack/performance/journeys_e2e/dashboard_listing_page.ts b/x-pack/performance/journeys_e2e/dashboard_listing_page.ts index bad94790b2805..34e9d8a34927a 100644 --- a/x-pack/performance/journeys_e2e/dashboard_listing_page.ts +++ b/x-pack/performance/journeys_e2e/dashboard_listing_page.ts @@ -36,9 +36,9 @@ export const journey = new Journey({ await kibanaPage.waitForListViewTable(); await deletedDashboard.waitFor({ state: 'detached' }); }) - .step('Add dashboard', async ({ page, inputDelays }) => { + .step('Add dashboard', async ({ page, inputDelays }) => { await page.click(subj('newItemButton')); - await page.click(subj('dashboardSaveMenuItem')); + await page.click(subj('dashboardInteractiveSaveMenuItem')); await page.type(subj('savedObjectTitle'), `foobar dashboard ${uuidv4()}`, { delay: inputDelays.TYPING, }); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f68a59f484638..8c5b395bdd3f3 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -1313,16 +1313,16 @@ "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "Le filtre temporel est défini sur l’option sélectionnée chaque fois que ce tableau de bord est chargé.", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel": "Enregistrer la plage temporelle avec le tableau de bord", "dashboard.topNave.cancelButtonAriaLabel": "Basculer en mode Affichage", - "dashboard.topNave.cloneButtonAriaLabel": "cloner", - "dashboard.topNave.cloneConfigDescription": "Créer une copie du tableau de bord", + "dashboard.topNave.viewModeInteractiveSaveButtonAriaLabel": "cloner", + "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "Créer une copie du tableau de bord", "dashboard.topNave.editButtonAriaLabel": "modifier", "dashboard.topNave.editConfigDescription": "Basculer en mode Édition", "dashboard.topNave.fullScreenButtonAriaLabel": "plein écran", "dashboard.topNave.fullScreenConfigDescription": "Mode Plein écran", "dashboard.topNave.resetChangesButtonAriaLabel": "Réinitialiser", "dashboard.topNave.resetChangesConfigDescription": "Réinitialiser les modifications apportées au tableau de bord", - "dashboard.topNave.saveAsButtonAriaLabel": "enregistrer sous", - "dashboard.topNave.saveAsConfigDescription": "Enregistrer en tant que nouveau tableau de bord", + "dashboard.topNave.editModeInteractiveSaveButtonAriaLabel": "enregistrer sous", + "dashboard.topNave.editModeInteractiveSaveConfigDescription": "Enregistrer en tant que nouveau tableau de bord", "dashboard.topNave.saveButtonAriaLabel": "enregistrer", "dashboard.topNave.saveConfigDescription": "Enregistrer le tableau de bord sans invite de confirmation", "dashboard.topNave.settingsButtonAriaLabel": "les paramètres d'index suivants déclassés ?", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e7d393cd5595c..48a3eefcb30ec 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1313,16 +1313,16 @@ "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "有効化すると、ダッシュボードが読み込まれるごとに現在選択された時刻の時間フィルターが変更されます。", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel": "ダッシュボードに時刻を保存", "dashboard.topNave.cancelButtonAriaLabel": "表示モードに切り替える", - "dashboard.topNave.cloneButtonAriaLabel": "クローンを作成", - "dashboard.topNave.cloneConfigDescription": "ダッシュボードのコピーを作成します", + "dashboard.topNave.viewModeInteractiveSaveButtonAriaLabel": "クローンを作成", + "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "ダッシュボードのコピーを作成します", "dashboard.topNave.editButtonAriaLabel": "編集", "dashboard.topNave.editConfigDescription": "編集モードに切り替えます", "dashboard.topNave.fullScreenButtonAriaLabel": "全画面", "dashboard.topNave.fullScreenConfigDescription": "全画面モード", "dashboard.topNave.resetChangesButtonAriaLabel": "リセット", "dashboard.topNave.resetChangesConfigDescription": "ダッシュボードの変更をリセット", - "dashboard.topNave.saveAsButtonAriaLabel": "名前を付けて保存", - "dashboard.topNave.saveAsConfigDescription": "新しいダッシュボードとして保存", + "dashboard.topNave.editModeInteractiveSaveButtonAriaLabel": "名前を付けて保存", + "dashboard.topNave.editModeInteractiveSaveConfigDescription": "新しいダッシュボードとして保存", "dashboard.topNave.saveButtonAriaLabel": "保存", "dashboard.topNave.saveConfigDescription": "プロンプトを表示せずにダッシュボードをクイック保存", "dashboard.topNave.settingsButtonAriaLabel": "設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 10435bf1bdfc8..abaaacf435a14 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1315,16 +1315,16 @@ "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "每次加载此仪表板时,都会将时间筛选更改为当前选定的时间。", "dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel": "将时间随仪表板保存", "dashboard.topNave.cancelButtonAriaLabel": "切换到查看模式", - "dashboard.topNave.cloneButtonAriaLabel": "克隆", - "dashboard.topNave.cloneConfigDescription": "创建仪表板的副本", + "dashboard.topNave.viewModeInteractiveSaveButtonAriaLabel": "克隆", + "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "创建仪表板的副本", "dashboard.topNave.editButtonAriaLabel": "编辑", "dashboard.topNave.editConfigDescription": "切换到编辑模式", "dashboard.topNave.fullScreenButtonAriaLabel": "全屏", "dashboard.topNave.fullScreenConfigDescription": "全屏模式", "dashboard.topNave.resetChangesButtonAriaLabel": "重置", "dashboard.topNave.resetChangesConfigDescription": "重置对仪表板所做的更改", - "dashboard.topNave.saveAsButtonAriaLabel": "另存为", - "dashboard.topNave.saveAsConfigDescription": "另存为新仪表板", + "dashboard.topNave.editModeInteractiveSaveButtonAriaLabel": "另存为", + "dashboard.topNave.editModeInteractiveSaveConfigDescription": "另存为新仪表板", "dashboard.topNave.saveButtonAriaLabel": "保存", "dashboard.topNave.saveConfigDescription": "没有任何提示,快速保存您的仪表板", "dashboard.topNave.settingsButtonAriaLabel": "设置", diff --git a/x-pack/test/accessibility/apps/group1/dashboard_links.ts b/x-pack/test/accessibility/apps/group1/dashboard_links.ts index 5e5d00dd34b94..3ec6df1b880ca 100644 --- a/x-pack/test/accessibility/apps/group1/dashboard_links.ts +++ b/x-pack/test/accessibility/apps/group1/dashboard_links.ts @@ -32,7 +32,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await common.navigateToApp('dashboard'); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); - await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); + await dashboard.saveDashboard(DASHBOARD_NAME, { + exitFromEditMode: false, + saveAsNew: true, + }); }); after(async () => { diff --git a/x-pack/test/functional/apps/dashboard/group1/created_by.ts b/x-pack/test/functional/apps/dashboard/group1/created_by.ts index 00c280f28f96d..ed3798e1b8809 100644 --- a/x-pack/test/functional/apps/dashboard/group1/created_by.ts +++ b/x-pack/test/functional/apps/dashboard/group1/created_by.ts @@ -76,6 +76,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.dashboard.preserveCrossAppState(); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.saveDashboard(DASHBOARD_NAME, { + saveAsNew: true, waitDialogIsClosed: false, exitFromEditMode: false, }); 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 ccf738af3825d..cb73963a68e97 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 @@ -48,7 +48,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await panelActions.customizePanel(); await dashboardCustomizePanel.disableCustomTimeRange(); await dashboardCustomizePanel.clickSaveButton(); - await dashboard.saveDashboard('Dashboard with Pie Chart'); + await dashboard.saveDashboard('Dashboard with Pie Chart', { + saveAsNew: false, + exitFromEditMode: true, + }); }); it('action exists in panel context menu', async () => { @@ -85,7 +88,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardCustomizePanel.clickCommonlyUsedTimeRange('Last_90 days'); await dashboardCustomizePanel.clickSaveButton(); - await dashboard.saveDashboard('Dashboard with Pie Chart'); + await dashboard.saveDashboard('Dashboard with Pie Chart', { + saveAsNew: false, + exitFromEditMode: true, + }); await panelActions.openContextMenu(); await testSubjects.clickWhenNotDisabledWithoutRetry(ACTION_TEST_SUBJ); diff --git a/x-pack/test/functional/apps/discover/saved_search_embeddable.ts b/x-pack/test/functional/apps/discover/saved_search_embeddable.ts index fd546315a1fd9..3f3f58dcdaf16 100644 --- a/x-pack/test/functional/apps/discover/saved_search_embeddable.ts +++ b/x-pack/test/functional/apps/discover/saved_search_embeddable.ts @@ -93,6 +93,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.saveDashboard('Dashboard with deleted saved search', { waitDialogIsClosed: true, exitFromEditMode: false, + saveAsNew: true, }); await kibanaServer.savedObjects.delete({ type: 'search', diff --git a/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts b/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts index 761278e69025c..965474919e6fa 100644 --- a/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group4/show_underlying_data_dashboard.ts @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.save('Embedded Visualization', true, false, false, 'new'); await PageObjects.dashboard.saveDashboard(`Open in Discover Testing ${uuidv4()}`, { + saveAsNew: true, exitFromEditMode: true, }); @@ -68,6 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.save('Embedded Visualization', false); await PageObjects.dashboard.saveDashboard(`Open in Discover Testing ${uuidv4()}`, { + saveAsNew: false, exitFromEditMode: true, }); diff --git a/x-pack/test/functional/apps/managed_content/managed_content.ts b/x-pack/test/functional/apps/managed_content/managed_content.ts index 8f2ff247709cd..1bee3acd761bb 100644 --- a/x-pack/test/functional/apps/managed_content/managed_content.ts +++ b/x-pack/test/functional/apps/managed_content/managed_content.ts @@ -159,7 +159,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); - await PageObjects.dashboard.clickClone(); + await PageObjects.dashboard.duplicateDashboard(); await PageObjects.dashboard.waitForRenderComplete(); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts index 3f7e726543485..76f1f0fcd5584 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_dashboard.ts @@ -106,7 +106,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.discover.assertFieldStatsTableNotExists(); - await PageObjects.dashboard.saveDashboard(dashboardTitle); + await PageObjects.dashboard.saveDashboard(dashboardTitle, { saveAsNew: false }); }); }); } diff --git a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts index 18be683db1d05..5a4a71c6abb2d 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/dashboard_integration.ts @@ -85,6 +85,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.saveDashboard('my-new-dashboard', { + saveAsNew: true, waitDialogIsClosed: true, tags: ['tag-1', 'tag-3'], }); @@ -102,7 +103,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.dashboard.clickNewDashboard(); - await testSubjects.click('dashboardSaveMenuItem'); + await testSubjects.click('dashboardInteractiveSaveMenuItem'); await testSubjects.setValue('savedObjectTitle', 'dashboard-with-new-tag'); await testSubjects.click('savedObjectTagSelector'); @@ -148,6 +149,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.dashboard.switchToEditMode(); await PageObjects.dashboard.saveDashboard('dashboard 4 with real data (tag-1)', { + saveAsNew: false, waitDialogIsClosed: true, tags: ['tag-3'], }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts b/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts index 2cd16b00feb6a..84d67386a7555 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/embeddable/_saved_search_embeddable.ts @@ -81,6 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.saveDashboard(dashboardName, { waitDialogIsClosed: true, exitFromEditMode: false, + saveAsNew: true, }); await refreshDashboardPage(); @@ -89,7 +90,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.changeRowsPerPageTo(10); - await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.dashboard.saveDashboard(dashboardName, { saveAsNew: false }); await refreshDashboardPage(); await dataGrid.checkCurrentRowsPerPageToBe(10);