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
<img width="1728" alt="Screenshot 2024-04-16 at 15 59 10"
src="48dc4565-1f75-4f46-839c-8d76f4fedefe">

### Edit mode
<img width="1725" alt="Screenshot 2024-04-16 at 15 59 00"
src="1ac743ac-33b4-4f68-ab59-ad19ab58fa1c">

## After

#### Managed Dashboard

5072a501-8d16-4f25-9575-6f11fed6e580

#### View mode

610d0952-97f0-46b8-a0ea-1546a799d387

#### Edit mode

4f596c07-7bd1-4c5a-9131-0c78731cb113



<!-- ### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Eyo O. Eyo 2024-05-29 11:46:23 +02:00 committed by GitHub
parent d5842d766a
commit 690690ea21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 756 additions and 590 deletions

View file

@ -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',
}),
},

View file

@ -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 };
};

View file

@ -323,7 +323,11 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty data-test-subj="cancelCustomizeDashboardButton" onClick={onClose}>
<EuiButtonEmpty
flush="left"
data-test-subj="cancelCustomizeDashboardButton"
onClick={onClose}
>
<FormattedMessage
id="dashboard.embeddableApi.showSettings.flyout.cancelButtonTitle"
defaultMessage="Cancel"

View file

@ -9,4 +9,4 @@
export { showSettings } from './show_settings';
export { addFromLibrary } from './add_panel_from_library';
export { addOrUpdateEmbeddable } from './panel_management';
export { runSaveAs, runQuickSave, runClone } from './run_save_functions';
export { runQuickSave, runInteractiveSave } from './run_save_functions';

View file

@ -19,14 +19,14 @@ describe('extractTitleAndCount', () => {
]);
});
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]);
});
});

View file

@ -15,5 +15,5 @@ export const extractTitleAndCount = (title: string): [string, number] => {
return [baseTitle, Number(count)];
}
}
return [title, 1];
return [title, 0];
};

View file

