/* * 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 =
{ error_message }
; } return (
{ error_element } { wait_element } { action_buttons }
); } }); /* * 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, };