From d294a655a81f1edd37ad19432d5b400dc7fa5cce Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Sun, 30 Sep 2012 14:21:59 +0200 Subject: [PATCH] added ui form to install plugin packages --- .../rest/RestActionResultMessageWriter.java | 126 ++++++++++++ .../api/rest/resources/PluginResource.java | 41 +++- scm-webapp/src/main/webapp/index.mustache | 2 + .../src/main/webapp/resources/css/style.css | 32 +++ .../resources/extjs/util/FileUploadField.js | 184 ++++++++++++++++++ .../resources/js/plugin/sonia.plugin.grid.js | 44 ++++- .../js/plugin/sonia.plugin.uploadform.js | 107 ++++++++++ 7 files changed, 531 insertions(+), 5 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/rest/RestActionResultMessageWriter.java create mode 100644 scm-webapp/src/main/webapp/resources/extjs/util/FileUploadField.js create mode 100644 scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.uploadform.js diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/RestActionResultMessageWriter.java b/scm-webapp/src/main/java/sonia/scm/api/rest/RestActionResultMessageWriter.java new file mode 100644 index 0000000000..3dcaad5f01 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/RestActionResultMessageWriter.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. 2. Redistributions in + * binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. 3. Neither the name of SCM-Manager; + * nor the names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.api.rest; + +//~--- non-JDK imports -------------------------------------------------------- + +import com.google.common.base.Charsets; + +//~--- JDK imports ------------------------------------------------------------ + +import java.io.IOException; +import java.io.OutputStream; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +/** + * + * @author Sebastian Sdorra + */ +@Provider +public class RestActionResultMessageWriter + implements MessageBodyWriter +{ + + /** + * Method description + * + * + * @param result + * @param type + * @param genericType + * @param annotations + * @param mediaType + * @param httpHeaders + * @param entityStream + * + * @throws IOException + * @throws WebApplicationException + */ + @Override + public void writeTo(RestActionResult result, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException + { + String v = + "{\"success\": ".concat(String.valueOf(result.isSuccess())).concat("}"); + + entityStream.write(v.getBytes(Charsets.UTF_8)); + } + + //~--- get methods ---------------------------------------------------------- + + /** + * Method description + * + * + * @param result + * @param type + * @param genericType + * @param annotations + * @param mediaType + * + * @return + */ + @Override + public long getSize(RestActionResult result, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType) + { + return -1; + } + + /** + * Method description + * + * + * @param type + * @param genericType + * @param annotations + * @param mediaType + * + * @return + */ + @Override + public boolean isWriteable(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType) + { + return RestActionResult.class.isAssignableFrom(type); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java index 2ad44c817f..f1a51940d6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/PluginResource.java @@ -44,6 +44,7 @@ import org.codehaus.enunciate.modules.jersey.ExternallyManagedLifecycle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.api.rest.RestActionResult; import sonia.scm.plugin.DefaultPluginManager; import sonia.scm.plugin.OverviewPluginFilter; import sonia.scm.plugin.PluginConditionFailedException; @@ -113,8 +114,6 @@ public class PluginResource *
  • 500 internal server error
  • * * - * - * * @param uploadedInputStream * @return * @@ -123,6 +122,7 @@ public class PluginResource @POST @Path("install-package") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON }) public Response install( @FormDataParam("package") InputStream uploadedInputStream) throws IOException @@ -132,13 +132,20 @@ public class PluginResource try { pluginManager.installPackage(uploadedInputStream); - response = Response.ok().build(); + response = Response.ok(new RestActionResult(true)).build(); } catch (PluginConditionFailedException ex) { logger.warn( "could not install plugin package, because the condition failed", ex); - response = Response.status(Status.CONFLICT).build(); + response = Response.status(Status.CONFLICT).entity( + new RestActionResult(false)).build(); + } + catch (Exception ex) + { + logger.warn("plugin installation failed", ex); + response = + Response.serverError().entity(new RestActionResult(false)).build(); } return response; @@ -165,6 +172,32 @@ public class PluginResource return Response.ok().build(); } + /** + * Installs a plugin from a package. This method is a workaround for ExtJS + * file upload, which requires text/html as content-type.
    + *
    + *
      + *
    • 200 success
    • + *
    • 412 conflict
    • + *
    • 500 internal server error
    • + *
    + * + * @param uploadedInputStream + * @return + * + * @throws IOException + */ + @POST + @Path("install-package.html") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.TEXT_HTML) + public Response installFromUI( + @FormDataParam("package") InputStream uploadedInputStream) + throws IOException + { + return install(uploadedInputStream); + } + /** * Uninstalls a plugin.
    *
    diff --git a/scm-webapp/src/main/webapp/index.mustache b/scm-webapp/src/main/webapp/index.mustache index b069ef9b62..ade05f14ba 100644 --- a/scm-webapp/src/main/webapp/index.mustache +++ b/scm-webapp/src/main/webapp/index.mustache @@ -62,6 +62,7 @@ + @@ -149,6 +150,7 @@ + diff --git a/scm-webapp/src/main/webapp/resources/css/style.css b/scm-webapp/src/main/webapp/resources/css/style.css index 0adbcadd9f..e9176a7942 100644 --- a/scm-webapp/src/main/webapp/resources/css/style.css +++ b/scm-webapp/src/main/webapp/resources/css/style.css @@ -202,3 +202,35 @@ div.noscript-container h1 { color: #D20005; border-bottom: 1px solid #AFAFAF; } + +/* + * FileUploadField component styles + */ +.x-form-file-wrap { + position: relative; + height: 22px; +} +.x-form-file-wrap .x-form-file { + position: absolute; + right: 0; + -moz-opacity: 0; + filter:alpha(opacity: 0); + opacity: 0; + z-index: 2; + height: 22px; +} +.x-form-file-wrap .x-form-file-btn { + position: absolute; + right: 0; + z-index: 1; +} +.x-form-file-wrap .x-form-file-text { + position: absolute; + left: 0; + z-index: 3; + color: #777; +} + +.upload-icon { + background: url('../images/add.png') no-repeat 0 0 !important; +} \ No newline at end of file diff --git a/scm-webapp/src/main/webapp/resources/extjs/util/FileUploadField.js b/scm-webapp/src/main/webapp/resources/extjs/util/FileUploadField.js new file mode 100644 index 0000000000..6291d361d5 --- /dev/null +++ b/scm-webapp/src/main/webapp/resources/extjs/util/FileUploadField.js @@ -0,0 +1,184 @@ +/*! + * Ext JS Library 3.4.0 + * Copyright(c) 2006-2011 Sencha Inc. + * licensing@sencha.com + * http://www.sencha.com/license + */ +Ext.ns('Ext.ux.form'); + +/** + * @class Ext.ux.form.FileUploadField + * @extends Ext.form.TextField + * Creates a file upload field. + * @xtype fileuploadfield + */ +Ext.ux.form.FileUploadField = Ext.extend(Ext.form.TextField, { + /** + * @cfg {String} buttonText The button text to display on the upload button (defaults to + * 'Browse...'). Note that if you supply a value for {@link #buttonCfg}, the buttonCfg.text + * value will be used instead if available. + */ + buttonText: 'Browse...', + /** + * @cfg {Boolean} buttonOnly True to display the file upload field as a button with no visible + * text field (defaults to false). If true, all inherited TextField members will still be available. + */ + buttonOnly: false, + /** + * @cfg {Number} buttonOffset The number of pixels of space reserved between the button and the text field + * (defaults to 3). Note that this only applies if {@link #buttonOnly} = false. + */ + buttonOffset: 3, + /** + * @cfg {Object} buttonCfg A standard {@link Ext.Button} config object. + */ + + // private + readOnly: true, + + /** + * @hide + * @method autoSize + */ + autoSize: Ext.emptyFn, + + // private + initComponent: function(){ + Ext.ux.form.FileUploadField.superclass.initComponent.call(this); + + this.addEvents( + /** + * @event fileselected + * Fires when the underlying file input field's value has changed from the user + * selecting a new file from the system file selection dialog. + * @param {Ext.ux.form.FileUploadField} this + * @param {String} value The file value returned by the underlying file input field + */ + 'fileselected' + ); + }, + + // private + onRender : function(ct, position){ + Ext.ux.form.FileUploadField.superclass.onRender.call(this, ct, position); + + this.wrap = this.el.wrap({cls:'x-form-field-wrap x-form-file-wrap'}); + this.el.addClass('x-form-file-text'); + this.el.dom.removeAttribute('name'); + this.createFileInput(); + + var btnCfg = Ext.applyIf(this.buttonCfg || {}, { + text: this.buttonText + }); + this.button = new Ext.Button(Ext.apply(btnCfg, { + renderTo: this.wrap, + cls: 'x-form-file-btn' + (btnCfg.iconCls ? ' x-btn-icon' : '') + })); + + if(this.buttonOnly){ + this.el.hide(); + this.wrap.setWidth(this.button.getEl().getWidth()); + } + + this.bindListeners(); + this.resizeEl = this.positionEl = this.wrap; + }, + + bindListeners: function(){ + this.fileInput.on({ + scope: this, + mouseenter: function() { + this.button.addClass(['x-btn-over','x-btn-focus']) + }, + mouseleave: function(){ + this.button.removeClass(['x-btn-over','x-btn-focus','x-btn-click']) + }, + mousedown: function(){ + this.button.addClass('x-btn-click') + }, + mouseup: function(){ + this.button.removeClass(['x-btn-over','x-btn-focus','x-btn-click']) + }, + change: function(){ + var v = this.fileInput.dom.value; + this.setValue(v); + this.fireEvent('fileselected', this, v); + } + }); + }, + + createFileInput : function() { + this.fileInput = this.wrap.createChild({ + id: this.getFileInputId(), + name: this.name||this.getId(), + cls: 'x-form-file', + tag: 'input', + type: 'file', + size: 1 + }); + }, + + reset : function(){ + if (this.rendered) { + this.fileInput.remove(); + this.createFileInput(); + this.bindListeners(); + } + Ext.ux.form.FileUploadField.superclass.reset.call(this); + }, + + // private + getFileInputId: function(){ + return this.id + '-file'; + }, + + // private + onResize : function(w, h){ + Ext.ux.form.FileUploadField.superclass.onResize.call(this, w, h); + + this.wrap.setWidth(w); + + if(!this.buttonOnly){ + var w = this.wrap.getWidth() - this.button.getEl().getWidth() - this.buttonOffset; + this.el.setWidth(w); + } + }, + + // private + onDestroy: function(){ + Ext.ux.form.FileUploadField.superclass.onDestroy.call(this); + Ext.destroy(this.fileInput, this.button, this.wrap); + }, + + onDisable: function(){ + Ext.ux.form.FileUploadField.superclass.onDisable.call(this); + this.doDisable(true); + }, + + onEnable: function(){ + Ext.ux.form.FileUploadField.superclass.onEnable.call(this); + this.doDisable(false); + + }, + + // private + doDisable: function(disabled){ + this.fileInput.dom.disabled = disabled; + this.button.setDisabled(disabled); + }, + + + // private + preFocus : Ext.emptyFn, + + // private + alignErrorIcon : function(){ + this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); + } + +}); + +Ext.reg('fileuploadfield', Ext.ux.form.FileUploadField); + +// backwards compat +Ext.form.FileUploadField = Ext.ux.form.FileUploadField; \ No newline at end of file diff --git a/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.grid.js b/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.grid.js index 49bb1fad3e..99c0277af6 100644 --- a/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.grid.js +++ b/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.grid.js @@ -48,8 +48,15 @@ Sonia.plugin.Grid = Ext.extend(Sonia.rest.Grid, { // grid emptyText: 'No plugins avaiable', + // TODO i18n + // buttons btnReload: 'Reload', + btnIconReload: 'resources/images/reload.png', + btnInstallPackage: 'Install Package', + btnIconInstallPackage: 'resources/images/add.png', + + uploadWindowTitle: 'Upload Plugin-Package', actionLinkTemplate: '{0}', @@ -107,8 +114,43 @@ Sonia.plugin.Grid = Ext.extend(Sonia.rest.Grid, { groupTextTpl: '{group} ({[values.rs.length]} {[values.rs.length > 1 ? "Plugins" : "Plugin"]})' }), tbar: [{ + text: this.btnInstallPackage, + icon: this.btnIconInstallPackage, + handler: function(){ + var window = new Ext.Window({ + title: this.uploadWindowTitle + }); + window.add({ + xtype: 'pluginPackageUploadForm', + listeners: { + success: { + fn: function(){ + this.close(); + Ext.MessageBox.alert( + Sonia.plugin.CenterInstance.installSuccessText, + Sonia.plugin.CenterInstance.restartText + ); + }, + scope: window + }, + failure: { + fn: function(){ + this.close(); + Ext.MessageBox.alert( + Sonia.plugin.CenterInstance.errorTitleText, + Sonia.plugin.CenterInstance.installFailedText + ); + }, + scope: window + } + } + }); + window.show(); + }, + scope: this + },'|',{ text: this.btnReload, - icon: 'resources/images/reload.png', + icon: this.btnIconReload, handler: function(){ this.getStore().reload(); }, diff --git a/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.uploadform.js b/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.uploadform.js new file mode 100644 index 0000000000..44ad6de8fa --- /dev/null +++ b/scm-webapp/src/main/webapp/resources/js/plugin/sonia.plugin.uploadform.js @@ -0,0 +1,107 @@ +/* * + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +Sonia.plugin.UploadForm = Ext.extend(Ext.FormPanel, { + + // TODO i18n + + emptyText: 'Select an Plugin-Package', + uploadFieldLabel: 'Package', + waitMsg: 'Uploading your package ...', + btnUpload: 'Upload', + btnReset: 'Reset', + + initComponent: function(){ + this.addEvents('success', 'failure'); + + var config = { + fileUpload: true, + width: 500, + frame: false, + autoHeight: true, + labelWidth: 50, + bodyStyle: 'padding: 5px 0 0 10px;', + defaults: { + anchor: '95%', + allowBlank: false, + msgTarget: 'side' + }, + items: [{ + xtype: 'fileuploadfield', + id: 'form-file', + emptyText: this.emptyText, + fieldLabel: this.uploadFieldLabel, + name: 'package', + buttonText: '', + buttonCfg: { + iconCls: 'upload-icon' + } + }], + buttons: [{ + text: this.btnUpload, + handler: function(){ + if(this.getForm().isValid()){ + this.getForm().submit({ + url: restUrl + 'plugins/install-package.html', + waitMsg: this.waitMsg, + success: function(form, action){ + if (debug){ + console.debug('upload success'); + } + this.fireEvent('success'); + }, + failure: function(form, action){ + if (debug){ + console.debug('upload failed'); + } + this.fireEvent('failure'); + }, + scope: this + }); + } + }, + scope: this + },{ + text: this.btnReset, + handler: function(){ + this.getForm().reset(); + }, + scope: this + }] + }; + + Ext.apply(this, Ext.apply(this.initialConfig, config)); + Sonia.plugin.UploadForm.superclass.initComponent.apply(this, arguments); + } + +}); + +Ext.reg('pluginPackageUploadForm', Sonia.plugin.UploadForm); \ No newline at end of file