@ -14,27 +14,44 @@ exports[`renders DashboardSaveModal 1`] = `
display="row"
hasChildLabel={true}
hasEmptyLabelSpace={false}
helpText={
<FormattedMessage
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
values={Object {}}
/>
}
labelType="label"
>
<EuiSwitch
checked={true}
data-test-subj="storeTimeWithDashboard"
label={
<FormattedMessage
defaultMessage="Store time with dashboard"
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
values={Object {}}
<EuiFlexGroup
alignItems="center"
gutterSize="s"
responsive={false}
>
<EuiFlexItem
grow={false}
>
<EuiSwitch
checked={true}
data-test-subj="storeTimeWithDashboard"
label={
<FormattedMessage
defaultMessage="Store time with dashboard"
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
values={Object {}}
/>
}
onChange={[Function]}
/>
}
onChange={[Function]}
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiIconTip
content={
<FormattedMessage
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
values={Object {}}
/>
}
position="top"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
</React.Fragment>
}

View file

@ -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;
}
type SaveDashboardHandler = (args: {
newTitle: string;
newDescription: string;
newCopyOnSave: boolean;
isTitleDuplicateConfirmed: boolean;
onTitleDuplicate: () => void;
}) => ReturnType<DashboardSaveModalProps['onSave']>;
export class DashboardSaveModal extends React.Component<Props, State> {
state: State = {
timeRestore: this.props.timeRestore,
tags: this.props.tags ?? [],
};
export const DashboardSaveModal: React.FC<DashboardSaveModalProps> = ({
customModalTitle,
description,
onClose,
onSave,
showCopyOnSave,
showStoreTimeOnSave = true,
tags,
title,
timeRestore,
}) => {
const [selectedTags, setSelectedTags] = React.useState<string[]>(tags ?? []);
const [persistSelectedTimeInterval, setPersistSelectedTimeInterval] = React.useState(timeRestore);
constructor(props: Props) {
super(props);
}
const saveDashboard = React.useCallback<SaveDashboardHandler>(
({ newTitle, newDescription, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => {
onSave({
newTitle,
newDescription,
newCopyOnSave,
newTimeRestore: persistSelectedTimeInterval,
isTitleDuplicateConfirmed,
onTitleDuplicate,
newTags: selectedTags,
});
},
[onSave, persistSelectedTimeInterval, selectedTags]
);
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,
});
};
onTimeRestoreChange = (event: any) => {
this.setState({
timeRestore: event.target.checked,
});
};
renderDashboardSaveOptions() {
const renderDashboardSaveOptions = useCallback(() => {
const {
savedObjectsTagging: { components },
} = pluginServices.getServices();
const tagSelector = components ? (
<components.SavedObjectSaveModalTagSelector
initialSelection={this.state.tags}
onTagsSelected={(tags) => {
this.setState({
tags,
});
initialSelection={selectedTags}
onTagsSelected={(selectedTagIds) => {
setSelectedTags(selectedTagIds);
}}
markOptional
/>
@ -103,46 +95,56 @@ export class DashboardSaveModal extends React.Component<Props, State> {
return (
<Fragment>
{tagSelector}
<EuiFormRow
helpText={
<FormattedMessage
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
/>
}
>
<EuiSwitch
data-test-subj="storeTimeWithDashboard"
checked={this.state.timeRestore}
onChange={this.onTimeRestoreChange}
label={
<FormattedMessage
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
defaultMessage="Store time with dashboard"
/>
}
/>
</EuiFormRow>
{showStoreTimeOnSave ? (
<EuiFormRow>
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiSwitch
data-test-subj="storeTimeWithDashboard"
checked={persistSelectedTimeInterval}
onChange={(event) => {
setPersistSelectedTimeInterval(event.target.checked);
}}
label={
<FormattedMessage
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowLabel"
defaultMessage="Store time with dashboard"
/>
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
content={
<FormattedMessage
id="dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText"
defaultMessage="This changes the time filter to the currently selected time each time this dashboard is loaded."
/>
}
position="top"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
) : null}
</Fragment>
);
}
}, [persistSelectedTimeInterval, selectedTags, showStoreTimeOnSave]);
render() {
return (
<SavedObjectSaveModal
onSave={this.saveDashboard}
onClose={this.props.onClose}
title={this.props.title}
description={this.props.description}
showDescription
showCopyOnSave={this.props.showCopyOnSave}
initialCopyOnSave={this.props.showCopyOnSave}
objectType={i18n.translate('dashboard.topNav.saveModal.objectType', {
defaultMessage: 'dashboard',
})}
options={this.renderDashboardSaveOptions()}
/>
);
}
}
return (
<SavedObjectSaveModal
onSave={saveDashboard}
onClose={onClose}
title={title}
description={description}
showDescription
showCopyOnSave={showCopyOnSave}
initialCopyOnSave={showCopyOnSave}
objectType={i18n.translate('dashboard.topNav.saveModal.objectType', {
defaultMessage: 'dashboard',
})}
customModalTitle={customModalTitle}
options={renderDashboardSaveOptions()}
/>
);
};

View file

@ -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<SaveDashboardReturn | undefined>((resolve) => {
if (managed) resolve(undefined);
const onSave = async ({
newTags,
newTitle,
newDescription,
newCopyOnSave,
newTimeRestore,
onTitleDuplicate,
isTitleDuplicateConfirmed,
}: DashboardSaveOptions): Promise<SaveDashboardReturn> => {
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 = (
<DashboardSaveModal
tags={currentState.tags}
title={currentState.title}
onClose={() => 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<SaveDashboardReturn | undefined>(async (resolve, reject) => {
try {
const [baseTitle, baseCount] = extractTitleAndCount(currentState.title);
let copyCount = baseCount;
let newTitle = `${baseTitle} (${copyCount})`;
while (
!(await checkForDuplicateDashboardTitle({
title: newTitle,
lastSavedTitle: currentState.title,
copyOnSave: true,
isTitleDuplicateConfirmed: false,
}))
) {
copyCount++;
newTitle = `${baseTitle} (${copyCount})`;
}
let stateToSave: DashboardContainerInput & {
controlGroupInput?: PersistableControlGroupInput;
} = currentState;
if (this.controlGroup) {
stateToSave = {
...stateToSave,
controlGroupInput: this.controlGroup.getPersistableInput(),
};
}
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;
}
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,
}
: {
error: saveResult.error,
};
} catch (error) {
reject(error);
return new Promise<SaveDashboardReturn | undefined>((resolve, reject) => {
if (interactionMode === ViewMode.EDIT && managed) {
resolve(undefined);
}
const onSaveAttempt = async ({
newTags,
newTitle,
newDescription,
newCopyOnSave,
newTimeRestore,
onTitleDuplicate,
isTitleDuplicateConfirmed,
}: DashboardSaveOptions): Promise<SaveDashboardReturn> => {
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,
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 dashboardStateToSave: DashboardContainerInput & {
controlGroupInput?: PersistableControlGroupInput;
} = {
...currentState,
...stateFromSaveModal,
};
let persistableControlGroupInput: PersistableControlGroupInput | undefined;
if (this.controlGroup) {
persistableControlGroupInput = this.controlGroup.getPersistableInput();
dashboardStateToSave = {
...dashboardStateToSave,
controlGroupInput: persistableControlGroupInput,
};
}
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;
}
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 = (
<DashboardSaveModal
tags={currentState.tags}
title={newTitle}
onClose={() => resolve(undefined)}
timeRestore={currentState.timeRestore}
showStoreTimeOnSave={!lastSavedId}
description={currentState.description ?? ''}
showCopyOnSave={false}
onSave={onSaveAttempt}
customModalTitle={customModalTitle}
/>
);
this.clearOverlays();
showSaveModal(dashboardDuplicateModal);
});
}

View file

@ -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;

View file

@ -43,9 +43,13 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = `
labelType="label"
>
<EuiFieldText
autoFocus={true}
data-test-subj="savedObjectTitle"
fullWidth={true}
inputRef={
Object {
"current": null,
}
}
isInvalid={false}
onChange={[Function]}
value="Saved Object title"
@ -167,9 +171,13 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali
labelType="label"
>
<EuiFieldText
autoFocus={true}
data-test-subj="savedObjectTitle"
fullWidth={true}
inputRef={
Object {
"current": null,
}
}
isInvalid={false}
onChange={[Function]}
value="Saved Object title"
@ -291,9 +299,13 @@ exports[`SavedObjectSaveModal should render matching snapshot when custom isVali
labelType="label"
>
<EuiFieldText
autoFocus={true}
data-test-subj="savedObjectTitle"
fullWidth={true}
inputRef={
Object {
"current": null,
}
}
isInvalid={false}
onChange={[Function]}
value="Saved Object title"
@ -419,9 +431,13 @@ exports[`SavedObjectSaveModal should render matching snapshot when given options
labelType="label"
>
<EuiFieldText
autoFocus={true}
data-test-subj="savedObjectTitle"
fullWidth={true}
inputRef={
Object {
"current": null,
}
}
isInvalid={false}
onChange={[Function]}
value="Saved Object title"

View file

@ -78,6 +78,7 @@ const generateId = htmlIdGenerator();
export class SavedObjectSaveModal extends React.Component<Props, SaveModalState> {
private warning = React.createRef<HTMLDivElement>();
private formId = generateId('form');
private savedObjectTitleInputRef = React.createRef<HTMLInputElement>();
public readonly state = {
title: this.props.title,
@ -89,6 +90,13 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
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<Props, SaveModalState>
>
<EuiFieldText
fullWidth
autoFocus
inputRef={this.savedObjectTitleInputRef}
data-test-subj="savedObjectTitle"
value={title}
onChange={this.onTitleChange}
@ -337,7 +345,7 @@ export class SavedObjectSaveModal extends React.Component<Props, SaveModalState>
/>
}
color="warning"
data-test-subj="titleDupicateWarnMsg"
data-test-subj="titleDuplicateWarnMsg"
id={duplicateWarningId}
>
<p>

View file

@ -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<MinimalSaveModalProps>,
Wrapper?: FC<PropsWithChildren<unknown>>
) {
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<ReturnType<typeof toMountPoint>>;
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(
<KibanaRenderContextProvider analytics={getAnalytics()} i18n={getI18n()} theme={getTheme()}>
<I18nContext>{Wrapper ? <Wrapper>{element}</Wrapper> : element}</I18nContext>
</KibanaRenderContextProvider>,
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'));
}

View file

@ -13,7 +13,6 @@
"@kbn/i18n-react",
"@kbn/utility-types",
"@kbn/ui-theme",
"@kbn/react-kibana-context-render",
"@kbn/react-kibana-mount",
],
"exclude": [

View file

@ -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();
});

