mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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:
parent
f750ab71c1
commit
fe1b90faa5
12 changed files with 445 additions and 418 deletions
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 } = {}) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue