mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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 Dashboard5072a501
-8d16-4f25-9575-6f11fed6e580 #### View mode610d0952
-97f0-46b8-a0ea-1546a799d387 #### Edit mode4f596c07
-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—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—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:
parent
d5842d766a
commit
690690ea21
46 changed files with 756 additions and 590 deletions
|
@ -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',
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,5 +15,5 @@ export const extractTitleAndCount = (title: string): [string, number] => {
|
|||
return [baseTitle, Number(count)];
|
||||
}
|
||||
}
|
||||
return [title, 1];
|
||||
return [title, 0];
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"@kbn/i18n-react",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/react-kibana-mount",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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 ?",
|
||||
|
|
|
@ -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": "設定",
|
||||
|
|
|
@ -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": "设置",
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue