Преглед изворни кода

AMBARI-7623. View: Files cleanup, enhancements and bugs. (jaimin)

Jaimin Jetly пре 10 година
родитељ
комит
d5b9af7458
36 измењених фајлова са 1306 додато и 537 уклоњено
  1. 37 0
      contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileOperationService.java
  2. 21 0
      contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/HdfsApi.java
  3. 37 22
      contrib/views/files/src/main/resources/ui/app/adapter.js
  4. 47 0
      contrib/views/files/src/main/resources/ui/app/components/breadCrumbs.js
  5. 21 0
      contrib/views/files/src/main/resources/ui/app/components/bsPopover.js
  6. 40 0
      contrib/views/files/src/main/resources/ui/app/components/bulkCheckbox.js
  7. 79 0
      contrib/views/files/src/main/resources/ui/app/components/chmodInput.js
  8. 59 0
      contrib/views/files/src/main/resources/ui/app/components/confirmDelete.js
  9. 20 16
      contrib/views/files/src/main/resources/ui/app/components/contextMenu.js
  10. 45 0
      contrib/views/files/src/main/resources/ui/app/components/mkdirInput.js
  11. 50 0
      contrib/views/files/src/main/resources/ui/app/components/popoverDelete.js
  12. 3 4
      contrib/views/files/src/main/resources/ui/app/components/renameInput.js
  13. 39 0
      contrib/views/files/src/main/resources/ui/app/components/sortArrow.js
  14. 56 0
      contrib/views/files/src/main/resources/ui/app/components/toggleContext.js
  15. 9 4
      contrib/views/files/src/main/resources/ui/app/components/uploader.js
  16. 2 3
      contrib/views/files/src/main/resources/ui/app/controllers/error.js
  17. 51 53
      contrib/views/files/src/main/resources/ui/app/controllers/file.js
  18. 46 57
      contrib/views/files/src/main/resources/ui/app/controllers/files.js
  19. 10 0
      contrib/views/files/src/main/resources/ui/app/controllers/filesAlert.js
  20. 19 5
      contrib/views/files/src/main/resources/ui/app/initialize.js
  21. 3 0
      contrib/views/files/src/main/resources/ui/app/routes/file.js
  22. 61 6
      contrib/views/files/src/main/resources/ui/app/styles/application.less
  23. 100 0
      contrib/views/files/src/main/resources/ui/app/templates/components/chmodInput.hbs
  24. 6 19
      contrib/views/files/src/main/resources/ui/app/templates/components/contextMenu.hbs
  25. 16 8
      contrib/views/files/src/main/resources/ui/app/templates/components/deleteBulk.hbs
  26. 6 6
      contrib/views/files/src/main/resources/ui/app/templates/components/deletePopover.hbs
  27. 37 0
      contrib/views/files/src/main/resources/ui/app/templates/components/mkdirInput.hbs
  28. 0 0
      contrib/views/files/src/main/resources/ui/app/templates/components/renameInput.hbs
  29. 0 0
      contrib/views/files/src/main/resources/ui/app/templates/components/uploader.hbs
  30. 75 154
      contrib/views/files/src/main/resources/ui/app/templates/files.hbs
  31. 84 0
      contrib/views/files/src/main/resources/ui/app/templates/util/fileRow.hbs
  32. 8 174
      contrib/views/files/src/main/resources/ui/app/views/file.js
  33. 23 0
      contrib/views/files/src/main/resources/ui/app/views/filesAlert.js
  34. 5 5
      contrib/views/files/src/main/resources/ui/bower.json
  35. 190 0
      contrib/views/files/src/main/resources/ui/vendor/js/bsPopover.js
  36. 1 1
      contrib/views/files/src/main/resources/view.xml

+ 37 - 0
contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/FileOperationService.java

@@ -95,6 +95,33 @@ public class FileOperationService extends HdfsService {
     }
   }
 
