Migrate save top nav in Discover and Visualize to EUI (#23190) (#23380)

* extract reusable save component from DashboardSaveModal

* update discover search to use SavedObjectSaveModal

* create generic show_save_model that works for both discover and dashboard

* fix last bits of discover save

* remove old save functionallity

* migrate visualize save to EUI

* fix functional tests

* disable save button if title is empty

* mark title input as invalid when title is not provided

* fix funtional tests
This commit is contained in:
Nathan Reese 2018-09-20 19:40:06 -06:00 committed by GitHub
parent f750ab71c1
commit fe1b90faa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 445 additions and 418 deletions

View file

@ -18,6 +18,7 @@
*/
import _ from 'lodash';
import React from 'react';
import angular from 'angular';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
@ -40,7 +41,8 @@ import { VisualizeConstants } from '../visualize/visualize_constants';
import { DashboardStateManager } from './dashboard_state_manager';
import { saveDashboard } from './lib';
import { showCloneModal } from './top_nav/show_clone_modal';
import { showSaveModal } from './top_nav/show_save_modal';
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { DashboardSaveModal } from './top_nav/save_modal';
import { showAddPanel } from './top_nav/show_add_panel';
import { showOptionsPopover } from './top_nav/show_options_popover';
import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
@ -271,7 +273,6 @@ app.directive('dashboardApp', function ($injector) {
function save(saveOptions) {
return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions)
.then(function (id) {
$scope.kbnTopNav.close('save');
if (id) {
toastNotifications.addSuccess({
title: `Dashboard '${dash.title}' was saved`,
@ -335,13 +336,17 @@ app.directive('dashboardApp', function ($injector) {
});
};
showSaveModal({
onSave,
title: currentTitle,
description: currentDescription,
timeRestore: currentTimeRestore,
showCopyOnSave: dash.id ? true : false,
});
const dashboardSaveModal = (
<DashboardSaveModal
onSave={onSave}
onClose={() => {}}
title={currentTitle}
description={currentDescription}
timeRestore={currentTimeRestore}
showCopyOnSave={dash.id ? true : false}
/>
);
showSaveModal(dashboardSaveModal);
};
navActions[TopNavIds.CLONE] = () => {
const currentTitle = dashboardStateManager.getTitle();

View file

@ -1,102 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders DashboardSaveModal 1`] = `
<EuiOverlayMask>
<EuiModal
className="dshSaveModal"
data-test-subj="dashboardSaveModal"
maxWidth={true}
onClose={[Function]}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
Save Dashboard
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiForm>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Save as a new dashboard"
>
<EuiSwitch
checked={false}
data-test-subj="saveAsNewCheckbox"
onChange={[Function]}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Title"
>
<EuiFieldText
autoFocus={true}
compressed={false}
data-test-subj="dashboardTitle"
fullWidth={false}
isInvalid={false}
isLoading={false}
onChange={[Function]}
value="dash title"
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Description"
>
<EuiTextArea
compressed={true}
data-test-subj="dashboardDescription"
fullWidth={false}
onChange={[Function]}
resize="vertical"
value="dash description"
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText="This changes the time filter to the currently selected time each time this dashboard is loaded."
label="Store time with dashboard"
>
<EuiSwitch
checked={true}
data-test-subj="storeTimeWithDashboard"
onChange={[Function]}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
color="primary"
data-test-subj="saveCancelButton"
fill={false}
iconSide="left"
onClick={[Function]}
type="button"
<SavedObjectSaveModal
objectType="dashboard"
onClose={[Function]}
onSave={[Function]}
options={
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Description"
>
Cancel
</EuiButton>
<EuiButton
color="primary"
data-test-subj="confirmSaveDashboardButton"
fill={true}
iconSide="left"
isLoading={false}
onClick={[Function]}
type="button"
<EuiTextArea
compressed={true}
data-test-subj="dashboardDescription"
fullWidth={false}
onChange={[Function]}
resize="vertical"
value="dash description"
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText="This changes the time filter to the currently selected time each time this dashboard is loaded."
label="Store time with dashboard"
>
Confirm Save
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
<EuiSwitch
checked={true}
data-test-subj="storeTimeWithDashboard"
onChange={[Function]}
/>
</EuiFormRow>
</React.Fragment>
}
showCopyOnSave={true}
title="dash title"
/>
`;

View file

@ -20,18 +20,8 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
import {
EuiButton,
EuiFieldText,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSpacer,
EuiCallOut,
EuiForm,
EuiFormRow,
EuiTextArea,
EuiSwitch,
@ -42,56 +32,19 @@ export class DashboardSaveModal extends React.Component {
super(props);
this.state = {
title: props.title,
description: props.description,
copyOnSave: false,
timeRestore: props.timeRestore,
isTitleDuplicateConfirmed: false,
hasTitleDuplicate: false,
isLoading: false,
};
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
onTitleDuplicate = () => {
this.setState({
isLoading: false,
isTitleDuplicateConfirmed: true,
hasTitleDuplicate: true,
});
}
saveDashboard = async () => {
if (this.state.isLoading) {
// ignore extra clicks
return;
}
this.setState({
isLoading: true,
});
await this.props.onSave({
newTitle: this.state.title,
saveDashboard = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => {
this.props.onSave({
newTitle,
newDescription: this.state.description,
newCopyOnSave: this.state.copyOnSave,
newCopyOnSave,
newTimeRestore: this.state.timeRestore,
isTitleDuplicateConfirmed: this.state.isTitleDuplicateConfirmed,
onTitleDuplicate: this.onTitleDuplicate,
});
};
onTitleChange = (event) => {
this.setState({
title: event.target.value,
isTitleDuplicateConfirmed: false,
hasTitleDuplicate: false,
isTitleDuplicateConfirmed,
onTitleDuplicate,
});
};
@ -101,136 +54,50 @@ export class DashboardSaveModal extends React.Component {
});
};
onCopyOnSaveChange = (event) => {
this.setState({
copyOnSave: event.target.checked,
});
}
onTimeRestoreChange = (event) => {
this.setState({
timeRestore: event.target.checked,
});
}
renderDuplicateTitleCallout = () => {
if (!this.state.hasTitleDuplicate) {
return;
}
renderDashboardSaveOptions() {
return (
<Fragment>
<EuiCallOut
title={`A Dashboard with the title '${this.state.title}' already exists.`}
color="warning"
data-test-subj="titleDupicateWarnMsg"
<EuiFormRow
label="Description"
>
<p>
Click <strong>Confirm Save</strong> to save the dashboard with the duplicate title.
</p>
</EuiCallOut>
<EuiSpacer />
<EuiTextArea
data-test-subj="dashboardDescription"
value={this.state.description}
onChange={this.onDescriptionChange}
compressed
/>
</EuiFormRow>
<EuiFormRow
label="Store time with dashboard"
helpText="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}
/>
</EuiFormRow>
</Fragment>
);
}
renderCopyOnSave = () => {
if (!this.props.showCopyOnSave) {
return;
}
return (
<EuiFormRow
label="Save as a new dashboard"
>
<EuiSwitch
data-test-subj="saveAsNewCheckbox"
checked={this.state.copyOnSave}
onChange={this.onCopyOnSaveChange}
/>
</EuiFormRow>
);
}
render() {
return (
<EuiOverlayMask>
<EuiModal
data-test-subj="dashboardSaveModal"
className="dshSaveModal"
onClose={this.props.onClose}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
Save Dashboard
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
{this.renderDuplicateTitleCallout()}
<EuiForm>
{this.renderCopyOnSave()}
<EuiFormRow
label="Title"
>
<EuiFieldText
autoFocus
data-test-subj="dashboardTitle"
value={this.state.title}
onChange={this.onTitleChange}
isInvalid={this.state.hasTitleDuplicate}
/>
</EuiFormRow>
<EuiFormRow
label="Description"
>
<EuiTextArea
data-test-subj="dashboardDescription"
value={this.state.description}
onChange={this.onDescriptionChange}
compressed
/>
</EuiFormRow>
<EuiFormRow
label="Store time with dashboard"
helpText="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}
/>
</EuiFormRow>
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
data-test-subj="saveCancelButton"
onClick={this.props.onClose}
>
Cancel
</EuiButton>
<EuiButton
fill
data-test-subj="confirmSaveDashboardButton"
onClick={this.saveDashboard}
isLoading={this.state.isLoading}
>
Confirm Save
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
<SavedObjectSaveModal
onSave={this.saveDashboard}
onClose={this.props.onClose}
title={this.props.title}
showCopyOnSave={this.props.showCopyOnSave}
objectType="dashboard"
options={this.renderDashboardSaveOptions()}
/>
);
}
}

View file

@ -18,6 +18,7 @@
*/
import _ from 'lodash';
import React from 'react';
import angular from 'angular';
import { getSort } from 'ui/doc_table/lib/get_sort';
import * as columnActions from 'ui/doc_table/actions/columns';
@ -60,6 +61,8 @@ import { RequestAdapter } from 'ui/inspector/adapters';
import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils';
import { showOpenSearchPanel } from '../top_nav/show_open_search_panel';
import { tabifyAggResponse } from 'ui/agg_response/tabify';
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
const app = uiModules.get('apps/discover', [
'kibana/notify',
@ -194,8 +197,36 @@ function discoverController(
}, {
key: 'save',
description: 'Save Search',
template: require('plugins/kibana/discover/partials/save_search.html'),
testId: 'discoverSaveButton',
run: async () => {
const onSave = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => {
const currentTitle = savedSearch.title;
savedSearch.title = newTitle;
savedSearch.copyOnSave = newCopyOnSave;
const saveOptions = {
confirmOverwrite: false,
isTitleDuplicateConfirmed,
onTitleDuplicate,
};
return saveDataSource(saveOptions).then(({ id, error }) => {
// If the save wasn't successful, put the original values back.
if (!id || error) {
savedSearch.title = currentTitle;
}
return { id, error };
});
};
const saveModal = (
<SavedObjectSaveModal
onSave={onSave}
onClose={() => {}}
title={savedSearch.title}
showCopyOnSave={savedSearch.id ? true : false}
objectType="search"
/>);
showSaveModal(saveModal);
}
}, {
key: 'open',
description: 'Open Saved Search',
@ -479,35 +510,40 @@ function discoverController(
});
});
$scope.opts.saveDataSource = function () {
return $scope.updateDataSource()
.then(function () {
savedSearch.columns = $scope.state.columns;
savedSearch.sort = $scope.state.sort;
async function saveDataSource(saveOptions) {
await $scope.updateDataSource();
return savedSearch.save()
.then(function (id) {
stateMonitor.setInitialState($state.toJSON());
$scope.kbnTopNav.close('save');
savedSearch.columns = $scope.state.columns;
savedSearch.sort = $scope.state.sort;
if (id) {
toastNotifications.addSuccess({
title: `Search '${savedSearch.title}' was saved`,
'data-test-subj': 'saveSearchSuccess',
});
if (savedSearch.id !== $route.current.params.id) {
kbnUrl.change('/discover/{{id}}', { id: savedSearch.id });
} else {
// Update defaults so that "reload saved query" functions correctly
$state.setDefaults(getStateDefaults());
docTitle.change(savedSearch.lastSavedTitle);
}
}
try {
const id = await savedSearch.save(saveOptions);
$scope.$evalAsync(() => {
stateMonitor.setInitialState($state.toJSON());
if (id) {
toastNotifications.addSuccess({
title: `Search '${savedSearch.title}' was saved`,
'data-test-subj': 'saveSearchSuccess',
});
})
.catch(notify.error);
};
if (savedSearch.id !== $route.current.params.id) {
kbnUrl.change('/discover/{{id}}', { id: savedSearch.id });
} else {
// Update defaults so that "reload saved query" functions correctly
$state.setDefaults(getStateDefaults());
docTitle.change(savedSearch.lastSavedTitle);
}
}
});
return { id };
} catch(saveError) {
toastNotifications.addDanger({
title: `Search '${savedSearch.title}' was not saved.`,
text: saveError.message
});
return { error: saveError };
}
}
$scope.opts.fetch = $scope.fetch = function () {
// ignore requests to fetch before the app inits

View file

@ -1,31 +0,0 @@
<form
role="form"
ng-submit="opts.saveDataSource()"
>
<h2 class="kuiLocalDropdownTitle">
Save Search
</h2>
<input
class="kuiLocalDropdownInput"
id="SaveSearch"
ng-model="opts.savedSearch.title"
input-focus="select"
placeholder="Name this search..."
aria-label="Name"
>
<saved-object-save-as-check-box
class="kuiVerticalRhythmSmall"
saved-object="opts.savedSearch"
></saved-object-save-as-check-box>
<button
ng-disabled="!opts.savedSearch.title"
data-test-subj="discoverSaveSearchButton"
type="submit"
class="kuiButton kuiButton--primary kuiVerticalRhythmSmall"
>
Save
</button>
</form>

View file

@ -25,6 +25,7 @@ import 'ui/visualize';
import 'ui/collapsible_sidebar';
import 'ui/query_bar';
import chrome from 'ui/chrome';
import React from 'react';
import angular from 'angular';
import { toastNotifications } from 'ui/notify';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
@ -44,6 +45,8 @@ import { timefilter } from 'ui/timefilter';
import { getVisualizeLoader } from '../../../../../ui/public/visualize/loader';
import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share';
import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing';
import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
uiRoutes
.when(VisualizeConstants.CREATE_PATH, {
@ -146,7 +149,6 @@ function VisEditor(
$scope.topNavMenu = [{
key: 'save',
description: 'Save Visualization',
template: require('plugins/kibana/visualize/editor/panels/save.html'),
testId: 'visualizeSaveButton',
disableButton() {
return Boolean(vis.dirty);
@ -155,6 +157,35 @@ function VisEditor(
if (vis.dirty) {
return 'Apply or Discard your changes before saving';
}
},
run: async () => {
const onSave = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => {
const currentTitle = savedVis.title;
savedVis.title = newTitle;
savedVis.copyOnSave = newCopyOnSave;
const saveOptions = {
confirmOverwrite: false,
isTitleDuplicateConfirmed,
onTitleDuplicate,
};
return doSave(saveOptions).then(({ id, error }) => {
// If the save wasn't successful, put the original values back.
if (!id || error) {
savedVis.title = currentTitle;
}
return { id, error };
});
};
const saveModal = (
<SavedObjectSaveModal
onSave={onSave}
onClose={() => {}}
title={savedVis.title}
showCopyOnSave={savedVis.id ? true : false}
objectType="visualization"
/>);
showSaveModal(saveModal);
}
}, {
key: 'share',
@ -257,7 +288,7 @@ function VisEditor(
$scope.isAddToDashMode = () => addToDashMode;
$scope.timeRange = timefilter.getTime();
$scope.opts = _.pick($scope, 'doSave', 'savedVis', 'isAddToDashMode');
$scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode');
stateMonitor = stateMonitorFactory.create($state, stateDefaults);
stateMonitor.ignoreProps([ 'vis.listeners' ]).onChange((status) => {
@ -342,55 +373,58 @@ function VisEditor(
/**
* Called when the user clicks "Save" button.
*/
$scope.doSave = function () {
function doSave(saveOptions) {
// vis.title was not bound and it's needed to reflect title into visState
$state.vis.title = savedVis.title;
$state.vis.type = savedVis.type || $state.vis.type;
savedVis.visState = $state.vis;
savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges());
savedVis.save()
return savedVis.save(saveOptions)
.then(function (id) {
stateMonitor.setInitialState($state.toJSON());
$scope.kbnTopNav.close('save');
$scope.$evalAsync(() => {
stateMonitor.setInitialState($state.toJSON());
if (id) {
toastNotifications.addSuccess({
title: `Saved '${savedVis.title}'`,
'data-test-subj': 'saveVisualizationSuccess',
});
if ($scope.isAddToDashMode()) {
const savedVisualizationParsedUrl = new KibanaParsedUrl({
basePath: chrome.getBasePath(),
appId: kbnBaseUrl.slice('/app/'.length),
appPath: kbnUrl.eval(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }),
if (id) {
toastNotifications.addSuccess({
title: `Saved '${savedVis.title}'`,
'data-test-subj': 'saveVisualizationSuccess',
});
// Manually insert a new url so the back button will open the saved visualization.
$window.history.pushState({}, '', savedVisualizationParsedUrl.getRootRelativePath());
// Since we aren't reloading the page, only inserting a new browser history item, we need to manually update
// the last url for this app, so directly clicking on the Visualize tab will also bring the user to the saved
// url, not the unsaved one.
chrome.trackSubUrlForApp('kibana:visualize', savedVisualizationParsedUrl);
const lastDashboardAbsoluteUrl = chrome.getNavLinkById('kibana:dashboard').lastSubUrl;
const dashboardParsedUrl = absoluteToParsedUrl(lastDashboardAbsoluteUrl, chrome.getBasePath());
dashboardParsedUrl.addQueryParameter(DashboardConstants.NEW_VISUALIZATION_ID_PARAM, savedVis.id);
kbnUrl.change(dashboardParsedUrl.appPath);
} else if (savedVis.id === $route.current.params.id) {
docTitle.change(savedVis.lastSavedTitle);
} else {
kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id });
if ($scope.isAddToDashMode()) {
const savedVisualizationParsedUrl = new KibanaParsedUrl({
basePath: chrome.getBasePath(),
appId: kbnBaseUrl.slice('/app/'.length),
appPath: kbnUrl.eval(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id }),
});
// Manually insert a new url so the back button will open the saved visualization.
$window.history.pushState({}, '', savedVisualizationParsedUrl.getRootRelativePath());
// Since we aren't reloading the page, only inserting a new browser history item, we need to manually update
// the last url for this app, so directly clicking on the Visualize tab will also bring the user to the saved
// url, not the unsaved one.
chrome.trackSubUrlForApp('kibana:visualize', savedVisualizationParsedUrl);
const lastDashboardAbsoluteUrl = chrome.getNavLinkById('kibana:dashboard').lastSubUrl;
const dashboardParsedUrl = absoluteToParsedUrl(lastDashboardAbsoluteUrl, chrome.getBasePath());
dashboardParsedUrl.addQueryParameter(DashboardConstants.NEW_VISUALIZATION_ID_PARAM, savedVis.id);
kbnUrl.change(dashboardParsedUrl.appPath);
} else if (savedVis.id === $route.current.params.id) {
docTitle.change(savedVis.lastSavedTitle);
} else {
kbnUrl.change(`${VisualizeConstants.EDIT_PATH}/{{id}}`, { id: savedVis.id });
}
}
}
});
return { id };
}, (err) => {
toastNotifications.addDanger({
title: `Error on saving '${savedVis.title}'`,
text: err.message,
'data-test-subj': 'saveVisualizationError',
});
return { error: err };
});
};
}
$scope.unlink = function () {
if (!$state.linked) return;

View file

@ -1,32 +0,0 @@
<form
role="form"
ng-submit="opts.doSave()"
>
<h2 class="kuiLocalDropdownTitle">
Save Visualization
</h2>
<input
class="kuiLocalDropdownInput"
input-focus="select"
type="text"
data-test-subj="visTitleInput"
name="visTitle"
ng-model="opts.savedVis.title"
aria-label="Name"
required
>
<saved-object-save-as-check-box
class="kuiVerticalRhythmSmall"
saved-object="opts.savedVis"
></saved-object-save-as-check-box>
<button
data-test-subj="saveVisualizationButton"
type="submit"
class="kuiButton kuiButton--primary kuiVerticalRhythmSmall"
>
{{ opts.isAddToDashMode() ? 'Save and Add to Dashboard' : 'Save'}}
</button>
</form>

View file

@ -0,0 +1,209 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
EuiButton,
EuiFieldText,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiSpacer,
EuiCallOut,
EuiForm,
EuiFormRow,
EuiSwitch,
} from '@elastic/eui';
export class SavedObjectSaveModal extends React.Component {
constructor(props) {
super(props);
this.state = {
title: props.title,
copyOnSave: false,
isTitleDuplicateConfirmed: false,
hasTitleDuplicate: false,
isLoading: false,
};
}
componentDidMount() {
this._isMounted = true;
}
componentWillUnmount() {
this._isMounted = false;
}
onTitleDuplicate = () => {
this.setState({
isLoading: false,
isTitleDuplicateConfirmed: true,
hasTitleDuplicate: true,
});
}
saveSavedObject = async () => {
if (this.state.isLoading) {
// ignore extra clicks
return;
}
this.setState({
isLoading: true,
});
await this.props.onSave({
newTitle: this.state.title,
newCopyOnSave: this.state.copyOnSave,
isTitleDuplicateConfirmed: this.state.isTitleDuplicateConfirmed,
onTitleDuplicate: this.onTitleDuplicate,
});
};
onTitleChange = (event) => {
this.setState({
title: event.target.value,
isTitleDuplicateConfirmed: false,
hasTitleDuplicate: false,
});
};
onCopyOnSaveChange = (event) => {
this.setState({
copyOnSave: event.target.checked,
});
}
renderDuplicateTitleCallout = () => {
if (!this.state.hasTitleDuplicate) {
return;
}
return (
<Fragment>
<EuiCallOut
title={`A ${this.props.objectType} with the title '${this.state.title}' already exists.`}
color="warning"
data-test-subj="titleDupicateWarnMsg"
>
<p>
Click <strong>Confirm Save</strong> to save the {this.props.objectType} with the duplicate title.
</p>
</EuiCallOut>
<EuiSpacer />
</Fragment>
);
}
renderCopyOnSave = () => {
if (!this.props.showCopyOnSave) {
return;
}
return (
<EuiFormRow
label={`Save as a new ${this.props.objectType}`}
>
<EuiSwitch
data-test-subj="saveAsNewCheckbox"
checked={this.state.copyOnSave}
onChange={this.onCopyOnSaveChange}
/>
</EuiFormRow>
);
}
render() {
return (
<EuiOverlayMask>
<EuiModal
data-test-subj="savedObjectSaveModal"
className="dshSaveModal"
onClose={this.props.onClose}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
Save {this.props.objectType}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
{this.renderDuplicateTitleCallout()}
<EuiForm>
{this.renderCopyOnSave()}
<EuiFormRow
label="Title"
>
<EuiFieldText
autoFocus
data-test-subj="savedObjectTitle"
value={this.state.title}
onChange={this.onTitleChange}
isInvalid={this.state.hasTitleDuplicate || this.state.title.length === 0}
/>
</EuiFormRow>
{this.props.options}
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
data-test-subj="saveCancelButton"
onClick={this.props.onClose}
>
Cancel
</EuiButton>
<EuiButton
fill
data-test-subj="confirmSaveSavedObjectButton"
onClick={this.saveSavedObject}
isLoading={this.state.isLoading}
isDisabled={this.state.title.length === 0}
>
Confirm Save
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}
}
SavedObjectSaveModal.propTypes = {
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
showCopyOnSave: PropTypes.bool.isRequired,
objectType: PropTypes.string.isRequired,
options: PropTypes.node,
};

View file

@ -17,17 +17,18 @@
* under the License.
*/
import { DashboardSaveModal } from './save_modal';
import React from 'react';
import ReactDOM from 'react-dom';
export function showSaveModal({ onSave, title, description, timeRestore, showCopyOnSave }) {
export function showSaveModal(saveModal) {
const container = document.createElement('div');
const closeModal = () => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
};
const onSave = saveModal.props.onSave;
const onSaveConfirmed = (...args) => {
onSave(...args).then(({ id, error }) => {
if (id || error) {
@ -36,15 +37,12 @@ export function showSaveModal({ onSave, title, description, timeRestore, showCop
});
};
document.body.appendChild(container);
const element = (
<DashboardSaveModal
onSave={onSaveConfirmed}
onClose={closeModal}
title={title}
description={description}
timeRestore={timeRestore}
showCopyOnSave={showCopyOnSave}
/>
const element = React.cloneElement(
saveModal,
{
onSave: onSaveConfirmed,
onClose: closeModal
}
);
ReactDOM.render(element, container);
}

View file

@ -292,7 +292,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
async renameDashboard(dashName) {
log.debug(`Naming dashboard ` + dashName);
await testSubjects.click('dashboardRenameButton');
await testSubjects.setValue('dashboardTitle', dashName);
await testSubjects.setValue('savedObjectTitle', dashName);
}
/**
@ -320,7 +320,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
async waitForSaveModalToClose() {
log.debug('Waiting for dashboard save modal to close');
await retry.try(async () => {
if (await testSubjects.exists('dashboardSaveModal')) {
if (await testSubjects.exists('savedObjectSaveModal')) {
throw new Error('dashboard save still open');
}
});
@ -342,7 +342,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
async clickSave() {
await retry.try(async () => {
log.debug('clicking final Save button for named dashboard');
return await testSubjects.click('confirmSaveDashboardButton');
return await testSubjects.click('confirmSaveSavedObjectButton');
});
}
@ -357,7 +357,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
log.debug('entering new title');
await testSubjects.setValue('dashboardTitle', dashboardTitle);
await testSubjects.setValue('savedObjectTitle', dashboardTitle);
if (saveOptions.storeTimeWithDashboard !== undefined) {
await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard);

View file

@ -53,8 +53,8 @@ export function DiscoverPageProvider({ getService, getPageObjects }) {
async saveSearch(searchName) {
log.debug('saveSearch');
await this.clickSaveSearchButton();
await getRemote().findDisplayedById('SaveSearch').pressKeys(searchName);
await testSubjects.click('discoverSaveSearchButton');
await testSubjects.setValue('savedObjectTitle', searchName);
await testSubjects.click('confirmSaveSavedObjectButton');
await PageObjects.header.waitUntilLoadingHasFinished();
// LeeDr - this additional checking for the saved search name was an attempt
// to cause this method to wait for the reloading of the page to complete so

View file

@ -701,24 +701,24 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
async ensureSavePanelOpen() {
log.debug('ensureSavePanelOpen');
let isOpen = await testSubjects.exists('saveVisualizationButton');
let isOpen = await testSubjects.exists('savedObjectSaveModal');
await retry.try(async () => {
while (!isOpen) {
await testSubjects.click('visualizeSaveButton');
isOpen = await testSubjects.exists('saveVisualizationButton');
isOpen = await testSubjects.exists('savedObjectSaveModal');
}
});
}
async saveVisualization(vizName, { saveAsNew = false } = {}) {
await this.ensureSavePanelOpen();
await testSubjects.setValue('visTitleInput', vizName);
await testSubjects.setValue('savedObjectTitle', vizName);
if (saveAsNew) {
log.debug('Check save as new visualization');
await testSubjects.click('saveAsNewCheckbox');
}
log.debug('Click Save Visualization button');
await testSubjects.click('saveVisualizationButton');
await testSubjects.click('confirmSaveSavedObjectButton');
}
async saveVisualizationExpectSuccess(vizName, { saveAsNew = false } = {}) {