View file

@ -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,
});
});

View file

@ -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);
});

View file

@ -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,
});

View file

@ -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);

View file

@ -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);
});

View file

@ -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';

View file

@ -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.expectDuplicateTitleWarningDisplayed({ displayed: false });
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, {
waitDialogIsClosed: false,
});
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
});
it('does not save on reject confirmation', async function () {
await PageObjects.dashboard.cancelSave();
await PageObjects.dashboard.gotoDashboardLandingPage();
await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1);
});
it('Saves on confirm duplicate title warning', async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, {
waitDialogIsClosed: 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.ensureDuplicateTitleCallout();
await PageObjects.dashboard.clickSave();
it('does not save on reject confirmation', async function () {
await PageObjects.dashboard.cancelSave();
await PageObjects.dashboard.gotoDashboardLandingPage();
// 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();
// after saving a new dashboard, the app state must be removed
await await PageObjects.dashboard.expectAppStateRemovedFromURL();
await PageObjects.dashboard.gotoDashboardLandingPage();
await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 2);
});
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 });
});
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,
await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 1);
});
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
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 PageObjects.dashboard.cancelSave();
});
await PageObjects.dashboard.ensureDuplicateTitleCallout();
await PageObjects.dashboard.clickSave();
it('Does not warn when only the prefix matches', async function () {
await PageObjects.dashboard.saveDashboard(dashboardName.split(' ')[0]);
// 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.expectDuplicateTitleWarningDisplayed({ displayed: false });
});
// after saving a new dashboard, the app state must be removed
await await PageObjects.dashboard.expectAppStateRemovedFromURL();
it('Warns when case is different', async function () {
await PageObjects.dashboard.switchToEditMode();
await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName.toUpperCase(), {
waitDialogIsClosed: false,
await PageObjects.dashboard.gotoDashboardLandingPage();
await listingTable.searchAndExpectItemsCount('dashboard', dashboardName, 2);
});
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
it('Saves new Dashboard using the Enter key', async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.enterDashboardTitleAndPressEnter(dashboardNameEnterKey);
await PageObjects.dashboard.cancelSave();
// 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 listingTable.searchAndExpectItemsCount('dashboard', dashboardNameEnterKey, 1);
});
});
it('Saves new Dashboard using the Enter key', async function () {
await PageObjects.dashboard.gotoDashboardLandingPage();
await PageObjects.dashboard.clickNewDashboard();
await PageObjects.dashboard.enterDashboardTitleAndPressEnter(dashboardNameEnterKey);
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();
});
// 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 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 listingTable.searchAndExpectItemsCount('dashboard', dashboardNameEnterKey, 1);
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('Stays in edit mode after performing a quick save', async function () {
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('dashboardQuickSaveMenuItem');
});
});
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('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();
});
it('Does not warn when only the prefix matches', async function () {
await PageObjects.dashboard.saveDashboard(dashboardName.split(' ')[0]);
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: false });
});
it('Warns when case is different', async function () {
await PageObjects.dashboard.switchToEditMode();
await PageObjects.dashboard.enterDashboardSaveModalApplyUpdatesAndClickSave(
dashboardName.toUpperCase(),
{
waitDialogIsClosed: false,
}
);
await PageObjects.dashboard.expectDuplicateTitleWarningDisplayed({ displayed: true });
await PageObjects.dashboard.cancelSave();
});
});
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');
describe('flyout settings', () => {
const dashboardNameFlyout = 'Dashboard Save Test with Flyout';
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 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);
await testSubjects.existOrFail('saveDashboardSuccess');
});
// 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();
it('Stays in edit mode after performing a quick save', async function () {
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.existOrFail('dashboardQuickSaveMenuItem');
await PageObjects.dashboard.switchToEditMode();
await PageObjects.dashboard.modifyExistingDashboardDetails(dashboardNameFlyout);
});
});
});
}