+  /**
+   * Chmod
+   * @param request chmod request
+   * @return response with success
+   */
+  @POST
+  @Path("/chmod")
+  @Consumes(MediaType.APPLICATION_JSON)
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response chmod(final ChmodRequest request) {
+    try {
+      HdfsApi api = getApi(context);
+      ResponseBuilder result;
+      if (api.chmod(request.path, request.mode)) {
+        result = Response.ok(HdfsApi.fileStatusToJSON(api
+            .getFileStatus(request.path)));
+      } else {
+        result = Response.ok(new BoolResult(false)).status(422);
+      }
+      return result.build();
+    } catch (WebApplicationException ex) {
+      throw ex;
+    } catch (Exception ex) {
+      throw new ServiceFormattedException(ex.getMessage(), ex);
+    }
+  }
+
   /**
    * Copy file
    * @param request source and destination request
@@ -229,6 +256,16 @@ public class FileOperationService extends HdfsService {
     public String path;
   }
 
+  /**
+   * Wrapper for json mapping of chmod request
+   */
+  @XmlRootElement
+  public static class ChmodRequest {
+    @XmlElement(nillable = false, required = true)
+    public String path;
+    @XmlElement(nillable = false, required = true)
+    public String mode;
+  }
 
   /**
    * Wrapper for json mapping of request with

+ 21 - 0
contrib/views/files/src/main/java/org/apache/ambari/view/filebrowser/HdfsApi.java

@@ -249,6 +249,27 @@ public class HdfsApi {
     });
   }
 
+  /**
+   * Change permissions
+   * @param path path
+   * @param permissions permissions in format rwxrwxrwx
+   * @throws IOException
+   * @throws InterruptedException
+   */
+  public boolean chmod(final String path, final String permissions) throws IOException,
+      InterruptedException {
+    return ugi.doAs(new PrivilegedExceptionAction<Boolean>() {
+      public Boolean run() throws Exception {
+        try {
+          fs.setPermission(new Path(path), new FsPermission(permissions));
+        } catch (Exception ex) {
+          return false;
+        }
+        return true;
+      }
+    });
+  }
+
   /**
    * Copy file
    * @param src source path

+ 37 - 22
contrib/views/files/src/main/resources/ui/app/adapter.js

@@ -87,11 +87,6 @@ function _move(adapter, store, record, query) {
 
     return store.push('file', payload);
   }, function(reason) {
-    if (reason instanceof DS.InvalidError) {
-      store.recordWasInvalid(record, reason.errors);
-    } else {
-      store.recordWasError(record, reason);
-    }
 
     throw reason;
   }, label);
@@ -113,19 +108,14 @@ function _mkdir(adapter, store, type, query) {
 
     return store.push('file', payload);
   }, function(reason) {
-    if (reason instanceof DS.InvalidError) {
-      store.recordWasInvalid(record, reason.errors);
-    } else {
-      store.recordWasError(record, reason);
-    }
-
     throw reason;
   }, label);
 }
 
-function _remove(adapter, store, record, query) {
+function _remove(adapter, store, record, query, toTrash) {
   var type = record.constructor;
-  var promise = adapter.remove(store, type, query),
+  var method = (toTrash)?'moveToTrash':'remove';
+  var promise = adapter[method](store, type, query),
       serializer = serializerForAdapter(adapter, type),
       label = "";
 
@@ -136,7 +126,8 @@ function _remove(adapter, store, record, query) {
     if (reason instanceof DS.InvalidError) {
       store.recordWasInvalid(record, reason.errors);
     } else {
-      store.recordWasError(record, reason);
+      record.rollback();
+      //store.recordWasError(record, reason);
     }
 
     throw reason;
@@ -172,12 +163,22 @@ App.Store = DS.Store.extend({
     move:function (store, type, record, query) {
       return this.ajax(this.buildURL('fileops','rename'), 'POST', { data: query });
     },
+    updateRecord:function (store, type, record) {
+      var query = {
+        "path":record.get('path'),
+        "mode":record.get('permission')
+      };
+      return this.ajax(this.buildURL('fileops','chmod'), 'POST', { data: query });
+    },
     mkdir:function (store, type, query) {
       return this.ajax(this.buildURL('fileops','mkdir'), 'PUT', { data: query });
     },
     remove:function (store, type, query) {
       return this.ajax(this.buildURL('fileops','remove'), 'DELETE', { data: query });
     },
+    moveToTrash:function (store, type, query) {
+      return this.ajax(this.buildURL('fileops','moveToTrash'), 'DELETE', { data: query });
+    },
     downloadUrl:function (option, query) {
       return [this.buildURL('download',option),Em.$.param(query)].join('?');
     },
@@ -218,6 +219,9 @@ App.Store = DS.Store.extend({
 
     return DS.PromiseObject.create({ promise: resolver.promise });
   },
+  chmod:function (record, path) {
+    return record.save();
+  },
   mkdir:function (path) {
     var query = {
       "path":path
@@ -231,7 +235,7 @@ App.Store = DS.Store.extend({
 
     return DS.PromiseObject.create({ promise: resolver.promise });
   },
-  remove:function (record) {
+  remove:function (record, toTrash) {
     var query = {
       "path":record.get('path'),
       "recursive":true
@@ -242,7 +246,7 @@ App.Store = DS.Store.extend({
     var adapter = this.adapterFor(type);
     
     record.deleteRecord();
-    resolver.resolve(_remove(adapter, this, record, query));
+    resolver.resolve(_remove(adapter, this, record, query, toTrash));
 
     return DS.PromiseObject.create({ promise: resolver.promise });
   },
@@ -279,7 +283,7 @@ App.Store = DS.Store.extend({
     return resolver.promise.then(function(response) {
       return adapter.downloadUrl(option,response);
     }, function(reason) {
-      //TODO reject
+      throw reason;
     });
   }
 })
@@ -293,7 +297,10 @@ App.FileSerializer = DS.RESTSerializer.extend({
   extractSingle: function(store, type, payload, id, requestType) {
     payload = {'files': payload};
     return this._super(store, type, payload, id, requestType);
-  }
+  },
+  extractChmod:function(store, type, payload, id, requestType) {
+    return this.extractSingle(store, type, payload, id, requestType);
+  },
 });
 
 App.Uploader = Ember.Uploader.create({
@@ -307,11 +314,19 @@ App.Uploader = Ember.Uploader.create({
 
     this.set('isUploading', true);
     
-    return this.ajax(url, data, type).then(function(respData) {
-      self.didUpload(respData);
-      return respData;
-    });
+    return this.ajax(url, data, type)
+      .then(Em.run.bind(this,this.uploadSuccess),Em.run.bind(this,this.uploadFailed));
+  },
+  uploadSuccess:function(respData) {
+    this.didUpload(respData);
+    return respData;
+  },
+  uploadFailed:function (error) {
+    this.set('isUploading', false);
+    this.sendAlert(error);
+    return error;
   },
+  sendAlert: Em.K,
   ajax: function(url, params, method) {
     var self = this;
     var settings = {

+ 47 - 0
contrib/views/files/src/main/resources/ui/app/components/breadCrumbs.js

@@ -0,0 +1,47 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.BreadCrumbsComponent = Ember.CollectionView.extend({
+  classNames: ['breadcrumb pull-left'],
+  tagName: 'ul',
+  path:'',
+  content: function (argument) {
+    var crumbs = [];
+    var currentPath = this.get('path').match(/((?!\/)\S)+/g)||[];
+    currentPath.forEach(function (cur,i,array) {
+      return crumbs.push({name:cur,path:'/'+array.slice(0,i+1).join('/')});
+    });
+    crumbs.unshift({name:'/',path:'/'});
+    crumbs.get('lastObject').last = 'true';
+    return crumbs;
+  }.property('path'),
+  itemViewClass: Ember.View.extend({
+    classNameBindings: ['isActive:active'],
+    template: Ember.Handlebars.compile("{{#link-to 'files' (query-params path=view.content.path)}}{{view.content.name}}{{/link-to}}"),
+    isActive: function () {
+      return this.get('content.last');
+    }.property('content'),
+    click:function () {
+      if (this.get('isActive')) {
+        this.get('controller').send('refreshDir');
+      }
+    }
+  })
+});

+ 21 - 0
contrib/views/files/src/main/resources/ui/app/components/bsPopover.js

@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.BsPopoverComponent = Ember.BsPopoverComponent.extend({});

+ 40 - 0
contrib/views/files/src/main/resources/ui/app/components/bulkCheckbox.js

@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.BulkCheckboxComponent = Em.Checkbox.extend({
+  changeBinding:'selectAll',
+  checkedBinding:'selectedAll',
+  selectedAll:false,
+  selectAll:function () {
+    var checked = this.get('checked');
+    var items = this.get('content');
+    return items.forEach(function (item) {
+      item.set('selected',checked);
+    });
+  },
+  selection:function () {
+    var selected = this.get('content').filterBy('selected',true);
+    if (selected.length == this.get('content.length') && selected.length > 0) {
+      this.set('selectedAll',true);
+    } else {
+      this.set('selectedAll',false);
+    }
+  }.observes('content.@each.selected'),
+});

+ 79 - 0
contrib/views/files/src/main/resources/ui/app/components/chmodInput.js

@@ -0,0 +1,79 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+var _permissionsProp = function(n, l) {
+  return function (arg,val) {
+    if (arguments.length > 1) {
+      this.set('permissions', this.replaceAt(n,(val)?l:'-'));
+      return val;
+    };
+    return this.get('permissions')[n]===l;
+  }
+}
+
+App.ChmodInputComponent = Em.Component.extend({
+  layoutName:'components/chmodInput',
+  tagName:'tr',
+  classNames:"chmod-row",
+  file:null,
+  permissions:Em.computed.alias('file.permission'),
+  usrR:_permissionsProp(1, 'r').property('permissions'),
+  usrW:_permissionsProp(2, 'w').property('permissions'),
+  usrE:_permissionsProp(3, 'x').property('permissions'),
+  grpR:_permissionsProp(4, 'r').property('permissions'),
+  grpW:_permissionsProp(5, 'w').property('permissions'),
+  grpE:_permissionsProp(6, 'x').property('permissions'),
+  otrR:_permissionsProp(7, 'r').property('permissions'),
+  otrW:_permissionsProp(8, 'w').property('permissions'),
+  otrE:_permissionsProp(9, 'x').property('permissions'),
+  replaceAt:function (index,p) {
+    var perm = this.get('permissions');
+    var newPerm = perm.substr(0, index) + p + perm.substr(index + p.length);
+    return newPerm;
+  },
+  markActive:function () {
+    if (this.get('isVisible')) {
+      this.$('.btn-chmod').each(function () {
+        if ($(this).children('input').is(':checked')) {
+          $(this).addClass('active');
+        } else {
+          $(this).removeClass('active');
+        }
+      });
+    }
+  }.observes('chVisible'),
+  showModal:function () {
+    this.$('.chmodal').modal('toggle');
+  }.observes('chVisible'),
+  actions:{
+    confirm:function (r) {
+      this.sendAction('confirm',r);
+      this.set('chVisible',false);
+    },
+    close:function () {
+      var file = this.get('file');
+      var diff = file.changedAttributes();
+      if (diff.permission) {
+        file.set('permission',diff.permission[0]);
+      };
+      this.set('chVisible',false);
+    }
+  }
+});

+ 59 - 0
contrib/views/files/src/main/resources/ui/app/components/confirmDelete.js

@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.DropdownWrapComponent = Em.Component.extend({
+  onResetConfirm:function () {
+    var childs = this.get('childViews').filter(function (view) {
+      return view instanceof App.ConfirmDeleteComponent;
+    });
+    childs.setEach('isRemoving',false);
+  }.on('resetConfirm'),
+  didInsertElement:function(){
+    this.$().on('hidden.bs.dropdown',Em.run.bind(this,this.onResetConfirm));
+  },
+});
+
+App.ConfirmDeleteComponent = Em.Component.extend({
+  layoutName:'components/deleteBulk',
+  tagName:'li',
+  deleteForever:false,
+  isRemoving:false,
+  cancelRemoving:function () {
+    this.set('isRemoving',false);
+  },
+  click:function  (e) {
+    if (!$(e.target).hasClass('delete')) {
+      e.stopPropagation();
+    };
+  },
+  actions:{
+    ask:function () {
+      this.get('parentView').trigger('resetConfirm');
+      this.set('isRemoving',true);
+      return false; 
+    },
+    cancel:function () {
+      this.cancelRemoving();
+    },
+    confirm:function () {
+      this.sendAction('confirm',this.get('deleteForever'));
+    }
+  }
+});

+ 20 - 16
contrib/views/files/src/main/resources/ui/app/components/contextMenu.js

@@ -18,21 +18,25 @@
 
 var App = require('app');
 
-App.ContextMenu = Em.Component.extend({
-  layoutName:'util/contextMenu',
-  waitConfirm: null,
-  startWaitConfirm:function (v,observer) {
-    var self = this,
-        action = this.get(observer);
-    if (!action) {
-      $(this.get('element')).off('hidden.bs.context');
-      return false;
-    };
-    $(this.get('element')).on('hidden.bs.context',function(){
-      self.get('target').send(self.get(observer),'cancel');
-      self.set('waitConfirm', null)
-    })
-    this.get('target').send(action,'ask');
-  }.observes('waitConfirm')
+App.ContextMenuComponent = Em.Component.extend({
+  layoutName:'components/contextMenu',
+
+  onTargetChange:function () {
+    this.$().off('hidden.bs.context');
+    this.$().on('hidden.bs.context', Em.run.bind(this, this.resetConfirmations));
+  }.observes('target'),
+
+  resetConfirmations:function () {
+    this.triggerRecursively('resetConfirm');
+  },
+
+  actions:{
+    removeFile:function () {
+      this.get('target').send('deleteFile',true);
+    },
+    moveToTrash:function () {
+      this.get('target').send('deleteFile');
+    },
+  }
 
 });

+ 45 - 0
contrib/views/files/src/main/resources/ui/app/components/mkdirInput.js

@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.MkdirInputComponent = Em.Component.extend({
+  layoutName:'components/mkdirInput',
+  newDirName:'',
+  isMkdir:false,
+  path:'',
+  actions:{
+    create:function () {
+      var name = this.get('newDirName');
+
+      if (Em.isEmpty(name)) {
+        return false;
+      }
+      newDir = [this.get('path'),name].join('/').replace('//','/');
+
+      this.sendAction('create',newDir);
+      this.setProperties({'newDirName':'','isMkdir':false});
+    },
+    edit:function () {
+      this.set('isMkdir',true);
+    },
+    cancel:function () {
+      this.setProperties({'newDirName':'','isMkdir':false});
+    }
+  }
+});

+ 50 - 0
contrib/views/files/src/main/resources/ui/app/components/popoverDelete.js

@@ -0,0 +1,50 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.PopoverDeleteComponent = Em.Component.extend({
+  popover:Em.computed.alias('childViews.firstObject'),
+  layoutName:'components/deletePopover',
+  deleteForever:false,
+  actions:{
+    confirm:function (deleteForever) {
+      this.sendAction('confirm',this.get('deleteForever'));
+    },
+    close:function () {
+      this.set('popover.isVisible',false);
+    }
+  },
+  didInsertElement:function () {
+    $('body').on('click.popover', Em.run.bind(this,this.hideMultiply));
+  },
+  hideMultiply:function (e) {
+    if (!this.$()) {
+      return;
+    }
+    if (!this.$().is(e.target)
+        && this.$().has(e.target).length === 0
+        && $('.popover').has(e.target).length === 0) {
+          this.set('popover.isVisible',false);
+    }
+  },
+  willClearRender:function () {
+    this.get('popover').$element.off('click');
+    $('body').off('click.popover');
+  },
+});

+ 3 - 4
contrib/views/files/src/main/resources/ui/app/components/renameInput.js

@@ -21,10 +21,10 @@ var App = require('app');
 
 App.RenameInputComponent = Ember.Component.extend({
   tagName:'span',
-  layoutName:'util/renameInput',
+  layoutName:'components/renameInput',
   actions:{
     rename:function (opt) {
-      var target, tmpName;
+      var tmpName;
 
       switch (opt) {
         case 'edit': this.set('isRenaming',true); break;
@@ -34,8 +34,7 @@ App.RenameInputComponent = Ember.Component.extend({
           if (tmpName.length==0) {
             break;
           };
-          target = this.get('targetObject');
-          target.send(this.get('actionName'),this.get('filePath'),tmpName);
+          this.sendAction('confirm',this.get('filePath'),tmpName);
           this.set('isRenaming',false);
           break;
 

+ 39 - 0
contrib/views/files/src/main/resources/ui/app/components/sortArrow.js

@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.SortArrowComponent = Em.Component.extend({
+  layout:Ember.Handlebars.compile('<i {{bind-attr class=":fa view.asc:fa-chevron-down:fa-chevron-up view.cur::fa-gr view.cur::fa-rotate-270" }} ></i>'),
+  classNames:['pull-right'],
+  tagName:'span',
+  sPs:[],
+  sA:false,
+  sP:null,
+  asc:true,
+  cur:false,
+  sorting:function () {
+    if (this.get('sPs.firstObject') == this.get('sP')) {
+      this.set('asc',this.get('sA'));
+      this.set('cur',true);
+    } else{
+      this.set('asc',true);
+      this.set('cur',false);
+    };
+  }.observes('sPs','sA').on('init'),
+});

+ 56 - 0
contrib/views/files/src/main/resources/ui/app/components/toggleContext.js

@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.ToggleContextComponent = Em.Component.extend({
+  didInsertElement:function () {
+    var fileRow = this.$().parents('.file-row'),
+        beforeHandler = Ember.run.bind(this, this.setContext),
+        itemHandler = Ember.run.bind(this, this.itemHandler);
+
+    fileRow.on('click',Ember.run.bind(this, this.openOnClick));
+
+    fileRow.contextmenu({
+      target:'#context-menu',
+      before:beforeHandler,
+      onItem:itemHandler
+    });
+  },
+  setContext:function(e) {
+    if (this.get('targetObject.isMoving')) {
+      return false;
+    };
+    this.set('targetObject.parentController.targetContextMenu',this.get('targetObject'));
+    return true;
+  },
+  itemHandler:function (t,e) {
+    if (e.target.dataset.disabled) {
+      return false;
+    };
+  },
+  openOnClick:function (e) {
+    if($(e.target).is('td') || $(e.target).hasClass('allow-open')){
+      this.get('targetObject').send('open');
+    }
+  },
+  willClearRender:function () {
+    this.$().parents('.file-row').off('click');
+    this.$().parents('.file-row').off('.context.data-api').removeData('context');
+  }
+});

+ 9 - 4
contrib/views/files/src/main/resources/ui/app/components/uploader.js

@@ -21,6 +21,12 @@ var App = require('app');
 
 App.FileUploaderComponent = Ember.Component.extend({
   didInsertElement:function () {
+    var _this = this;
+    this.uploader.reopen({
+      sendAlert:function (e) {
+        _this.sendAction('alert',e);
+      }
+    });
     this.fileInput.reopen({
       filesDidChange: function() {
         var files = this.get('files');
@@ -42,12 +48,11 @@ App.FileUploaderComponent = Ember.Component.extend({
   },
   actions:{
     upload:function (files) {
-      var self = this;
       var uploader = this.get('uploader');
       var uploadBtn = Ladda.create(this.uploadButton.get('element'));
       var reset = function () {
         uploadBtn.stop();
-        self.send('clear');
+        this.send('clear');
       };
       if (!uploader.get('isUploading')) {
         var path = this.get('path');
@@ -57,7 +62,7 @@ App.FileUploaderComponent = Ember.Component.extend({
           uploader.on('progress',function (e) {
             uploadBtn.setProgress(e.percent/100);
           })
-          uploader.upload(file,{path:path}).then(reset,reset);
+          uploader.upload(file,{path:path}).finally(Em.run.bind(this,reset));
         }
       };
     },
@@ -66,7 +71,7 @@ App.FileUploaderComponent = Ember.Component.extend({
     }
   },
   uploader: null,
-  layoutName:'util/uploader',
+  layoutName:'components/uploader',
   path:'',
   info:'',
   files:null,

+ 2 - 3
contrib/views/files/src/main/resources/ui/app/controllers/error.js

@@ -17,9 +17,6 @@
  */
 
 App.ErrorController = Ember.ObjectController.extend({
-  init:function () {
-    this._super();
-  },
   actions: {
     toggleStackTrace:function () {
       var value = this.get('isExpanded');
@@ -35,6 +32,8 @@ App.ErrorController = Ember.ObjectController.extend({
     if (content && content.responseText) {
       var json = JSON.parse(content.responseText);
       text = json.message;
+    } else if (content && content.message) {
+      text = content.message;
     }
     return text;
   }.property('content'),

+ 51 - 53
contrib/views/files/src/main/resources/ui/app/controllers/file.js

@@ -16,6 +16,8 @@
  * limitations under the License.
  */
 
+var App = require('app');
+
 App.FileController = Ember.ObjectController.extend({
   init:function () {
     this.set('content.selected', false);
@@ -25,41 +27,33 @@ App.FileController = Ember.ObjectController.extend({
     download:function (option) {
       this.store.linkFor([this.get('content')],option).then(function (link) {
         window.location.href = link;
-      });
+      },Em.run.bind(this,this.sendAlert));
+    },
+    showChmod:function () {
+      this.toggleProperty('chmodVisible',true);
     },
-    rename:function (opt,file) {
+    rename:function (opt,name) {
       var file = this.get('content'),
-          self,path,name,newPath;
-      if (opt === 'edit') {
-        this.set('tmpName',file.get('name'));
-        this.set('isRenaming',true);
-      };
-
-      if (opt === 'cancel') {
-        this.set('tmpName','');
-        this.set('isRenaming',false);
-      };
-
-      if (opt === 'confirm') {
-        self = this;
-        path = file.get('path');
-        name = this.get('tmpName');
+          path = file.get('path'),
+          newPath;
 
-        if (Em.isEmpty(name)) {
-          return false;
-        }
-
-        if (name === file.get('name')) {
-          return self.set('isRenaming',false);
-        }
+      if (name === file.get('name') || Em.isEmpty(name)) {
+        return this.set('isRenaming',!Em.isEmpty(name));
+      }
 
-        newPath = path.substring(0,path.lastIndexOf('/')+1)+name;
+      newPath = path.substring(0,path.lastIndexOf('/')+1)+name;
 
-        this.store.move(file,newPath).then(function () {
-          self.set('tmpName','');
-          self.set('isRenaming',false);
-        });
-      };
+      this.store.move(file,newPath)
+        .then(Em.run.bind(this,this.set,'isRenaming',false),Em.run.bind(this,this.sendAlert));
+    },
+    editName:function () {
+      this.set('isRenaming',true);
+    },
+    chmod:function (r) {
+      var record = this.get('content');
+      this.store
+        .chmod(record)
+        .then(null,Em.run.bind(this,this.chmodErrorCallback,record));
     },
     open:function (file) {
       if (this.get('content.isDirectory')) {
@@ -68,32 +62,22 @@ App.FileController = Ember.ObjectController.extend({
         return this.send('download');
       };
     },
-    removeFile:function (opt) {
-      if (opt=='ask') {
-        this.toggleProperty('isRemoving');
-        console.log('ask removeFile')
-        return false;
-      };
-
-      if (opt == 'cancel'  && !this.isDestroyed) {
-        this.set('isRemoving',false);
-        console.log('cancel removeFile')
-      }
-
-      if (opt == 'confirm') {
-        this.set('isRemoving',false);
-        this.store.remove(this.get('content'));
-      }
-    },
-    deleteFile:function () {
-      var file = this.get('content');
-      this.store.remove(file);
+    deleteFile:function (deleteForever) {
+      this.store
+        .remove(this.get('content'),!deleteForever)
+        .then(null,Em.run.bind(this,this.sendAlert));
     },
   },
-  tmpName:'',
   selected:false,
   isRenaming:false,
-  isRemoving:false,
+  isMovingToTrash:false,
+  chmodVisible:false,
+  targetContextMenu:null,
+  isPermissionsDirty:function () {
+    var file = this.get('content');
+    var diff = file.changedAttributes();
+    return !!diff.permission;
+  }.property('content.permission'),
   isMoving:function () {
     var movingFile = this.get('parentController.movingFile.path');
     var thisFile = this.get('content.id');
@@ -102,5 +86,19 @@ App.FileController = Ember.ObjectController.extend({
   
   setSelected:function (controller,observer) {
     this.set('selected',this.get(observer))
-  }.observes('content.selected')
+  }.observes('content.selected'),
+
+  renameSuccessCallback:function (record,error) {
+    record.rollback();
+    this.sendAlert(error);
+  },
+
+  chmodErrorCallback:function (record,error) {
+    record.rollback();
+    this.sendAlert(error);
+  },
+
+  sendAlert:function (error) {
+    this.send('showAlert',error);
+  },
 });

+ 46 - 57
contrib/views/files/src/main/resources/ui/app/controllers/files.js

@@ -17,11 +17,12 @@
  */
 
 var App = require('app');
+var bind = Ember.run.bind;
 
 App.FilesController = Ember.ArrayController.extend({
   actions:{
     moveFile:function (opt,file) {
-      var src, title, self,
+      var src, title,
           file = file || this.get('selectedFiles.firstObject'),
           moving = this.get('movingFile');
 
@@ -32,11 +33,8 @@ App.FilesController = Ember.ArrayController.extend({
       };
 
       if (opt == 'move') {
-        self = this;
         this.store.move(moving.path,[this.get('path'),moving.name].join('/').replace('//','/'))
-          .then(function () {
-            self.set('movingFile',null);
-          });
+          .then(bind(this,this.set,'movingFile',null),bind(this,this.throwAlert));
       };
 
       if (opt == 'cancel') {
@@ -47,7 +45,7 @@ App.FilesController = Ember.ArrayController.extend({
       this.toggleProperty('isRenaming');
     },
     renameDir:function (path,newName) {
-      var self = this,
+      var _this = this,
           basedir = path.substring(0,path.lastIndexOf('/')+1);
           newPath = basedir + newName;
 
@@ -59,25 +57,28 @@ App.FilesController = Ember.ArrayController.extend({
         var recordExists = listdir.isAny('id',newPath);
 
         listdir.forEach(function (file) {
-          self.store.unloadRecord(file);
+          _this.store.unloadRecord(file);
         });
 
         if (recordExists) {
-          return self.send('showAlert',{message:newPath + ' already exists.'});
+          return _this.throwAlert({message:newPath + ' already exists.'});
         };
 
-        self.store.move(path,newPath).then(function (newDir) {
-          self.store.unloadRecord(newDir);
-          self.set('path',newPath);
-        });
-      });
+        return _this.store.move(path,newPath);
+      }).then(function (newDir) {
+        if (newDir) {
+          _this.store.unloadRecord(newDir);
+          _this.set('path',newPath);
+        };
+      }).catch(bind(this,this.throwAlert));
 
     },
-    deleteFile:function () {
+    deleteFile:function (deleteForever) {
       var self = this;
       var selected = this.get('selectedFiles');
+      var moveToTrash = !deleteForever;
       selected.forEach(function (file) {
-        self.store.remove(file);
+        self.store.remove(file,moveToTrash).then(null,bind(self,self.throwAlert));
       });
     },
     download:function (option) {
@@ -86,31 +87,9 @@ App.FilesController = Ember.ArrayController.extend({
         window.location.href = link;
       });
     },
-    mkdir:function (opt) {
-      var name,self,newDir;
-      if (opt === 'edit') {
-        this.set('isMkdir',true);
-      };
-
-      if (opt === 'cancel') {
-        this.set('newDirName','');
-        this.set('isMkdir',false);
-      };
-
-      if (opt === 'confirm') {
-        self = this;
-        name = this.get('newDirName');
-
-        if (Em.isEmpty(name)) {
-          return false;
-        }
-        newDir = [this.get('path'),name].join('/').replace('//','/');
-
-        this.store.mkdir(newDir).then(function () {
-          self.set('newDirName','');
-          self.set('isMkdir',false);
-        });
-      };
+    mkdir:function (newDirName) {
+      this.store.mkdir(newDirName)
+        .then(bind(this,this.mkdirSuccessCalback),bind(this,this.throwAlert));
     },
     upload:function (opt) {
       if (opt === 'open') {
@@ -129,6 +108,9 @@ App.FilesController = Ember.ArrayController.extend({
         this.set('sortProperties',[pr]);
         this.set('sortAscending',true);
       };
+    },
+    clearSearchField:function () {
+      this.set('searchString','');
     }
   },
   init:function () {
@@ -140,6 +122,7 @@ App.FilesController = Ember.ArrayController.extend({
 
       controller.store.pushPayload('file',{file:e});
     });
+    this._super();
   },
 
   sortProperties: ['name'],
@@ -149,10 +132,7 @@ App.FilesController = Ember.ArrayController.extend({
   movingFile:null,
   uploader:App.Uploader,
   isRenaming:false,
-  isRemoving:false,
-  isMkdir:false,
   isUploading:false,
-  newDirName:'',
   queryParams: ['path'],
   path: '/',
   isRootDir:Ember.computed.equal('path', '/'),
@@ -165,23 +145,32 @@ App.FilesController = Ember.ArrayController.extend({
   }.property('path'),
   selectedOne:Ember.computed.equal('selectedFiles.length', 1),
   isSelected:Ember.computed.gt('selectedFiles.length', 0),
-  selectedFiles:Ember.computed.filterBy('content', 'selected', true),
+  selectedFiles:function () {
+    return this.get('content').filterBy('selected', true);
+  }.property('content.@each.selected'),
   canConcat:function () {
     return this.get('selectedFiles').filterProperty('isDirectory').get('length')==0;
   }.property('selectedFiles.length'),
 
-  fileList: Ember.computed.alias('arrangedContent')
-});
+  searchString:'',
+  fileList: function () {
+    var fileList = this.get('arrangedContent');
+    var search = this.get('searchString');
+    return (search)?fileList.filter(function (file) {
+      return !!file.get('name').match(search);
+    }):fileList;
+  }.property('arrangedContent','searchString'),
+
+  mkdirSuccessCalback:function (newDir) {
+    if (newDir.get('path') != [this.get('path'),newDir.get('name')].join('/')){
+      newDir.unloadRecord();
+      newDir.store.listdir(this.get('path'));
+    }
+  },
 
-App.FilesAlertController = Em.ObjectController.extend({
-  content:null,
-  output:function () {
-    var error = this.get('content'),output;
-    if (error instanceof Em.Error) {
-      output = error;
-    } else {
-      output = {status:error.status, message:error.statusText||error.message};
-    };
-    return output;
-  }.property('content')
+  throwAlert:function (error) {
+    this.send('showAlert',error);
+  }
 });
+
+

+ 10 - 0
contrib/views/files/src/main/resources/ui/app/controllers/filesAlert.js

@@ -17,4 +17,14 @@
  */
 
 App.FilesAlertController = App.ErrorController.extend({
+  content:null,
+  output:function () {
+    var error = this.get('content'),output;
+    if (error instanceof Em.Error) {
+      output = error;
+    } else {
+      output = {status:error.status, message:error.statusText||error.message};
+    };
+    return output;
+  }.property('content')
 });

+ 19 - 5
contrib/views/files/src/main/resources/ui/app/initialize.js

@@ -30,12 +30,16 @@ require('templates/application');
 require('templates/index');
 require('templates/files');
 require('templates/error');
-require('templates/util/deletePopover');
-require('templates/util/uploader');
-require('templates/util/contextMenu');
-require('templates/util/deleteBulk');
 require('templates/util/errorRow');
-require('templates/util/renameInput');
+require('templates/util/fileRow');
+
+require('templates/components/uploader');
+require('templates/components/renameInput');
+require('templates/components/deletePopover');
+require('templates/components/mkdirInput');
+require('templates/components/contextMenu');
+require('templates/components/deleteBulk');
+require('templates/components/chmodInput');
 
 //////////////////////////////////
 // Models
@@ -59,12 +63,22 @@ require('controllers/filesAlert');
 require('components/uploader');
 require('components/contextMenu');
 require('components/renameInput');
+require('components/bsPopover');
+require('components/confirmDelete');
+require('components/sortArrow');
+require('components/breadCrumbs');
+require('components/popoverDelete');
+require('components/bulkCheckbox');
+require('components/mkdirInput');
+require('components/toggleContext');
+require('components/chmodInput');
 
 /////////////////////////////////
 // Views
 /////////////////////////////////
 
 require('views/file');
+require('views/filesAlert');
 
 /////////////////////////////////
 // Routes

+ 3 - 0
contrib/views/files/src/main/resources/ui/app/routes/file.js

@@ -25,6 +25,9 @@ App.FilesRoute = Em.Route.extend({
     }
   },
   actions:{
+    refreshDir:function () {
+      this.refresh();
+    },
     error:function (error,transition,e) {
       if (this.router._lookupActiveView('files')) {
         this.send('showAlert',error);

+ 61 - 6
contrib/views/files/src/main/resources/ui/app/styles/application.less

@@ -68,6 +68,22 @@
     .i-am-in {
       margin: 0;
       width: 80%;
+      .dir-name {
+        color: black;
+      }
+    }
+    .input-group-search {
+      .input-search {
+        padding-right: 25px;
+      }
+      .form-control-feedback {
+        position: absolute;
+        z-index: 2;
+        top: 8px;
+        right: 39px;
+        cursor: pointer;
+        opacity: 0.5;
+      }
     }
   }
 
@@ -139,6 +155,24 @@
           }
         }
       }
+      .chmod-row {
+        &:hover > td {
+          background-color: #fff;
+          cursor: default;
+        }
+        & > td {
+          border-top: 0;
+          padding: 0;
+        }
+/*         .chmod-wrap {
+  transition: all 0.3s ease;
+  opacity: 1;
+  margin-right: 0px;
+  height: 32px;
+  overflow: hidden;
+  padding-top: 5px;
+} */
+      }
       .btn-delete {
         .popover-content{
           width: 80px;
@@ -161,8 +195,8 @@
           width: 130px;
           margin-bottom: 0;
           .delete-forever {
-            display: inline-block;
-            margin: 5px;
+            float: right;
+            margin: 0px 0px 0 10px;
           }
         }
         .mod-time{
@@ -249,6 +283,27 @@
   }
 }
 
+#bulkDropdown {
+  .sub-label{
+    display: inline-block;
+    width: 55%;
+  }
+}
+
+#context-menu {
+  .sub-label{
+    display: inline-block;
+    width: 55%;
+  }
+  .dropdown-confirm {
+    margin: -4px 8px;
+  }
+}
+
+.dropdown-confirm {
+  margin: -4px 0;
+}
+
 
 .fa-right {
   top: 3px;
@@ -293,10 +348,6 @@
   }
 }
 
-.dropdown-confirm {
-  margin: -4px 0;
-}
-
 .renameable {
   display: inline-block;
   &.half {
@@ -321,3 +372,7 @@
     margin-left: -1px;
   }
 }
+.modal-backdrop.in {
+  filter: alpha(opacity=0);
+  opacity: 0;
+}

+ 100 - 0
contrib/views/files/src/main/resources/ui/app/templates/components/chmodInput.hbs

@@ -0,0 +1,100 @@
+{{!
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF 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.
+}}
+
+<td colspan="8" class="">
+
+<div class="modal chmodal" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static">
+  <div class="modal-dialog modal-sm">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+        <h4 class="modal-title">Edit permission</h4>
+      </div>
+      <div class="modal-body">
+
+        <form class="form-horizontal" role="form">
+          <div class="form-group">
+            <label class="col-sm-2 control-label">User</label>
+            <div class="col-sm-10">
+              <div class="btn-group" data-toggle="buttons">
+                <label {{bind-attr class=":btn :btn-sm usrR:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=usrR}} <span>Read</span>
+                </label>
+                <label {{bind-attr class=":btn :btn-sm usrW:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=usrW}} <span>Write</span>
+                </label>
+                <label {{bind-attr class=":btn :btn-sm usrE:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=usrE}} <span>Execute</span>
+                </label>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="col-sm-2 control-label">Group</label>
+            <div class="col-sm-10">
+              <div class="btn-group" data-toggle="buttons">
+                <label {{bind-attr class=":btn :btn-sm grpR:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=grpR}} <span>Read</span>
+                </label>
+                <label {{bind-attr class=":btn :btn-sm grpW:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=grpW}} <span>Write</span>
+                </label>
+                <label {{bind-attr class=":btn :btn-sm grpE:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=grpE}} <span>Execute</span>
+                </label>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="col-sm-2 control-label">Other</label>
+            <div class="col-sm-10">
+              <div class="btn-group" data-toggle="buttons">
+                <label {{bind-attr class=":btn :btn-sm otrR:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=otrR}} <span>Read</span>
+                </label>
+                <label {{bind-attr class=":btn :btn-sm otrW:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=otrW}} <span>Write</span>
+                </label>
+                <label {{bind-attr class=":btn :btn-sm otrE:btn-primary:btn-default :btn-chmod" }} >
+                  {{input type="checkbox" checked=otrE}} <span>Execute</span>
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div class="form-group">
+            <div class="col-sm-offset-2 col-sm-10">
+              <div class="checkbox">
+                <label>
+                  {{input type="checkbox"}} <span> Modify recursively</span>
+                </label>
+              </div>
+            </div>
+          </div>
+        </form>
+
+      </div>
+      <div class="modal-footer">
+        <button type="button" class="btn btn-default" {{action 'close'}}>Close</button>
+        <button type="button" class="btn btn-primary" {{action 'confirm'}}>Save changes</button>
+      </div>
+    </div>
+  </div>
+</div>
+
+</td>

+ 6 - 19
contrib/views/files/src/main/resources/ui/app/templates/util/contextMenu.hbs → contrib/views/files/src/main/resources/ui/app/templates/components/contextMenu.hbs

@@ -16,6 +16,7 @@
    limitations under the License.
 }}
 
+{{#dropdown-wrap}}
 <div id="context-menu">
   <ul class="dropdown-menu dropdown-context compressed-context" role="menu">
     {{#if view.target.content.isDirectory}} 
@@ -24,8 +25,8 @@
     <li><a tabindex="-1" href="#" {{action 'download'}}>Download</a></li>
     {{/if}}
     <li><a tabindex="-1" href="#" {{action 'moveFile' 'cut' view.target.content}}>Move</a></li>
-    <li class="disabled"><a data-disabled="disabled" tabindex="-1" href="#">Permissions</a></li>
-    <li><a tabindex="-1" href="#" {{action 'rename' 'edit'}} >Rename</a></li>
+    <li><a tabindex="-1" href="#" {{action 'showChmod'}} >Permissions</a></li>
+    <li><a tabindex="-1" href="#" {{action 'editName'}} >Rename</a></li>
     <li class="divider"></li>
     <li class="dropdown-submenu">
       <a href="#" data-disabled="disabled">
@@ -33,24 +34,10 @@
         <i class="fa fa-chevron-right pull-right fa-right"></i>
       </a>
       <ul class="dropdown-menu">
-        {{#if view.target.isRemoving}}
-        <li>
-        <a  tabindex="-1" href="#" data-disabled="disabled">
-          Delete forever
-          <div class="btn-group text-center dropdown-confirm">
-            <button {{action 'removeFile' 'cancel'}} type="button" class="btn btn-xs btn-danger">
-              <i class="fa fa-times"></i>
-            </button>
-            <button {{action 'removeFile' 'confirm'}} type="button" class="btn btn-xs btn-success">
-              <i class="fa fa-check"></i>
-            </button>
-          </div>
-        </a>
-        </li>
-        {{else}}
-        <li><a class="confirm-action" data-action="removeFile" tabindex="-1" href="#">Delete forever</a></li>
-        {{/if}}
+        {{confirm-delete confirm="removeFile" deleteForever=true }}
+        {{confirm-delete confirm="moveToTrash" deleteForever=false }}
       </ul>
     </li>
   </ul>
 </div>
+{{/dropdown-wrap}}

+ 16 - 8
contrib/views/files/src/main/resources/ui/app/templates/util/deleteBulk.hbs → contrib/views/files/src/main/resources/ui/app/templates/components/deleteBulk.hbs

@@ -17,22 +17,30 @@
 }}
 
 {{#if isRemoving}}
-<a  tabindex="-1" href="#">
-  <i class="fa fa-exclamation-triangle"></i>
-  Delete forever
+<a  tabindex="-1">
+  {{#if deleteForever}}
+    <i class="fa fa-fw fa-exclamation-triangle"></i>
+    <span class="sub-label" > Delete forever </span>
+  {{else}}
+    <i class="fa fa-fw fa-trash-o"></i>
+    <span class="sub-label" >Move To Trash</span>
+  {{/if}}
   <div class="btn-group text-center dropdown-confirm">
-    <button {{action 'ask' target="view" }} type="button" class="btn btn-xs btn-danger">
+    <button {{action 'cancel'}} type="button" class="btn btn-xs btn-danger">
       <span class="glyphicon glyphicon-remove"></span>
     </button>
-    <button {{action 'deleteFile'}} type="button" class="btn btn-xs btn-success delete">
+    <button {{action 'confirm'}} type="button" class="btn btn-xs btn-success delete">
       <span class="glyphicon glyphicon-ok delete"></span>
     </button>
   </div>
 </a>
 {{else}}
-<a {{action 'ask' target="view" }} tabindex="-1" href="#">
-  <i class="fa fa-exclamation-triangle"></i>
-  Delete forever
+<a {{action 'ask'}} tabindex="-1" href="#">
+  {{#if deleteForever}}
+    <i class="fa fa-fw fa-exclamation-triangle"></i> <span class="sub-label" > Delete forever </span>
+  {{else}}
+    <i class="fa fa-fw fa-trash-o"></i> <span class="sub-label" >Move To Trash</span>
+  {{/if}}
 </a>
 {{/if}}
 

+ 6 - 6
contrib/views/files/src/main/resources/ui/app/templates/util/deletePopover.hbs → contrib/views/files/src/main/resources/ui/app/templates/components/deletePopover.hbs

@@ -16,23 +16,23 @@
    limitations under the License.
 }}
 
-<a {{action 'show' target="view"}} data-toggle="tooltip" data-placement="bottom" title="Delete"> <i class="fa fa-trash-o fa-lg"></i> </a>
+<a data-toggle="tooltip" data-placement="bottom" title="Delete"> <i class="fa fa-trash-o fa-lg"></i> </a>
 
-<div class='df-popover hide'>
+{{#bs-popover triggers='click' placement='left'}}
   <div class="input-group" >
     <div class="btn-group ">
-      <button {{action 'close' target="view"}} type="button" class="btn btn-xs btn-danger">
+      <button {{action 'close'}} type="button" class="btn btn-xs btn-danger">
         <i class="fa fa-times fa-fw"></i>
       </button>
-      <button {{action 'deleteFile'}} type="button" class="btn btn-xs btn-success">
+      <button {{action 'confirm'}} type="button" class="btn btn-xs btn-success">
         <i class="fa fa-check fa-fw"></i>
       </button>
     </div>
     <div class="checkbox delete-forever">
       <label>
-        <input type="checkbox"> Delete forever
+      {{input type="checkbox" checkedBinding='deleteForever' }} Delete forever
       </label>
     </div>
   </div>
-</div>
+{{/bs-popover}}
 

+ 37 - 0
contrib/views/files/src/main/resources/ui/app/templates/components/mkdirInput.hbs

@@ -0,0 +1,37 @@
+{{!
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF 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.
+}}
+
+{{#unless isMkdir}} 
+  <button type="button" {{action 'edit'}} {{bind-attr class=":btn :btn-default :btn-sm :pull-right :mkdirwrap"}}>
+    <i class="fa fa-plus"></i> New directory
+  </button>
+{{else}}
+  <div class="input-group input-group-sm pull-right mkdir-area">
+    {{input class="form-control mkdir-input" valueBinding='newDirName' placeholder="Enter Directory Name"}}
+    <div class="input-group-btn">
+      <button  type="button" {{action 'cancel'}} {{bind-attr class=":btn :btn-danger :btn-sm :btn-mkdir-cancel"}} >
+        <i class="fa fa-times"></i> Cancel
+      </button>
+    </div>
+    <div class="input-group-btn">
+      <button  type="button" {{action 'create'}} {{bind-attr class="newDirName::disabled :btn :btn-success :btn-sm :btn-mkdir"}} >
+        <i class="fa fa-check"></i> Create
+      </button>
+    </div>
+  </div>
+{{/unless}}

+ 0 - 0
contrib/views/files/src/main/resources/ui/app/templates/util/renameInput.hbs → contrib/views/files/src/main/resources/ui/app/templates/components/renameInput.hbs


+ 0 - 0
contrib/views/files/src/main/resources/ui/app/templates/util/uploader.hbs → contrib/views/files/src/main/resources/ui/app/templates/components/uploader.hbs


+ 75 - 154
contrib/views/files/src/main/resources/ui/app/templates/files.hbs

@@ -19,13 +19,13 @@
 <div class="panel-default panel-files">
   <div class="panel-heading">
     {{!-- BREADCRUMBS --}}  
-    {{view view.breadcrumbsView}}
+    {{bread-crumbs path=path}}
       
     <div class="um-section">
     {{!-- UPLOADER --}}  
     <div {{bind-attr class="isUploading::hide :pull-right" }}>
       <button {{action 'upload' 'close'}} type="button" class="close" aria-hidden="true">&times;</button>
-      {{file-uploader path=path uploader=uploader class=" upload-area pull-right"}}
+      {{file-uploader path=path uploader=uploader class=" upload-area pull-right" alert='showAlert'}}
     </div>
 
     <div {{bind-attr class="isUploading:hide: :pull-right :uploadwrap" }}>
@@ -34,57 +34,26 @@
       </button>
     </div>
 
-    {{!-- MKDIR --}}  
-    {{#unless isMkdir}} 
-      <button type="button" {{action 'mkdir' 'edit'}} {{bind-attr class=":btn :btn-default :btn-sm :pull-right :mkdirwrap"}}>
-        <i class="fa fa-plus"></i> New directory
-      </button>
-    {{else}}
-      <div class="input-group input-group-sm pull-right mkdir-area">
-        {{input class="form-control mkdir-input" valueBinding='newDirName'}}
-        <div class="input-group-btn">
-          <button  type="button" {{action 'mkdir' 'cancel'}} {{bind-attr class=":btn :btn-danger :btn-sm :btn-mkdir-cancel"}} >
-            <i class="fa fa-times"></i> Cancel
-          </button>
-        </div>
-        <div class="input-group-btn">
-          <button  type="button" {{action 'mkdir' 'confirm'}} {{bind-attr class=":btn :btn-success :btn-sm :btn-mkdir"}} >
-            <i class="fa fa-check"></i> Create
-          </button>
-        </div>
-      </div>
-    {{/unless}}
-
+    {{!-- MKDIR --}} 
+    {{mkdir-input create="mkdir" path=path}}
 
     </div>
   </div>
 
   <div class="panel-body">
     <h4 class="i-am-in pull-left"> <i class="fa fa-folder fa-lg"></i>
-    {{#rename-input file=path actionName='renameDir' isRenaming=isRenaming class='renameable stocked half'}}
-      {{currentDir}} <a href="#" {{bind-attr class="isRootDir:hide"}} {{action showRenameInput}}><i class="fa fa-edit"></i></a>
+    {{#rename-input file=path confirm='renameDir' isRenaming=isRenaming class='renameable stocked half'}}
+      <a href="#" class="dir-name" {{action refreshDir}}>{{currentDir}}</a>
+      <a href="#" {{bind-attr class="isRootDir:hide"}} {{action showRenameInput}}><i class="fa fa-edit"></i></a>
     {{/rename-input}}
     </h4>
 
-    <div class="btn-group btn-sort pull-right" data-toggle="tooltip" data-placement="left" title="Sort by:">
-      <button type="button" class="btn btn-xs btn-default" {{action sort 'toggle'}}>
-      {{#if sortAscending}} Asc {{else}} Desc {{/if}}
-      </button>
-
-      <button type="button" class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown">
-        <span>
-        {{capitalize sortProperties.firstObject}}
-        </span>
-        <span class="caret"></span>
-      </button>
-      <ul class="dropdown-menu" role="menu">
-        <li><a href="#" {{action 'sort' 'name'}} >Name</a></li>
-        <li><a href="#" {{action 'sort' 'size'}} >Size</a></li>
-        <li><a href="#" {{action 'sort' 'owner'}} >Owner</a></li>
-        <li><a href="#" {{action 'sort' 'group'}} >Group</a></li>
-        <li><a href="#" {{action 'sort' 'permission'}} >Permission</a></li>
-        <li><a href="#" {{action 'sort' 'date'}} >Date</a></li>
-      </ul>
+    <div class="input-group input-group-sm input-group-search">
+      {{input valueBinding='searchString' class="form-control input-search" placeholder="Search File Names" }}
+      {{#if searchString}}
+        <i {{action 'clearSearchField'}} class="fa fa-times form-control-feedback"></i>
+      {{/if}}
+      <div class="input-group-addon"><i class="fa fa-search"></i></div>
     </div>
   </div>
 
@@ -92,44 +61,66 @@
     <thead>
       <tr>
         <th class="icon"></th>
-        <th class="path" {{action 'sort' 'name'}}> Name {{view view.sortArrow sortProperty='name'}} </th>
-        <th class="size" {{action 'sort' 'size'}}>Size {{view view.sortArrow sortProperty='size'}}</th>
-        <th class="owner" {{action 'sort' 'owner'}}>Owner {{view view.sortArrow sortProperty='owner'}}</th>
-        <th class="grp" {{action 'sort' 'group'}} >Group {{view view.sortArrow sortProperty='group'}}</th>
-        <th class="perm" {{action 'sort' 'permission'}} >Permission {{view view.sortArrow sortProperty='permission'}}</th>
+        <th class="path" {{action 'sort' 'name'}}> Name {{sort-arrow sPs=sortProperties sA=sortAscending sP='name'}} </th>
+        <th class="size" {{action 'sort' 'size'}}>Size {{sort-arrow sPs=sortProperties sA=sortAscending sP='size'}}</th>
+        <th class="owner" {{action 'sort' 'owner'}}>Owner {{sort-arrow sPs=sortProperties sA=sortAscending sP='owner'}}</th>
+        <th class="grp" {{action 'sort' 'group'}} >Group {{sort-arrow sPs=sortProperties sA=sortAscending sP='group'}}</th>
+        <th class="perm" {{action 'sort' 'permission'}} >Permission {{sort-arrow sPs=sortProperties sA=sortAscending sP='permission'}}</th>
         <th class="download">
+          <div class="btn-group btn-sort pull-right" data-toggle="tooltip" data-placement="left" title="Sort by:">
+            <button type="button" class="btn btn-xs btn-default" {{action sort 'toggle'}}>
+            {{#if sortAscending}} Asc {{else}} Desc {{/if}}
+            </button>
+
+            <button type="button" class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown">
+              <span>
+              {{capitalize sortProperties.firstObject}}
+              </span>
+              <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu" role="menu">
+              <li><a href="#" {{action 'sort' 'name'}} >Name</a></li>
+              <li><a href="#" {{action 'sort' 'size'}} >Size</a></li>
+              <li><a href="#" {{action 'sort' 'owner'}} >Owner</a></li>
+              <li><a href="#" {{action 'sort' 'group'}} >Group</a></li>
+              <li><a href="#" {{action 'sort' 'permission'}} >Permission</a></li>
+              <li><a href="#" {{action 'sort' 'date'}} >Date</a></li>
+            </ul>
+          </div>
         </th>
         <th class="check"> 
+        {{#dropdown-wrap}}
           <div id="bulkDropdown" class="btn-group">
-              <span class="input-group-addon">
-                <div class="checkbox">
-                    {{view view.checkboxAll contentBinding='fileList'}}
-                </div>
-              </span>
-          <button  type="button" data-toggle="dropdown" {{bind-attr class=":btn :btn-xs :btn-default :dropdown-toggle isSelected::disabled"}} >
-            <span class="caret"></span>
-          </button>
-          <ul class="dropdown-menu pull-right" role="menu">
-            <li><a href="#"  {{action 'download' 'zip'}} ><i class="fa fa-archive fa-fw"></i> Download zip</a></li>
-            {{#if canConcat}}
-            <li><a href="#"  {{action 'download' 'concat'}} ><i class="fa fa-th fa-fw"></i> Concat</a></li>
-            {{/if}}
-            <li class="divider"></li>
-            <li class="dropdown-submenu">
-              <a href="#" disabled="disabled">
-                <i class="fa fa-chevron-left fa-gr fa-fw"></i> Delete
-              </a>
-              <ul class="dropdown-menu left-submenu">
-                {{view view.deleteBulkView}}
-              </ul>
-            </li>
-          </ul>
-        </div>
+            <span class="input-group-addon">
+              <div class="checkbox">
+                {{bulk-checkbox content=fileList}}
+              </div>
+            </span>
+            <button  type="button" data-toggle="dropdown" {{bind-attr class=":btn :btn-xs :btn-default :dropdown-toggle isSelected::disabled"}} >
+              <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu pull-right" role="menu">
+              <li><a href="#"  {{action 'download' 'zip'}} ><i class="fa fa-archive fa-fw"></i> Download zip</a></li>
+              {{#if canConcat}}
+              <li><a href="#"  {{action 'download' 'concat'}} ><i class="fa fa-th fa-fw"></i> Concat</a></li>
+              {{/if}}
+              <li class="divider"></li>
+              <li class="dropdown-submenu">
+                <a href="#" disabled="disabled">
+                  <i class="fa fa-chevron-left fa-gr fa-fw"></i> Delete
+                </a>
+                <ul class="dropdown-menu left-submenu">
+                  {{confirm-delete confirm="deleteFile" deleteForever=true selector='bulkDropdown'}}
+                  {{confirm-delete confirm="deleteFile" deleteForever=false selector='bulkDropdown'}}
+                </ul>
+              </li>
+            </ul>
+          </div>
+        {{/dropdown-wrap}}
       </th>
       </tr>
     </thead>
     <tbody>
-    <div>
       <tr>
         <td><i class="fa fa-folder"></i></td>
         <td  {{action 'dirUp'}} colspan="7">
@@ -167,7 +158,7 @@
           </td>
           <td>
             {{#unless content.isDirectory}}
-              {{humanSize movingFile.size}}
+              {{humanSize movingFile.len}}
             {{/unless}}
           </td>
           <td >{{movingFile.owner}}</td>
@@ -186,87 +177,17 @@
         </tr>
       {{/if}}
       {{/unless}}
-    </div>
       {{#each fileList itemController="file"}}
-        <tr {{bind-attr class=":file-row isMoving:isMoving"}}>
-          <td>
-            {{#if content.isDirectory}}
-            <i class="fa fa-folder"></i>
-            {{else}}
-            <i class="fa fa-file"></i>
-            {{/if}}
-          </td>
-          <td>
-            {{#unless isRenaming}}
-              <div class="file-name allow-open">
-                <span>
-                  <a {{action 'open'}}>
-                    <strong>
-                      {{content.name}}
-                    </strong>
-                  </a>
-                </span>
-                <span class="help-block mod-time allow-open">
-                  <small class='allow-open'>
-                    Updated {{showDate modificationTime 'YYYY-MM-DD HH:mm'}}
-                  </small>
-                </span>
-              </div>
-            {{else}}
-              <div class="input-group input-group-sm rename-area">
-                {{view view.renameInputView class="form-control rename-input" valueBinding='tmpName'}}
-                <div class="input-group-btn">
-                  <button  type="button" {{action 'rename' 'cancel'}} {{bind-attr class=":btn :btn-danger :btn-xs :btn-rename-cancel isRenaming:show"}} >
-                    <i class="fa fa-times"></i> Cancel
-                  </button>
-                </div>
-                <div class="input-group-btn">
-                  <button  type="button" {{action 'rename' 'confirm'}} {{bind-attr class=":btn :btn-success :btn-xs :btn-rename isRenaming:show"}} >
-                    <i class="fa fa-check"></i> Rename
-                  </button>
-                </div>
-              </div>
-            {{/unless}}
-          </td>
-          <td>
-            {{#unless content.isDirectory}}
-              {{humanSize content.size}}
-            {{/unless}}
-          </td>
-          <td >{{content.owner}}</td>
-          <td>{{content.group}}</td>
-          <td>{{content.permission}}</td>
-          <td>
-            {{#unless isMoving}}
-              <ul class="list-inline file-actions text-right">
-                <li>
-                {{#if content.isDirectory}}
-                  <a href="#" {{action 'download' 'zip'}} data-toggle="tooltip" data-placement="bottom" title="Download zip"><i class="fa fa-archive fa-fw fa-lg"></i></a>  
-                {{else}}
-                  <a href="#" {{action 'download' 'browse'}} data-toggle="tooltip" data-placement="bottom" title="Download"><i class="fa fa-download fa-fw fa-lg"></i></a>  
-                {{/if}}
-                </li>
-                <li>
-                  <a href="#" {{action 'moveFile' 'cut' this.content}} data-toggle="tooltip" data-placement="bottom" title="Move"><i class="fa fa-level-down fa-rotate-270 fa-fw fa-lg"></i></span></a>  
-                </li>
-                <li>
-                  <a {{action 'rename' 'edit'}} data-toggle="tooltip" data-placement="bottom" title="Rename"><i class="fa fa-edit fa-lg"></i></a>
-                </li>
-                <li>{{view view.deleteSingleView}}</li>
-              </ul>
-            {{/unless}}
-            </td>
-          <td>
-          {{#if isMoving}}
-            <a href="#" {{action 'moveFile' 'cancel' target="parentController" }} data-toggle="tooltip" data-placement="bottom" title="Cancel moving"> <i class="fa fa-times fa-lg"></i></a>
-          {{else}}
-            {{input type="checkbox" checkedBinding='content.selected'}}
-          {{/if}}
-          {{view view.togglecontext}}
-          </td>
-        </tr>
+        {{partial 'util/fileRow'}}
       {{/each}}
+      {{#unless fileList}}
+      <tr>
+        <td colspan="8">
+          No files
+        </td>
+      </tr>
+      {{/unless}}
     </tbody>
   </table>
-  {{view view.contextMenu }}
+  {{context-menu target=targetContextMenu}}
 </div>

+ 84 - 0
contrib/views/files/src/main/resources/ui/app/templates/util/fileRow.hbs

@@ -0,0 +1,84 @@
+{{!
+   Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF 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.
+}}
+
+<tr {{bind-attr class=":file-row isMoving:isMoving"}}>
+  <td>
+    {{#if content.isDirectory}}
+    <i class="fa fa-folder"></i>
+    {{else}}
+    <i class="fa fa-file"></i>
+    {{/if}}
+  </td>
+  <td>
+    {{#rename-input fileBinding='content' confirm='rename' isRenaming=isRenaming}}
+      <div class="file-name allow-open">
+        <span>
+          <a {{action 'open'}}>
+            <strong>
+              {{content.name}}
+            </strong>
+          </a>
+        </span>
+        <span class="help-block mod-time allow-open">
+          <small class='allow-open'>
+            Updated {{showDate modificationTime 'YYYY-MM-DD HH:mm'}}
+          </small>
+        </span>
+      </div>
+    {{/rename-input}}
+  </td>
+  <td>
+    {{#unless content.isDirectory}}{{humanSize content.size}}{{else}}-{{/unless}}
+  </td>
+  <td >{{content.owner}}</td>
+  <td>{{content.group}}</td>
+  <td class="permission-cell">
+    {{content.permission}}
+    {{#if isPermissionsDirty}} <span>*</span>  {{/if}}
+  </td>
+  <td>
+    {{#unless isMoving}}
+      <ul class="list-inline file-actions text-right">
+        <li>
+        {{#if content.isDirectory}}
+          <a href="#" {{action 'download' 'zip'}} data-toggle="tooltip" data-placement="bottom" title="Download zip"><i class="fa fa-archive fa-fw fa-lg"></i></a>  
+        {{else}}
+          <a href="#" {{action 'download' 'browse'}} data-toggle="tooltip" data-placement="bottom" title="Download"><i class="fa fa-download fa-fw fa-lg"></i></a>  
+        {{/if}}
+        </li>
+        <li>
+          <a href="#" {{action 'moveFile' 'cut' this.content}} data-toggle="tooltip" data-placement="bottom" title="Move"><i class="fa fa-level-down fa-rotate-270 fa-fw fa-lg"></i></span></a>  
+        </li>
+        {{!-- <li>
+          <a {{action 'rename' 'edit'}} data-toggle="tooltip" data-placement="bottom" title="Rename"><i class="fa fa-edit fa-lg"></i></a>
+        </li> --}}
+        <li>{{popover-delete confirm="deleteFile"}}</li>
+      </ul>
+    {{/unless}}
+    </td>
+  <td>
+  {{#if isMoving}}
+    <a href="#" {{action 'moveFile' 'cancel' target="parentController" }} data-toggle="tooltip" data-placement="bottom" title="Cancel moving"> <i class="fa fa-times fa-lg"></i></a>
+  {{else}}
+    {{input type="checkbox" checkedBinding='content.selected'}}
+  {{/if}}
+  {{toggle-context}}
+  </td>
+</tr>
+{{chmod-input chVisible=chmodVisible file=content confirm="chmod"}}
+        

+ 8 - 174
contrib/views/files/src/main/resources/ui/app/views/file.js

@@ -18,116 +18,17 @@
 
 var App = require('app');
 
-App.DeleteBulkView = Em.View.extend({
-  actions:{
-    ask:function (argument) {
-      this.get('controller').toggleProperty('isRemoving');
-      return false; 
-    }
-  }
-});
-
 App.FilesView = Em.View.extend({
     templateName: 'files',
     didInsertElement:function () {
-      $('.btn-sort').tooltip();
+      this.scheduleRebind();
+    },
+    scheduleRebind:function () {
+      Em.run.scheduleOnce('render', this, this.get('reBindTooltips'));
+    },
+    reBindTooltips:function () {
+      this.$().tooltip({selector:'[data-toggle=tooltip]'});
     },
-    deleteBulkView:App.DeleteBulkView.create({
-      didInsertElement:function(){
-        var self = this;
-        $('#bulkDropdown').on('hidden.bs.dropdown', function () {
-          self.get('controller').set('isRemoving',false);
-        })
-      },
-      templateName:'util/deleteBulk',
-      tagName:'li',
-      click:function  (e) {
-        if (!$(e.target).hasClass('delete')) {
-          e.stopPropagation();
-        };
-      }
-    }),
-    deleteSingleView: Em.View.extend({
-      popoverSelector:'.df-popover',
-      actions:{
-        close:function () {
-          $(this.get('element')).popover('toggle');
-        },
-        show:function () {
-          $(this.get('element')).popover('toggle');
-        }
-      },
-      didInsertElement:function () {
-        var self = this,
-            element = $(this.get('element'));
-        $(element).popover({
-          html:true,
-          trigger:'manual',
-          placement:'left',
-          content:function() {
-            var content = element.find('.df-popover');
-            return content.html();
-          }
-        });
-
-        $('body').on('click.popover', function (e) {
-          if (!element.is(e.target) 
-              && element.has(e.target).length === 0 
-              && $('.popover').has(e.target).length === 0) {
-            element.popover('hide');
-          }
-        });
-
-        element.on('hidden.bs.popover', function () {
-          element.parent().find('.popover').remove();
-        });
-      },
-      willClearRender:function () {
-        $('body').off('click.popover');
-      },
-      templateName:'util/deletePopover',
-    }),
-    checkboxAll:Em.Checkbox.extend({
-      changeBinding:'selectAll',
-      checkedBinding:'selectedAll',
-      selectedAll:false,
-      selectAll:function () {
-        var checked = this.get('checked');
-        var items = this.get('content');
-        return items.forEach(function (item) {
-          item.set('selected',checked);
-        });
-      },
-      selection:function () {
-        var selected = this.get('content').filterProperty('selected',true);
-        if (selected.length == this.get('content.length') && selected.length > 0) {
-          this.set('selectedAll',true);
-        } else {
-          this.set('selectedAll',false);
-        }
-      }.observes('content.@each.selected'),
-    }),
-    breadcrumbsView: Ember.CollectionView.extend({
-      classNames: ['breadcrumb pull-left'],
-      tagName: 'ul',
-      content: function (argument) {
-        var crumbs = [];
-        var currentPath = this.get('controller.path').match(/((?!\/)\S)+/g)||[];
-        currentPath.forEach(function (cur,i,array) {
-          return crumbs.push({name:cur,path:'/'+array.slice(0,i+1).join('/')});
-        });
-        crumbs.unshift({name:'/',path:'/'});
-        crumbs.get('lastObject').last = 'true';
-        return crumbs;
-      }.property('controller.path'),
-      itemViewClass: Ember.View.extend({
-        classNameBindings: ['isActive:active'],
-        template: Ember.Handlebars.compile("{{#link-to 'files' (query-params path=view.content.path)}}{{view.content.name}}{{/link-to}}"),
-        isActive: function () {
-          return this.get('content.last');
-        }.property('content')
-      })
-    }),
     renameInputView: Em.TextField.extend({
       controller:null,
       didInsertElement:function (argument) {
@@ -144,72 +45,5 @@ App.FilesView = Em.View.extend({
           return target.send('rename', 'cancel');
         };
       }
-    }),
-    togglecontext:Em.View.extend({
-      didInsertElement:function () {
-        var self = this;
-        var fileRow = $(this.get('element')).parents('.file-row');
-        fileRow.contextmenu({
-          target:'#context-menu',
-          before: function(e) {
-            if (self.get('controller.isMoving')) {
-              return false;
-            };
-            self.get('parentView.contextMenu').set('target',self.get('context'));
-            return true;
-          },
-          onItem:function (t,e) {
-            if (e.target.className=='confirm-action') {
-              console.log('set waitConfirm onItem');
-              self.get('parentView.contextMenu').set('waitConfirm',e.target.dataset.action);
-              return false;
-            }
-
-            if (e.target.dataset.disabled) {
-              return false;
-            };
-          }
-        });
-        fileRow.find('[data-toggle=tooltip]').tooltip();
-        fileRow.on('click',function(e){
-          if($(e.target).is('td') || $(e.target).hasClass('allow-open')){
-            self.get('controller').send('open');
-          }
-        });
-      },
-      reBind:function(){
-        var row = $(this.get('element')).parents('.file-row');
-        Em.run.next(function(){
-          row.find('[data-toggle=tooltip]').tooltip();
-        });
-
-      }.observes('context.isMoving')
-    }),
-    contextMenu: App.ContextMenu.create(),
-    sortArrow:Em.View.extend({
-      sortProperty:null,
-      asc:true,
-      cur:false,
-      sorting:function () {
-        if (this.get('controller.sortProperties.firstObject')==this.get('sortProperty')) {
-          this.set('asc',this.get('controller.sortAscending'));
-          this.set('cur',true);
-        } else{
-          this.set('asc',true);
-          this.set('cur',false);
-        };
-      }.observes('controller.sortProperties','controller.sortAscending').on('init'),
-      tagName:'span',
-      classNames:['pull-right'],
-      template:Ember.Handlebars.compile('<i {{bind-attr class=":fa view.asc:fa-chevron-down:fa-chevron-up view.cur::fa-gr view.cur::fa-rotate-270" }} ></i>')
-    }),
-    reBind:function(){
-      Em.run.next(function(){
-        $('.isMoving').find('[data-toggle=tooltip]').tooltip();
-      });
-    }.observes('controller.hideMoving')
-});
-
-App.FilesAlertView = Em.View.extend({
-  templateName:'util/errorRow'
+    })
 });

+ 23 - 0
contrib/views/files/src/main/resources/ui/app/views/filesAlert.js

@@ -0,0 +1,23 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+
+var App = require('app');
+
+App.FilesAlertView = Em.View.extend({
+  templateName:'util/errorRow'
+});

+ 5 - 5
contrib/views/files/src/main/resources/ui/bower.json

@@ -3,10 +3,10 @@
   "version": "0.0.1",
   "main": "public/app.js",
   "dependencies": {
-    "ember": "1.6.0-beta.1",
-    "ember-data": "1.0.0-beta.7",
+    "ember": "1.7.0",
+    "ember-data": "1.0.0-beta.9",
     "jquery": "1.9.0",
-    "bootstrap": "3.0.x",
+    "bootstrap": "3.1.x",
     "ember-uploader": "~0.2.7",
     "ladda-bootstrap": "git://github.com/msurguy/ladda-bootstrap.git#~0.1.0",
     "moment": "~2.5.1",
@@ -27,8 +27,8 @@
     "ember-i18n": {
       "scripts": []
     },
-    "font-awesome":{
-      "main":"css/font-awesome.css"
+    "font-awesome": {
+      "main": "css/font-awesome.css"
     }
   }
 }

+ 190 - 0
contrib/views/files/src/main/resources/ui/vendor/js/bsPopover.js

@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ */
+var template = '' +
+        '<div class="arrow"></div>' +
+        '{{#if title}}' +
+        '    <h3 class="popover-title">{{title}}</h3>' +
+        '{{/if}}' +
+        '<div class="popover-content">' +
+        '{{#if content}}' +
+        '        {{content}}' +
+        '{{else}}' +
+        '{{yield}}' +
+        '{{/if}}' +
+        '    </div>';
+ 
+Ember.TEMPLATES["components/bs-popover"] = Ember.Handlebars.compile(template);
+ 
+Ember.BsPopoverComponent = Ember.Component.extend({
+    classNames: 'popover',
+    classNameBindings:  ['fade', 'in', 'top', 'left', 'right', 'bottom'],
+ 
+    top: function(){
+        return this.get('realPlacement')=='top';
+    }.property('realPlacement'),
+    left: function(){
+        return this.get('realPlacement')=='left';
+    }.property('realPlacement'),
+    right: function(){
+        return this.get('realPlacement')=='right';
+    }.property('realPlacement'),
+    bottom: function(){
+        return this.get('realPlacement')=='bottom';
+    }.property('realPlacement'),
+ 
+    title: '',
+    content: '',
+    html: false,
+    delay: 0,
+    isVisible: false,
+    animation: true,
+    fade: function(){
+        return this.get('animation');
+    }.property('animation'),
+    in: function(){
+        return this.get('isVisible');
+    }.property('isVisible'),
+    triggers: 'hover focus',
+    placement: 'top',
+    onElement: null,
+    $element: null,
+    $tip: null,
+    inserted: false,
+ 
+    styleUpdater: function(){
+        if( !this.$tip || !this.get('isVisible')){
+            return;
+        }
+        this.$tip.css('display','block');
+        var placement = this.get('realPlacement');
+        var pos = this.getPosition();
+        var actualWidth = this.$tip[0].offsetWidth;
+        var actualHeight = this.$tip[0].offsetHeight;
+        var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight);
+ 
+        this.$tip.css('top',calculatedOffset.top);
+        this.$tip.css('left',calculatedOffset.left);
+        if(this.firstTime){
+            this.firstTime = false;
+            this.styleUpdater();
+            this.firstTime = true;
+        }
+    }.observes('content','realPlacement','inserted', 'isVisible'),
+ 
+ 
+    didInsertElement: function(){
+ 
+        this.$tip = this.$();
+        if(this.get('onElement')){
+            this.$element=$('#'+this.get('onElement'));
+        }else if(this.$tip.prev(':not(script)').length){
+            this.$element = this.$tip.prev(':not(script)');
+        }else{
+            this.$element = this.$tip.parent(':not(script)');
+        }
+ 
+ 
+        var triggers = this.triggers.split(' ');
+ 
+        for (var i = triggers.length; i--;) {
+            var trigger = triggers[i];
+ 
+            if (trigger == 'click') {
+                this.$element.on('click',$.proxy(this.toggle, this));
+            } else if (trigger != 'manual') {
+                var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focus';
+                var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur';
+ 
+                this.$element.on(eventIn, $.proxy(this.enter, this));
+                this.$element.on(eventOut, $.proxy(this.leave, this));
+            }
+        }
+        this.set('inserted',true);
+    },
+ 
+ 
+    toggle: function(){
+        this.toggleProperty('isVisible');
+    },
+ 
+    enter: function(){
+        this.set('isVisible',true);
+    },
+ 
+    leave: function(){
+        this.set('isVisible',false);
+    },
+ 
+    afterRender: function(){
+        this.notifyPropertyChange('content');
+    },
+ 
+ 
+    realPlacement: function(){
+ 
+        if(!this.$tip) return null;
+        var placement = this.get('placement') || '';
+        var autoToken = /\s?auto?\s?/i;
+        var autoPlace = autoToken.test(placement);
+        if (autoPlace)
+            placement = placement.replace(autoToken, '') || 'top';
+ 
+        var pos = this.getPosition();
+        var actualWidth = this.$tip[0].offsetWidth;
+        var actualHeight = this.$tip[0].offsetHeight;
+ 
+        if (autoPlace) {
+            var $parent = this.$element.parent();
+ 
+            var orgPlacement = placement;
+            var docScroll = document.documentElement.scrollTop || document.body.scrollTop;
+            var parentWidth = $parent.outerWidth();
+            var parentHeight = $parent.outerHeight();
+            var parentLeft = $parent.offset().left;
+ 
+            placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
+                    placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
+                            placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
+                                    placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
+                                            placement;
+        }
+        return placement;
+ 
+    }.property('placement','inserted'),
+ 
+ 
+    hasContent: function () {
+        return this.get('title');
+    },
+ 
+    getPosition: function () {
+        var el = this.$element[0];
+        return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
+            width: el.offsetWidth, height: el.offsetHeight
+        }, this.$element.offset());
+    },
+ 
+ 
+    getCalculatedOffset: function (placement, pos, actualWidth, actualHeight) {
+        return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2  } :
+                placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
+                        placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
+                            /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
+    }
+ 
+});

+ 1 - 1
contrib/views/files/src/main/resources/view.xml

@@ -21,7 +21,7 @@
 
     <parameter>
         <name>dataworker.defaultFs</name>
-        <description>FileSystem URI</description>
+        <description>The FileSystem URI (for example, hdfs://c6401.ambari.apache.org:8020)</description>
         <required>true</required>
     </parameter>
     <parameter>