/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
var cockpit = require("cockpit");
var React = require("react");
var _ = cockpit.gettext;
require("page.css");
/*
* React template for a Cockpit dialog footer
* It can display an error, wait for an action to complete,
* has a 'Cancel' button and an action button (defaults to 'OK')
* Expected props:
* - cancel_clicked optional
* Callback called when the dialog is canceled
* - cancel_caption optional, defaults to 'Cancel'
* - list of actions, each an object with:
* - clicked
* Callback function that is expected to return a promise.
* parameter: callback to set the progress text (will be displayed next to spinner)
* - caption optional, defaults to 'Ok'
* - disabled optional, defaults to false
* - style defaults to 'default', other options: 'primary', 'danger'
* - static_error optional, always show this error
* - dialog_done optional, callback when dialog is finished (param true if success, false on cancel)
*/
var DialogFooter = React.createClass({
propTypes: {
cancel_clicked: React.PropTypes.func,
cancel_caption: React.PropTypes.string,
actions: React.PropTypes.array,
static_error: React.PropTypes.string,
dialog_done: React.PropTypes.func,
},
getInitialState: function() {
return {
action_in_progress: false,
action_in_progress_promise: null,
action_progress_message: '',
action_canceled: false,
error_message: null,
};
},
keyUpHandler: function(e) {
if (e.keyCode == 27) {
this.cancel_click();
e.stopPropagation();
}
},
componentDidMount: function() {
document.body.classList.add("modal-in");
document.addEventListener('keyup', this.keyUpHandler.bind(this));
},
componentWillUnmount: function() {
document.body.classList.remove("modal-in");
document.removeEventListener('keyup', this.keyUpHandler.bind(this));
},
update_progress: function(msg) {
this.setState({ action_progress_message: msg });
},
action_click: function(handler, e) {
// only consider clicks with the primary button
if (e && e.button !== 0)
return;
var self = this;
this.setState({
error_message: null,
action_progress_message: '',
action_in_progress: true,
action_canceled: false,
});
this.state.action_in_progress_promise = handler(this.update_progress.bind(this))
.done(function() {
self.setState({ action_in_progress: false, error_message: null });
if (self.props.dialog_done)
self.props.dialog_done(true);
})
.fail(function(error) {
if (self.state.action_canceled) {
if (self.props.dialog_done)
self.props.dialog_done(false);
}
/* Always log global dialog errors for easier debugging */
console.warn(error);
self.setState({ action_in_progress: false, error_message: error });
})
.progress(this.update_progress.bind(this));
if (e)
e.stopPropagation();
},
cancel_click: function(e) {
// only consider clicks with the primary button
if (e && e.button !== 0)
return;
this.setState({ action_canceled: true });
if (this.props.cancel_clicked)
this.props.cancel_clicked();
// an action might be in progress, let that handler decide what to do if they added a cancel function
if (this.state.action_in_progress && 'cancel' in this.state.action_in_progress_promise) {
this.state.action_in_progress_promise.cancel();
return;
}
if (this.props.dialog_done)
this.props.dialog_done(false);
if (e)
e.stopPropagation();
},
render: function() {
var cancel_caption;
if ('cancel_caption' in this.props)
cancel_caption = this.props.cancel_caption;
else
cancel_caption = _("Cancel");
// If an action is in progress, show the spinner with its message and disable all actions except cancel
var wait_element;
var actions_disabled;
if (this.state.action_in_progress) {
actions_disabled = 'disabled';
wait_element =
{ this.state.action_progress_message }
;
}
var self = this;
var action_buttons = this.props.actions.map(function(action) {
var caption;
if ('caption' in action)
caption = action.caption;
else
caption = _("Ok");
var button_style = "btn-default";
var button_style_mapping = { 'primary': 'btn-primary', 'danger': 'btn-danger' };
if ('style' in action && action.style in button_style_mapping)
button_style = button_style_mapping[action.style];
button_style = "btn " + button_style + " apply";
var action_disabled = actions_disabled || ('disabled' in action && action.disabled);
return (
);
});
// If we have an error message, display the error
var error_element;
var error_message;
if (this.props.static_error !== undefined && this.props.static_error !== null)
error_message = this.props.static_error;
else
error_message = this.state.error_message;
if (error_message) {
error_element =
);
}
});
/*
* React template for a Cockpit dialog
* The primary action button is disabled while its action is in progress (waiting for promise)
* Removes focus on other elements on showing
* Expected props:
* - title (string)
* - no_backdrop optional, skip backdrop if true
* - body (react element, top element should be of class modal-body)
* It is recommended for information gathering dialogs to pass references
* to the input components to the controller. That way, the controller can
* extract all necessary information (e.g. for input validation) when an
* action is triggered.
* - footer (react element, top element should be of class modal-footer)
* - id optional, id that is assigned to the top level dialog node, but not the backdrop
*/
var Dialog = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
no_backdrop: React.PropTypes.bool,
body: React.PropTypes.element.isRequired,
footer: React.PropTypes.element.isRequired,
id: React.PropTypes.string,
},
componentDidMount: function() {
// if we used a button to open this, make sure it's not focused anymore
if (document.activeElement)
document.activeElement.blur();
},
render: function() {
var backdrop;
if (!this.props.no_backdrop) {
backdrop = ;
}
return (
{ backdrop }
{ this.props.title }
{ this.props.body }
{ this.props.footer }
);
}
});
/* Create and show a dialog
* For this, create a containing DOM node at the body level
* The returned object has the following methods:
* - setFooterProps replace the current footerProps and render
* - setProps replace the current props and render
* - render render again using the stored props
* The DOM node and React metadata are freed once the dialog has closed
*/
var show_modal_dialog = function(props, footerProps) {
var dialogName = 'cockpit_modal_dialog';
// don't allow nested dialogs
if (document.getElementById(dialogName)) {
console.warn('Unable to create nested dialog');
return;
}
// create an element to render into
var rootElement = document.createElement("div");
rootElement.id = dialogName;
document.body.appendChild(rootElement);
// register our own on-close callback
var origCallback;
var closeCallback = function() {
if (origCallback)
origCallback.apply(this, arguments);
React.unmountComponentAtNode(rootElement);
rootElement.remove();
};
var dialogObj = { };
dialogObj.props = null;
dialogObj.footerProps = null;
dialogObj.render = function() {
dialogObj.props.footer = ;
React.render(, rootElement);
};
function updateFooterAndRender() {
if (dialogObj.props === null || dialogObj.props === undefined)
dialogObj.props = { };
dialogObj.props.footer = ;
dialogObj.render();
}
dialogObj.setFooterProps = function(footerProps) {
/* Always log error messages to console for easier debugging */
if (footerProps.static_error)
console.warn(footerProps.static_error);
dialogObj.footerProps = footerProps;
if (dialogObj.footerProps === null || dialogObj.footerProps === undefined)
dialogObj.footerProps = { };
if (dialogObj.footerProps.dialog_done != closeCallback) {
origCallback = dialogObj.footerProps.dialog_done;
dialogObj.footerProps.dialog_done = closeCallback;
}
updateFooterAndRender();
};
dialogObj.setProps = function(props) {
dialogObj.props = props;
updateFooterAndRender();
};
dialogObj.setFooterProps(footerProps);
dialogObj.setProps(props);
// now actually render
dialogObj.render();
return dialogObj;
};
module.exports = {
Dialog: Dialog,
DialogFooter: DialogFooter,
show_modal_dialog: show_modal_dialog,
};