View file

@ -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 () {

View file

@ -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}`);

View file

@ -63,12 +63,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(isInViewMode).to.be(false);
});
describe('save', function () {
it('auto exits out of edit mode', async function () {
describe('save as new', () => {
it('keeps duplicated dashboard in edit mode', async () => {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
await PageObjects.dashboard.saveDashboard(dashboardName);
await PageObjects.dashboard.duplicateDashboard('edit');
const isViewMode = await PageObjects.dashboard.getIsInViewMode();
expect(isViewMode).to.equal(true);
expect(isViewMode).to.equal(false);
});
});
describe('save', function () {
it('keeps dashboard in edit mode', async function () {
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
await PageObjects.dashboard.saveDashboard(dashboardName, {
storeTimeWithDashboard: true,
saveAsNew: false,
});
const isViewMode = await PageObjects.dashboard.getIsInViewMode();
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'

View file

@ -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();

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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,
});

View file

@ -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();
});

View file

@ -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);

View file

@ -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<SaveDashboardOptions, 'storeTimeWithDashboard' | 'tags' | 'needsConfirm'> = {}
) {
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<SaveDashboardOptions, 'saveAsNew'> = { 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() {

View file

@ -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,
});

View file

@ -1313,16 +1313,16 @@
"dashboard.topNav.saveModal.storeTimeWithDashboardFormRowHelpText": "Le filtre temporel est défini sur loption 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 ?",

View file

@ -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": "設定",

View file

@ -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": "设置",

View file

@ -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 () => {

View file

@ -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,
});

View file

@ -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);

View file

@ -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',

View file

@ -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,
});

View file

@ -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();

View file

@ -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 });
});
});
}

View file

@ -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'],
});

View file

@ -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);