Эх сурвалжийг харах

HUE-776 [fb] Support bulk operations

Filebrowser now uses Knockout.js to render
Fixed tests
Fixed upload button
Removed console.log
Removed condensed class from the table
Clicking on a file now shows it
Put app title back on the page
Removed Edit and Download buttons
Split breadcrumbs and action bar
Moved action buttons to the left and added text to them
Added support for window.location.hash
The action bar now sticks to the top on window scroll
Added filter on keyup
The breadcrumb path on the filebrowser is now editable
Removed search button and enlarged search input field
Added client side error notification in case of HTTP 500
Introduced support for browser history
Added tooltip and pointer cursor to the edit path icon
Enrico Berti 13 жил өмнө
parent
commit
25505a8898

+ 66 - 32
apps/filebrowser/src/filebrowser/templates/fb_components.mako

@@ -19,37 +19,71 @@ from django.utils.translation import ugettext as _
 %>
 
 <%def name="breadcrumbs(path, breadcrumbs, from_listdir=False)">
-    <%
-    current_filter = ""
-    if filter_str is not None:
-        current_filter = filter_str
-    %>
-    <div class="subnav">
-        % if from_listdir:
-        <p class="pull-right">
-            <a href="#" class="btn upload-link">${_('Upload files')}</a>
-            <a href="#" class="btn create-directory-link">${_('New directory')}</a>
-            &nbsp;&nbsp;&nbsp;&nbsp;
-            <input type="text" value="${current_filter}" class="input-medium search-query span4" placeholder="${_('Search for file name')}">
-            <a href="#" class="btn filter">${_('Search')}</a>
-        </p>
-        % endif
-        <ul class="nav nav-pills">
-          <li><a href="${url('filebrowser.views.view', path=urlencode(path))}?default_to_home"><i class="icon-home"></i> ${_('Home')}</a></li>
-          <li>
-            <ul class="hueBreadcrumb">
-                % for breadcrumb_item in breadcrumbs:
-                    <% label = breadcrumb_item['label'] %>
-                    %if label == '/':
-                        <li><a href="/filebrowser/view${breadcrumb_item['url']}"><span
-                            class="divider">${label | h}<span></a></li>
-                    %else:
-                        <li><a href="/filebrowser/view${breadcrumb_item['url']}">${label | h}</a><span class="divider">/</span></li>
-                    %endif
-                % endfor
-            </ul>
-          </li>
+    % if from_listdir:
+        <div class="subnavContainer">
+            <div class="subnav">
+                <p class="pull-right">
+                    <input type="text" class="input-xlarge search-query" placeholder="${_('Search for file name')}" data-bind="value: searchQuery">
+                </p>
+                <p style="padding: 4px">
+                    <button class="btn fileToolbarBtn" title="${_('Rename')}" data-bind="click: renameFile, enable: selectedFiles().length == 1"><i class="icon-font"></i> ${_('Rename')}</button>
+                    <button class="btn fileToolbarBtn" title="${_('Move')}" data-bind="click: move, enable: selectedFiles().length == 1"><i class="icon-random"></i> ${_('Move')}</button>
+                    <button class="btn fileToolbarBtn" title="${_('Change Owner / Group')}" data-bind="click: changeOwner, enable: selectedFiles().length == 1"><i class="icon-user"></i> ${_('Change Owner / Group')}</button>
+                    <button class="btn fileToolbarBtn" title="${_('Change Permissions')}" data-bind="click: changePermissions, enable: selectedFiles().length == 1"><i class="icon-list-alt"></i> ${_('Change Permissions')}</button>
+                    <button class="btn fileToolbarBtn" title="${_('Delete')}" data-bind="click: deleteSelected, enable: selectedFiles().length == 1"><i class="icon-trash"></i> ${_('Delete')}</button>
+                    &nbsp;&nbsp;&nbsp;&nbsp;
+                    <a href="#" class="btn upload-link" title="${_('Upload files')}"><i class="icon-upload"></i> ${_('Upload files')}</a>
+                    <a href="#" class="btn create-directory-link" title="${_('New directory')}"><i class="icon-folder-close"></i> ${_('New directory')}</a>
+                    &nbsp;&nbsp;&nbsp;&nbsp;
+                </p>
+            </div>
+        </div>
+        <br/>
+        <ul class="nav nav-pills hueBreadcrumbBar">
+            <li><a href="${url('filebrowser.views.view', path=urlencode(path))}?default_to_home"><i class="icon-home"></i> ${_('Home')}</a></li>
+            <li>
+                <span style="float:right; margin-top:10px;"><i id="editBreadcrumb" class="icon-pencil hand" rel="tooltip" title="${_('Edit path')}"></i></span>
+                <ul class="hueBreadcrumb" data-bind="foreach: breadcrumbs" style="padding-right:40px">
+                    <li data-bind="visible: label == '/'"><a href="#" data-bind="click: show"><span class="divider" data-bind="text: label"></span></a></li>
+                    <li data-bind="visible: label != '/'"><a href="#" data-bind="text: label, click: show"></a><span class="divider">/</span></li>
+                </ul>
+                <input id="hueBreadcrumbText" type="text" class="input-xlarge" style="margin-top:4px;margin-right:4px;display:none" data-bind="value: currentPath" />
+            </li>
         </ul>
-    </div>
-    <br/>
+    % else:
+        <ul class="nav nav-pills hueBreadcrumbBar">
+            <li><a href="${url('filebrowser.views.view', path=urlencode(path))}?default_to_home"><i class="icon-home"></i> ${_('Home')}</a></li>
+            <li>
+                <ul class="hueBreadcrumb">
+                        % for breadcrumb_item in breadcrumbs:
+                        <% label = breadcrumb_item['label'] %>
+                        %if label == '/':
+                                <li><a href="/filebrowser/view${breadcrumb_item['url']}"><span
+                                        class="divider">${label | h}<span></a></li>
+                        %else:
+                                <li><a href="/filebrowser/view${breadcrumb_item['url']}">${label | h}</a><span class="divider">/</span></li>
+                        %endif
+                        % endfor
+                </ul>
+            </li>
+        </ul>
+    % endif
+
+    <style type="text/css">
+        .subnavContainer {
+            height: 36px;
+        }
+        .hueBreadcrumbBar {
+            padding: 8px 15px;
+            margin: 0 0 20px;
+            list-style: none;
+            border: 1px solid #E5E5E5;
+            -webkit-border-radius: 4px;
+            -moz-border-radius: 4px;
+            border-radius: 4px;
+        }
+        .hueBreadcrumb {
+            margin: 0!important;
+        }
+    </style>
 </%def>

+ 539 - 412
apps/filebrowser/src/filebrowser/templates/listdir_components.mako

@@ -22,67 +22,27 @@ from django.utils.encoding import smart_str
 from django.utils.translation import ugettext as _
 %>
 
-
-<%def name="pageref(num)">
-    <%
-    sortby_param = ""
-    if sortby:
-        sortby_param = "&sortby="+sortby
-
-    descending_param = ""
-    if descending:
-        descending_param = "&descending=true"
-
-    filter_param = ""
-    if filter_str is not None:
-        filter_param = "&filter="+filter_str
-    %>
-    href="?pagenum=${num}&pagesize=${pagesize}${sortby_param}${descending_param}${filter_param}"
-</%def>
-
-<%def name="prevpage(page)">
-  ${pageref(page.previous_page_number())}
-</%def>
-
-<%def name="nextpage(page)">
-  ${pageref(page.next_page_number())}
-</%def>
-
-<%def name="toppage(page)">
-  ${pageref(1)}
-</%def>
-
-<%def name="bottompage(page)">
-  ${pageref(page.num_pages())}
-</%def>
-
-<%def name="pagination(localpage)">
-    <div class="pagination">
-        <ul class="pull-right">
-            % if page.number > 1:
-                <li class="prev"><a title="${_('Beginning of List')}" ${toppage(page)}>&larr; ${_('Beginning of List')}</a></li>
-                <li><a title="${_('Previous Page')}" ${prevpage(page)}>${_('Previous Page')}</a></li>
-            % endif
-            % if page.number < page.num_pages():
-                <li><a title="${_('Next page')}" ${nextpage(page)}>${_('Next Page')}</a></li>
-                <li class="next"><a title="${_('End of List')}" ${bottompage(page)}>${_('End of List')} &rarr;</a></li>
-            % endif
-        </ul>
-        <p>${_('Show')} <select id="pagesize" class="input-mini"><option>15</option><option selected>30</option><option>45</option><option>60</option><option>100</option><option>200</option></select> ${_('items per page')}. ${_('Showing %(start_index)s to %(end_index)s of %(total_count)s items, page %(page)s of %(num_pages)s.') % dict(start_index=page.start_index(), end_index=page.end_index(), total_count=page.total_count(), page=page.number, num_pages=page.num_pages())}</p>
-    </div>
-</%def>
-
 <%def name="list_table_chooser(files, path, current_request_path)">
   ${_table(files, path, current_request_path, 'chooser')}
 </%def>
 
 <%def name="list_table_browser(files, path, current_request_path, cwd_set=True)">
-  ${_table(files, path, current_request_path, 'view', cwd_set)}
+  ${_table(files, path, current_request_path, 'view')}
 </%def>
 
-<%def name="_table(files, path, current_request_path, view, cwd_set=False)">
+<%def name="_table(files, path, current_request_path, view)">
+    <script src="/static/ext/js/knockout-2.0.0.js" type="text/javascript" charset="utf-8"></script>
+    <script src="/static/ext/js/moment.min.js" type="text/javascript" charset="utf-8"></script>
     <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
     <style type="text/css">
+        .fixed {
+            position: fixed;
+            top: 40px;
+            filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0);
+            -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+            -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+            box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065);
+        }
         .pull-right {
             margin: 4px;
         }
@@ -92,110 +52,74 @@ from django.utils.translation import ugettext as _
         .file-row {
             height:37px;
         }
-        .parent {
-            display: none;
-        }
     </style>
 
-    %if len(files) > 0:
-
-    <table class="table table-condensed table-striped datatables">
+    <table class="table table-striped datatables" data-bind="">
         <thead>
             <tr>
-                <th class="sortable sorting" data-sort="type" width="5%">${_('Type')}</th>
-            % if cwd_set:
-                <th class="sortable sorting" data-sort="name">${_('Name')}</th>
-            % else:
-                <th class="sortable sorting" data-sort="name">${_('Path')}</th>
-            % endif
-                <th class="sortable sorting" data-sort="size" width="10%">${_('Size')}</th>
-                <th class="sortable sorting" data-sort="user" width="10%">${_('User')}</th>
-                <th class="sortable sorting" data-sort="group" width="10%">${_('Group')}</th>
+                <th width="1%"><input id="selectAll" type="checkbox" data-bind="click: selectAll, checked: allSelected"/></th>
+                <th class="sortable sorting" data-sort="type" width="4%" data-bind="click: sort">Type</th>
+                <th class="sortable sorting" data-sort="name" data-bind="click: sort">${_('Name')}</th>
+                <th class="sortable sorting" data-sort="size" width="10%" data-bind="click: sort">${_('Size')}</th>
+                <th class="sortable sorting" data-sort="user" width="10%" data-bind="click: sort">${_('User')}</th>
+                <th class="sortable sorting" data-sort="group" width="10%" data-bind="click: sort">${_('Group')}</th>
                 <th width="10%">${_('Permissions')}</th>
-                <th class="sortable sorting" data-sort="mtime" width="15%">${_('Date')}</th>
-                <th width="100">&nbsp;</th>
+                <th class="sortable sorting" data-sort="mtime" width="15%" data-bind="click: sort">${_('Date')}</th>
             </tr>
         </thead>
-        <tbody id="files" class="hide">
-            % for file in files:
-            <%
-              icon = 'icon-file'
-              if file['type'] == 'dir':
-                icon = 'icon-folder-close'
-              endif
-
-              if cwd_set:
-                display_name = file['name']
-              else:
-                display_name = file['path']
-              endif
-
-              row_class = 'file-row'
-              if '..' == display_name:
-                row_class += ' parent'
-              endif
-            %>
-
-            <% path = file['path'] %>
-            <tr class="${row_class}" data-search="${display_name}">
-                <td class="left"><i class="${icon}"></i></td>
-                <td>
-                    <h5><a href="${url('filebrowser.views.'+view, path=urlencode(path))}?file_filter=${file_filter}" data-row-selector="true">${display_name}</a></h5>
-                </td>
-                <td>
-                    % if "dir" == file['type']:
-                    <span></span>
-                    % else:
-                    <span>${file['stats']['size']|filesizeformat}</span>
-                    % endif
-                </td>
-                <td>${file['stats']['user']}</td>
-                <td>${file['stats']['group']}</td>
-                <td>${file['rwx']}</td>
-                <td>${date(datetime.datetime.fromtimestamp(file['stats']['mtime']))} ${time(datetime.datetime.fromtimestamp(file['stats']['mtime'])).replace("p.m.","PM").replace("a.m.","AM")}</td>
-                <td data-row-selector-exclude="true">
-                    % if ".." != file['name']:
-                    <%
-                    path_digest = urlencode(md5.md5(smart_str(path)).hexdigest())
-                    %>
-                    <div class="btn-group" data-row-selector-exclude="true">
-                        <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
-                            ${_('Operations')}
-                            <span class="caret"></span>
-                        </a>
-                        <ul class="dropdown-menu pull-right">
-                            % if "file" == file['type']:
-                            <li><a href="${url('filebrowser.views.view', path=urlencode(path))}">${_('View File')}</a></li>
-                            <li><a href="${url('filebrowser.views.edit', path=urlencode(path))}">${_('Edit File')}</a></li>
-                            <li><a href="${url('filebrowser.views.download', path=urlencode(path))}" target="_blank">${_('Download File')}</a></li>
-                            % endif
-                            <li><a class="rename" file-to-rename="${path}">${_('Rename')}</a></li>
-                            <li><a onclick="openChownWindow('${path}','${file['stats']['user']}','${file['stats']['group']}','${current_request_path}')">${_('Change Owner / Group')}</a></li>
-                            <li><a onclick="openChmodWindow('${path}','${stringformat(file['stats']['mode'], "o")}','${current_request_path}')">${_('Change Permissions')}</a></li>
-                            <li><a onclick="openMoveModal('${path}','${stringformat(file['stats']['mode'], "o")}', '${current_request_path}')">${_('Move')}</a></li>
-                            % if "dir" == file['type']:
-                            <li><a class="delete" delete-type="rmdir" file-to-delete="${path}" data-keyboard="true">${_('Delete')}</a></li>
-                            <li><a class="delete" delete-type="rmtree" file-to-delete="${path}" data-keyboard="true">${_('Delete Recursively')}</a></li>
-                            % else:
-                            <li><a class="delete" delete-type="remove" file-to-delete="${path}" data-keyboard="true">${_('Delete')}</a></li>
-                            % endif
-                        </ul>
+        <tbody id="files" data-bind="template: {name: 'fileTemplate', foreach: files}">
+
+        </tbody>
+        <tfoot>
+        <tr data-bind="visible: isLoading()">
+            <td colspan="8" class="left">
+                <img src="/static/art/spinner.gif" />
+            </td>
+        </tr>
+            <tr data-bind="visible: files().length == 0 && !isLoading()">
+                <td colspan="8">
+                    <div class="alert">
+                        There are no files matching the search criteria.
                     </div>
-                    % endif
                 </td>
             </tr>
-            % endfor
-        </tbody>
+        </tfoot>
     </table>
 
-    ${pagination(page)}
-    %else:
-        <div class="alert">
-            ${_('There are no files matching the search criteria.')}
-        </div>
-    %endif
+    <script id="fileTemplate" type="text/html">
+        <tr style="cursor: pointer">
+            <td class="center">
+                <input data-bind="value: path, checked: selected, visible: name != '..'" type="checkbox" data-row-selector-exclude="true"  />
+            </td>
+            <td data-bind="click: $root.viewFile" class="left"><i data-bind="css: {'icon-file': type == 'file', 'icon-folder-close': type != 'file'}"></i></td>
+            <td data-bind="click: $root.viewFile">
+                <h5><a href="#" data-bind="click: $root.viewFile, text: name"></a></h5>
+            </td>
+            <td data-bind="click: $root.viewFile">
+                <span data-bind="visible: type=='file', text: stats.size"></span>
+            </td>
+            <td data-bind="click: $root.viewFile, text: stats.user"></td>
+            <td data-bind="click: $root.viewFile, text: stats.group"></td>
+            <td data-bind="click: $root.viewFile, text: permissions"></td>
+            <td data-bind="click: $root.viewFile, text: stats.mtime"></td>
+        </tr>
+    </script>
+
+
+    <div class="pagination" data-bind="visible: !isLoading()">
+        <ul class="pull-right">
+            <li class="prev" data-bind="visible: page().number > 1"><a href="#" data-bind="click: firstPage" title="${_('Beginning of List')}">&larr; ${_('Beginning of List')}</a></li>
+            <li data-bind="visible: page().number > 1"><a href="#" data-bind="click: previousPage" title="${_('Previous Page')}">${_('Previous Page')}</a></li>
+            <li data-bind="visible: page().number < page().num_pages"><a href="#" data-bind="click: nextPage" title="${_('Next page')}">${_('Next Page')}</a></li>
+            <li class="next" data-bind="visible: page().number < page().num_pages"><a href="#" data-bind="click: lastPage" title="${_('End of List')}">${_('End of List')} &rarr;</a></li>
+        </ul>
+        <p>${_('Show')}
+            <select class="input-mini" data-bind="options: recordsPerPageChoices, value: recordsPerPage"></select>
+        ${_('items per page')}.
+        ${_('Showing')} <span data-bind="text: page().start_index"></span> ${_('to')} <span data-bind="text: page().end_index"></span> ${_('of')} <span data-bind="text: page().total_count"></span> ${_('items, page')}
+            <span data-bind="text: page().number"></span> ${_('of')} <span data-bind="text: page().num_pages"></span></p>
+    </div>
 
-%if len(files) > 0:
     <!-- delete modal -->
     <div id="deleteModal" class="modal hide fade">
         <div class="modal-header">
@@ -241,324 +165,527 @@ from django.utils.translation import ugettext as _
     <div id="changePermissionModal" class="modal hide fade"></div>
 
     <div id="moveModal" class="modal hide fade"></div>
-%endif
 
-<!-- upload modal -->
-<div id="uploadModal" class="modal hide fade">
-    <div class="modal-header">
-        <a href="#" class="close" data-dismiss="modal">&times;</a>
-        <h3>${_('Uploading to:')} <span id="uploadDirName">${current_dir_path}</span></h3>
-    </div>
-    <div class="modal-body">
-        <form action="/filebrowser/upload?next=${current_dir_path}" method="POST" enctype="multipart/form-data" class="form-stacked">
+    <!-- upload modal -->
+    <div id="uploadModal" class="modal hide fade">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>${_('Uploading to:')} <span id="uploadDirName" data-bind="text: currentPath"></span></h3>
+        </div>
+        <div class="modal-body">
             <div id="fileUploader">
             <noscript>
                 <p>${_('Please enable JavaScript to use the file uploader.')}</p>
             </noscript>
             </div>
-        </form>
-    </div>
-    <div class="modal-footer"></div>
-</div>
-
-<!-- new directory modal -->
-<div id="createDirectoryModal" class="modal hide fade">
-    <form id="createDirectoryForm" action="/filebrowser/mkdir?next=${current_request_path}" method="POST" enctype="multipart/form-data" class="form-inline form-padding-fix">
-    <div class="modal-header">
-        <a href="#" class="close" data-dismiss="modal">&times;</a>
-        <h3>${_('Create Directory')}</h3>
-    </div>
-    <div class="modal-body">
-        <label>${_('Directory Name')} <input id="newDirectoryNameInput" name="name" value="" type="text" class="input-xlarge"/></label>
-        <input type="hidden" name="path" type="text" value="${current_dir_path}"/>
+        </div>
+        <div class="modal-footer"></div>
     </div>
-    <div class="modal-footer">
-         <div id="directoryNameRequiredAlert" class="alert-message error hide" style="position: absolute; left: 10;">
-            <p><strong>${_('Sorry, directory name is required.')}</strong>
+
+    <!-- new directory modal -->
+    <div id="createDirectoryModal" class="modal hide fade">
+        <form id="createDirectoryForm" data-bind="submit: createDirectory" method="POST" enctype="multipart/form-data" class="form-inline form-padding-fix">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>${_('Create Directory')}</h3>
+        </div>
+        <div class="modal-body">
+            <label>${_('Directory Name')} <input id="newDirectoryNameInput" name="name" value="" type="text" class="input-xlarge"/></label>
+            <input type="hidden" name="path" type="text" data-bind="value: currentPath"/>
+        </div>
+        <div class="modal-footer">
+             <div id="directoryNameRequiredAlert" class="alert-message error hide" style="position: absolute; left: 10;">
+                <p><strong>${_('Sorry, directory name is required.')}</strong>
+            </div>
+            <a id="cancelCreateDirectoryBtn" class="btn" href="#">${_('Cancel')}</a>
+            <input class="btn primary" type="submit" value="${_('Submit')}" />
         </div>
-        <a id="cancelCreateDirectoryBtn" class="btn" href="#">${_('Cancel')}</a>
-        <input class="btn primary" type="submit" value="${_('Submit')}" />
+        </form>
     </div>
-    </form>
-</div>
-
-<script type="text/javascript" charset="utf-8">
-    %if len(files) > 0 :
-    // ajax modal windows
-    function openChownWindow(path, user, group, next){
-        $.ajax({
-            url: "/filebrowser/chown",
-            data: {"path":path, "user":user, "group" : group, "next" : next},
-            beforeSend: function(xhr){
-                xhr.setRequestHeader("X-Requested-With", "Hue");
-            },
-            dataType: "html",
-            success: function(data){
-                $("#changeOwnerModal").html(data);
-                $("#changeOwnerModal").modal({
-                    keyboard: true,
-                    show: true
-                });
-            }
-        });
-    }
-
-    function openChmodWindow(path, mode, next){
-        $.ajax({
-            url: "/filebrowser/chmod",
-            data: {"path":path, "mode":mode, "next" : next},
-            beforeSend: function(xhr){
-                xhr.setRequestHeader("X-Requested-With", "Hue");
-            },
-            dataType: "html",
-            success: function(data){
-                $("#changePermissionModal").html(data);
-                $("#changePermissionModal").modal({
-                    keyboard: true,
-                    show: true
-                });
-            }
-        });
-    }
-
-    function openMoveModal(src_path, mode, next){
-        $.ajax({
-            url: "/filebrowser/move",
-            data: {"src_path":src_path, "mode":mode, "next" : next},
-            beforeSend: function(xhr){
-                xhr.setRequestHeader("X-Requested-With", "Hue");
-            },
-            dataType: "html",
-            success: function(data){
-                $("#moveModal").html(data);
-                $("#moveModal").modal({
-                    keyboard: true,
-                    show: true
-                });
-            }
-        });
-    }
-    %endif
-
-
-    //uploader
-    var num_of_pending_uploads = 0;
-    function createUploader(){
-        var uploader = new qq.FileUploader({
-            element: document.getElementById("fileUploader"),
-            action: "/filebrowser/upload",
-            template: '<div class="qq-uploader">' +
-                    '<div class="qq-upload-drop-area"><span>${_('Drop files here to upload')}</span></div>' +
-                    '<div class="qq-upload-button">${_('Upload a file')}</div>' +
-                    '<ul class="qq-upload-list"></ul>' +
-                    '</div>',
-            fileTemplate: '<li>' +
-                    '<span class="qq-upload-file"></span>' +
-                    '<span class="qq-upload-spinner"></span>' +
-                    '<span class="qq-upload-size"></span>' +
-                    '<a class="qq-upload-cancel" href="#">${_('Cancel')}</a>' +
-                    '<span class="qq-upload-failed-text">${_('Failed')}</span>' +
-                    '</li>',
-            params:{
-                dest: "${current_dir_path}",
-                fileFieldLabel: "hdfs_file"
-            },
-            onComplete:function(id, fileName, responseJSON){
-                num_of_pending_uploads--;
-                if(num_of_pending_uploads == 0){
-                    window.location = "/filebrowser/view${current_dir_path}";
+
+    <script type="text/javascript" charset="utf-8">
+        // ajax modal windows
+        function openChownWindow(path, user, group, next){
+            $.ajax({
+                url: "/filebrowser/chown",
+                data: {"path":path, "user":user, "group" : group, "next" : next},
+                beforeSend: function(xhr){
+                    xhr.setRequestHeader("X-Requested-With", "Hue");
+                },
+                dataType: "html",
+                success: function(data){
+                    $("#changeOwnerModal").html(data);
+                    $("#changeOwnerModal").modal({
+                        keyboard: true,
+                        show: true
+                    });
                 }
-            },
-            onSubmit:function(id, fileName, responseJSON){
-                num_of_pending_uploads++;
-            },
-            debug: false
-        });
-    }
-
-    // in your app create uploader as soon as the DOM is ready
-    // don"t wait for the window to load
-    window.onload = createUploader;
-
-    function getQueryString(){
-        var queryString = {};
-        if (window.location.href.indexOf("?") > -1) {
-            window.location.href.split("?").pop().split("&").forEach(function (prop) {
-                var item = prop.split("=");
-                queryString[item.shift()] = item.shift();
             });
         }
-        return queryString;
-    }
 
-    function setQueryString(queryString){
-        var qs = "?"
-        for (var key in queryString) {
-            qs += key + "=" + queryString[key] + "&";
+        function openChmodWindow(path, mode, next){
+            $.ajax({
+                url: "/filebrowser/chmod",
+                data: {"path":path, "mode":mode, "next" : next},
+                beforeSend: function(xhr){
+                    xhr.setRequestHeader("X-Requested-With", "Hue");
+                },
+                dataType: "html",
+                success: function(data){
+                    $("#changePermissionModal").html(data);
+                    $("#changePermissionModal").modal({
+                        keyboard: true,
+                        show: true
+                    });
+                }
+            });
         }
-        return qs.substring(0, qs.length - 1);
-    }
-
-    $(document).ready(function(){
-        var qs = getQueryString();
 
-    %if len(files) > 0:
-        if (qs["sortby"] == null){
-            qs["sortby"] = "name";
+        function openMoveModal(src_path, mode, next){
+            $.ajax({
+                url: "/filebrowser/move",
+                data: {"src_path":src_path, "mode":mode, "next" : next},
+                beforeSend: function(xhr){
+                    xhr.setRequestHeader("X-Requested-With", "Hue");
+                },
+                dataType: "html",
+                success: function(data){
+                    $("#moveModal").html(data);
+                    $("#moveModal").modal({
+                        keyboard: true,
+                        show: true
+                    });
+                }
+            });
         }
 
-        var el = $(".sortable[data-sort=" + qs["sortby"] + "]");
-        el.removeClass("sorting");
-        if (qs["descending"] != null && qs["descending"] == "true") {
-            el.addClass("sorting_desc");
-        }
-        else {
-            el.addClass("sorting_asc");
+        //uploader
+        var num_of_pending_uploads = 0;
+        var uploader = null;
+        function createUploader(){
+            uploader = new qq.FileUploader({
+                element: document.getElementById("fileUploader"),
+                action: "/filebrowser/upload",
+                template: '<div class="qq-uploader">' +
+                        '<div class="qq-upload-drop-area"><span>${_('Drop files here to upload')}</span></div>' +
+                        '<div class="qq-upload-button">${_('Upload a file')}</div>' +
+                        '<ul class="qq-upload-list"></ul>' +
+                        '</div>',
+                fileTemplate: '<li>' +
+                        '<span class="qq-upload-file"></span>' +
+                        '<span class="qq-upload-spinner"></span>' +
+                        '<span class="qq-upload-size"></span>' +
+                        '<a class="qq-upload-cancel" href="#">${_('Cancel')}</a>' +
+                        '<span class="qq-upload-failed-text">${_('Failed')}</span>' +
+                        '</li>',
+                params:{
+                    dest: viewModel.currentPath(),
+                    fileFieldLabel: "hdfs_file"
+                },
+                onComplete:function(id, fileName, responseJSON){
+                    num_of_pending_uploads--;
+                    if(num_of_pending_uploads == 0){
+                        window.location = "/filebrowser/view" + viewModel.currentPath();
+                    }
+                },
+                onSubmit:function(id, fileName, responseJSON){
+                    num_of_pending_uploads++;
+                },
+                debug: false
+            });
         }
 
-
-        $(".sortable").click(function(){
-            qs["sortby"] = $(this).data("sort");
-            if ($(this).hasClass("sorting_asc")) {
-                qs["descending"] = "true";
+        $(document).scroll(function(){
+            var el = $(".subnav");
+            if (!el.data("top")) {
+                if (el.hasClass("fixed")){
+                    return;
+                }
+                var offset = el.offset()
+                el.data("top", offset.top).data("width", el.width());
+            }
+            if (el.data("top") - el.outerHeight() <= $(this).scrollTop()+1){
+                el.width(el.data("width")).addClass("fixed");
             }
             else {
-                delete qs["descending"];
+                el.width("100%").data("width", el.width()).removeClass("fixed");
             }
-            location.href = setQueryString(qs);
         });
 
-        $("#pagesize").val("${pagesize}");
-        $("#pagesize").change(function(){
-            qs["pagenum"] = "1";
-            qs["pagesize"] = $("#pagesize").val();
-            location.href = setQueryString(qs);
-        });
+        $(document).ready(function(){
+
+            createUploader();
 
-        //delete handlers
-        $(".delete").live("click", function(e){
-            $("#fileToDeleteInput").attr("value", $(e.target).attr("file-to-delete"));
-            $("#deleteForm").attr("action", "/filebrowser/" + $(e.target).attr("delete-type") + "?next=" + encodeURI("${current_request_path}") + "&path=" + encodeURI("${path}"));
-            $("#deleteModal").modal({
-                keyboard: true,
-                show: true
+            $("#cancelDeleteBtn").click(function(){
+                $("#deleteModal").modal("hide");
             });
-        });
 
-        $("#cancelDeleteBtn").click(function(){
-            $("#deleteModal").modal("hide");
-        });
+            $("#cancelRenameBtn").click(function(){
+                $("#renameModal").modal("hide");
+            });
 
-        //rename handlers
-        $(".rename").live("click",function(eventObject){
-            $("#renameSrcPath").attr("value", $(eventObject.target).attr("file-to-rename"));
-            $("#renameFileName").text($(eventObject.target).attr("file-to-rename"));
-            $("#newNameInput").val($(eventObject.target).attr("file-to-rename"));
-            $("#renameModal").modal({
-                keyboard: true,
-                show: true
+            $("#renameForm").submit(function(){
+                if($("#newNameInput").val() == ""){
+                    $("#renameNameRequiredAlert").show();
+                    $("#newNameInput").addClass("fieldError");
+                    return false;
+                }
             });
-        });
 
-        $("#cancelRenameBtn").click(function(){
-            $("#renameModal").modal("hide");
-        });
+            $("#newNameInput").focus(function(){
+                $("#renameNameRequiredAlert").hide();
+                $("#newNameInput").removeClass("fieldError");
+            });
 
-        $("#renameForm").submit(function(){
-            if($("#newNameInput").val() == ""){
-                $("#renameNameRequiredAlert").show();
-                $("#newNameInput").addClass("fieldError");
-                return false;
-            }
-        });
+            $("#moveForm").live("submit", function(){
+                if ($.trim($("#moveForm").find("input[name='dest_path']").val()) == ""){
+                    $("#moveNameRequiredAlert").show();
+                    $("#moveForm").find("input[name='dest_path']").addClass("fieldError");
+                    return false;
+                }
+                return true;
+            });
 
-        $("#newNameInput").focus(function(){
-            $("#renameNameRequiredAlert").hide();
-            $("#newNameInput").removeClass("fieldError");
-        });
+            $("#moveForm").find("input[name='dest_path']").live("focus", function(){
+                $("#moveNameRequiredAlert").hide();
+                $("#moveForm").find("input[name='dest_path']").removeClass("fieldError");
+            });
 
-        $("#moveForm").live("submit", function(){
-            if ($.trim($("#moveForm").find("input[name='dest_path']").val()) == ""){
-                $("#moveNameRequiredAlert").show();
-                $("#moveForm").find("input[name='dest_path']").addClass("fieldError");
-                return false;
-            }
-            return true;
-        });
+            //upload handlers
+            $(".upload-link").click(function(){
+                $("#uploadModal").modal({
+                    keyboard: true,
+                    show: true
+                });
+            });
 
-        $("#moveForm").find("input[name='dest_path']").live("focus", function(){
-            $("#moveNameRequiredAlert").hide();
-            $("#moveForm").find("input[name='dest_path']").removeClass("fieldError");
-        });
+            //create directory handlers
+            $(".create-directory-link").click(function(){
+                $("#createDirectoryModal").modal({
+                    keyboard: true,
+                    show: true
+                });
+            });
 
-        $(".parent").prependTo("#files").removeClass("parent");
-        $("#files").removeClass("hide");
-        $("a[data-row-selector='true']").jHueRowSelector();
-    %endif
+            $("#cancelCreateDirectoryBtn").click(function(){
+                $("#createDirectoryModal").modal("hide");
+            });
 
-        //upload handlers
-        $(".upload-link").click(function(){
-            $("#uploadModal").modal({
-                keyboard: true,
-                show: true
+            $("#createDirectoryForm").submit(function(){
+                if ($.trim($("#newDirectoryNameInput").val())==""){
+                    $("#directoryNameRequiredAlert").show();
+                    $("#newDirectoryNameInput").addClass("fieldError");
+                    return false;
+                }
+                return true;
             });
-        });
 
-        //create directory handlers
-        $(".create-directory-link").click(function(){
-            $("#createDirectoryModal").modal({
-                keyboard: true,
-                show: true
+            $("#newDirectoryNameInput").focus(function(){
+                $("#newDirectoryNameInput").removeClass("fieldError");
+                $("#directoryNameRequiredAlert").hide();
             });
-        });
 
-        // handle search
-        $(".search-query").keydown(function(e){
-            if (e.keyCode == 13){
-                $(".filter").click();
+            $(".pathChooser").click(function(){
+                var self = this;
+                $("#fileChooserRename").jHueFileChooser({
+                    initialPath: $(self).val(),
+                    onFileChoose: function(filePath) {
+                        $(self).val(filePath);
+                    },
+                    onFolderChange: function(folderPath){
+                        $(self).val(folderPath);
+                    },
+                    createFolder: false,
+                    uploadFile: false
+                });
+                $("#fileChooserRename").slideDown();
+            });
+
+            $("*[rel='tooltip']").tooltip({ placement: "bottom" });
+            if (window.location.hash != null && window.location.hash.length > 1){
+                viewModel.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + window.location.hash.substring(2));
             }
-        });
+            viewModel.retrieveData();
+
+            var filterTimeout = -1;
+            $(".search-query").keyup(function(){
+                window.clearTimeout(filterTimeout);
+                filterTimeout = window.setTimeout(function(){
+                    viewModel.searchQuery($(".search-query").val());
+                    viewModel.filter();
+                }, 500);
+            });
+
+            $("#editBreadcrumb").click(function(){
+                $(this).hide();
+                $(".hueBreadcrumb").hide();
+                $("#hueBreadcrumbText").show().focus();
+            });
+
+            $("#hueBreadcrumbText").keyup(function(e){
+                if (e.keyCode == 13){
+                    viewModel.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + $(this).val().substring(1));
+                    viewModel.retrieveData();
+                }
+            });
+
+            $("#hueBreadcrumbText").blur(function(){
+                $(this).hide();
+                $(".hueBreadcrumb").show();
+                $("#editBreadcrumb").show();
+            });
+
+            $.ajaxSetup({
+                error: function(x, e) {
+                    if (x.status == 500) {
+                        $.jHueNotify.error("${_('There was a problem with your request.')}");
+                        $("#hueBreadcrumbText").blur();
+                    }
+                }
+            });
+
+            $(window).bind("hashchange", function() {
+                viewModel.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + window.location.hash.substring(2));
+                viewModel.retrieveData();
+            });
 
-        $(".filter").click(function(){
-            qs["filter"] = $(".search-query").val();
-            qs["pagenum"] = "1";
-            location.href = setQueryString(qs);
-        });
 
-        $("#cancelCreateDirectoryBtn").click(function(){
-            $("#createDirectoryModal").modal("hide");
         });
 
-        $("#createDirectoryForm").submit(function(){
-            if ($.trim($("#newDirectoryNameInput").val())==""){
-                $("#directoryNameRequiredAlert").show();
-                $("#newDirectoryNameInput").addClass("fieldError");
-                return false;
+
+        var Page = function (page) {
+            if (page != null) {
+                return {
+                    number:page.number,
+                    num_pages:page.num_pages,
+                    previous_page_number:page.previous_page_number,
+                    next_page_number:page.next_page_number,
+                    start_index:page.start_index,
+                    end_index:page.end_index,
+                    total_count:page.total_count
+                }
             }
-            return true;
-        });
+            return {
+            }
+        }
 
-        $("#newDirectoryNameInput").focus(function(){
-            $("#newDirectoryNameInput").removeClass("fieldError");
-            $("#directoryNameRequiredAlert").hide();
-        });
+        var File = function (file) {
+            return {
+                name:file.name,
+                path:file.path,
+                url:file.url,
+                type:file.type,
+                permissions:file.rwx,
+                mode:file.mode,
+                stats:{
+                    size:file.humansize,
+                    user:file.stats.user,
+                    group:file.stats.group,
+                    mtime:moment.unix(file.stats.mtime).format("MMMM DD, YYYY hh:mm a")
+                },
+                selected:ko.observable(false)
+            }
+        }
+
+        var Breadcrumb = function (breadcrumb) {
+            return {
+                url:breadcrumb.url,
+                label:breadcrumb.label,
+                show:function () {
+                    viewModel.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + this.url);
+                    window.location.hash = this.url;
+                }
+            }
+        }
 
-        $(".pathChooser").click(function(){
+        var FileBrowserModel = function (files, page, breadcrumbs, currentDirPath) {
             var self = this;
-            $("#fileChooserRename").jHueFileChooser({
-                initialPath: $(self).val(),
-                onFileChoose: function(filePath) {
-                    $(self).val(filePath);
-                },
-                onFolderChange: function(folderPath){
-                    $(self).val(folderPath);
-                },
-                createFolder: false,
-                uploadFile: false
+
+            self.page = ko.observable(new Page(page));
+            self.recordsPerPageChoices = ["15", "30", "45", "60", "100", "200"],
+                    self.recordsPerPage = ko.observable("30");
+            self.targetPageNum = ko.observable(1);
+            self.targetPath = ko.observable("${current_request_path}");
+
+            self.sortBy = ko.observable("name");
+            self.sortDescending = ko.observable(false);
+
+            self.searchQuery = ko.observable("");
+
+            self.files = ko.observableArray(ko.utils.arrayMap(files, function (file) {
+                return new File(file);
+            }));
+
+            self.breadcrumbs = ko.observableArray(ko.utils.arrayMap(breadcrumbs, function (breadcrumb) {
+                return new Breadcrumb(breadcrumb);
+            }));
+
+            self.sort = function (viewModel, event) {
+                var el = $(event.currentTarget);
+                el.siblings(".sortable").attr("class", "sortable sorting");
+                self.sortBy(el.data("sort"))
+                el.removeClass("sorting");
+                if (el.hasClass("sorting_asc")) {
+                    self.sortDescending(true);
+                }
+                else {
+                    self.sortDescending(false);
+                }
+                el.attr("class", "sortable");
+                if (self.sortDescending() == true) {
+                    el.addClass("sorting_desc");
+                }
+                else {
+                    el.addClass("sorting_asc");
+                }
+                self.retrieveData();
+            }
+
+            self.isLoading = ko.observable(true);
+
+            self.allSelected = ko.observable(false);
+
+            self.selectedFiles = ko.computed(function () {
+                return ko.utils.arrayFilter(self.files(), function (file) {
+                    return file.selected();
+                });
+            }, self);
+
+            self.selectedFile = ko.computed(function () {
+                return self.selectedFiles()[0];
+            }, self);
+
+            self.currentPath = ko.observable(currentDirPath);
+
+            self.retrieveData = function () {
+                self.isLoading(true);
+                $.getJSON(self.targetPath() + "?pagesize=" + self.recordsPerPage() + "&pagenum=" + self.targetPageNum() + "&filter=" + self.searchQuery() + "&sortby=" + self.sortBy() + "&descending=" + self.sortDescending() + "&format=json", function (data) {
+                    self.updateFileList(data.files, data.page, data.breadcrumbs, data.current_dir_path);
+                    if ($("#hueBreadcrumbText").is(":visible")){
+                        $(".hueBreadcrumb").show();
+                        $("#hueBreadcrumbText").hide();
+                        $("#editBreadcrumb").show();
+                    }
+                });
+            };
+
+            self.updateFileList = function (files, page, breadcrumbs, currentDirPath) {
+                self.page(new Page(page));
+                self.files(ko.utils.arrayMap(files, function (file) {
+                    return new File(file);
+                }));
+                self.breadcrumbs(ko.utils.arrayMap(breadcrumbs, function (breadcrumb) {
+                    return new Breadcrumb(breadcrumb);
+                }));
+                self.currentPath(currentDirPath);
+                if (uploader != null){
+                    uploader.setParams({
+                        dest: self.currentPath(),
+                        fileFieldLabel: "hdfs_file"
+                    });
+                }
+                self.isLoading(false);
+            };
+
+            self.recordsPerPage.subscribe(function (newValue) {
+                self.retrieveData();
             });
-            $("#fileChooserRename").slideDown();
-        });
-    });
-</script>
+
+            self.goToPage = function (pageNumber) {
+                self.targetPageNum(pageNumber);
+                self.retrieveData();
+            };
+
+            self.firstPage = function () {
+                self.goToPage(1)
+            };
+
+            self.previousPage = function () {
+                self.goToPage(self.page().previous_page_number)
+            };
+
+            self.nextPage = function () {
+                self.goToPage(self.page().next_page_number)
+            };
+
+            self.lastPage = function () {
+                self.goToPage(self.page().num_pages)
+            };
+
+            self.selectAll = function () {
+                ko.utils.arrayForEach(self.files(), function (file) {
+                    file.selected(!self.allSelected());
+                });
+                return true;
+            };
+
+            self.searchQuery.subscribe(function (newValue) {
+                self.filter();
+            });
+
+            self.filter = function () {
+                self.targetPageNum(1);
+                self.retrieveData();
+            };
+
+            self.viewFile = function (file) {
+                if (file.type == "dir") {
+                    self.targetPath("${url('filebrowser.views.view', path=urlencode('/'))}" + "." + file.path);
+                    window.location.hash = file.path;
+                }
+                else {
+                    location.href = "${url('filebrowser.views.view', path=urlencode('/'))}" + file.path;
+                }
+            };
+
+            self.editFile = function () {
+                location.href = "${url('filebrowser.views.edit', path=urlencode('/'))}" + self.selectedFile().path;
+            };
+
+            self.downloadFile = function () {
+                location.href = "${url('filebrowser.views.download', path=urlencode('/'))}" + self.selectedFile().path;
+            };
+
+            self.renameFile = function () {
+                $("#renameSrcPath").attr("value", self.selectedFile().path);
+                $("#renameFileName").text(self.selectedFile().path);
+                $("#newNameInput").val(self.selectedFile().path);
+                $("#renameModal").modal({
+                    keyboard:true,
+                    show:true
+                });
+            };
+
+            self.move = function () {
+                openMoveModal(self.selectedFile().path, self.selectedFile().mode, '${current_request_path}');
+            };
+
+            self.changeOwner = function () {
+                openChownWindow(self.selectedFile().path, self.selectedFile().stats.user, self.selectedFile().stats.group, '${current_request_path}');
+            };
+
+            self.changePermissions = function () {
+                openChmodWindow(self.selectedFile().path, self.selectedFile().mode, '${current_request_path}');
+            };
+
+            self.deleteSelected = function () {
+                $("#fileToDeleteInput").attr("value", self.selectedFile().path);
+                $("#deleteForm").attr("action", "/filebrowser/" + (self.selectedFile().type == "dir" ? "rmtree" : "remove") + "?next=${url('filebrowser.views.view', path=urlencode('/'))}" + "." + self.currentPath() + "&path=" + self.selectedFile().path);
+                $("#deleteModal").modal({
+                    keyboard:true,
+                    show:true
+                });
+            };
+
+            self.createDirectory = function (formElement) {
+                $(formElement).attr("action", "/filebrowser/mkdir?next=${url('filebrowser.views.view', path=urlencode('/'))}"+ "." + self.currentPath());
+                return true;
+            };
+        };
+
+        var viewModel = new FileBrowserModel([], null, [], "/");
+        ko.applyBindings(viewModel);
+
+    </script>
 
 </%def>

+ 29 - 11
apps/filebrowser/src/filebrowser/views.py

@@ -27,13 +27,16 @@ import operator
 import posixpath
 import stat as stat_module
 import os
+from types import MethodType
+
 try:
   import json
 except ImportError:
   import simplejson as json
 
 from django.contrib import messages
-from django.core import urlresolvers
+from django.core import urlresolvers, serializers
+from django.template.defaultfilters import stringformat, filesizeformat
 from django.http import Http404, HttpResponse, HttpResponseNotModified
 from django.views.static import was_modified_since
 from django.utils.http import http_date, urlquote
@@ -352,6 +355,17 @@ def listdir(request, path, chooser):
     else:
         return render('listdir.mako', request, data)
 
+def _massage_page(page):
+    return {
+        'number': page.number,
+        'num_pages': page.num_pages(),
+        'previous_page_number': page.previous_page_number(),
+        'next_page_number': page.next_page_number(),
+        'start_index': page.start_index(),
+        'end_index': page.end_index(),
+        'total_count': page.total_count()
+    }
+
 def listdir_paged(request, path):
     """
     A paginated version of listdir.
@@ -378,15 +392,6 @@ def listdir_paged(request, path):
 
     all_stats = request.fs.listdir_stats(path)
 
-    # Include parent dir, unless at filesystem root.
-    if Hdfs.normpath(path) != posixpath.sep:
-        parent_path = request.fs.join(path, "..")
-        parent_stat = request.fs.stats(parent_path)
-        # The 'path' field would be absolute, but we want its basename to be
-        # actually '..' for display purposes. Encode it since _massage_stats expects byte strings.
-        parent_stat['path'] = parent_path
-        parent_stat['name'] = ".."
-        all_stats.insert(0, parent_stat)
 
     # Filter first
     filter_str = request.GET.get('filter', None)
@@ -406,17 +411,28 @@ def listdir_paged(request, path):
                                key=operator.attrgetter(sortby),
                                reverse=coerce_bool(descending_param))
 
+
     # Do pagination
     page = paginator.Paginator(all_stats, pagesize).page(pagenum)
     shown_stats = page.object_list
+    # Include parent dir always as first option, unless at filesystem root.
+    if Hdfs.normpath(path) != posixpath.sep:
+        parent_path = request.fs.join(path, "..")
+        parent_stat = request.fs.stats(parent_path)
+        # The 'path' field would be absolute, but we want its basename to be
+        # actually '..' for display purposes. Encode it since _massage_stats expects byte strings.
+        parent_stat['path'] = parent_path
+        parent_stat['name'] = ".."
+        shown_stats.insert(0, parent_stat)
     page.object_list = [ _massage_stats(request, s) for s in shown_stats ]
 
+
     data = {
         'path': path,
         'breadcrumbs': breadcrumbs,
         'current_request_path': request.path,
         'files': page.object_list,
-        'page': page,
+        'page': _massage_page(page),
         'pagesize': pagesize,
         'home_directory': request.fs.isdir(home_dir_path) and home_dir_path or None,
         'filter_str': filter_str,
@@ -460,8 +476,10 @@ def _massage_stats(request, stats):
         'path': normalized,
         'name': stats['name'],
         'stats': stats.to_json_dict(),
+        'humansize': filesizeformat(stats['size']),
         'type': filetype(stats['mode']),
         'rwx': rwx(stats['mode']),
+        'mode': stringformat(stats['mode'], "o"),
         'url': make_absolute(request, "view", dict(path=urlquote(normalized))),
         }
 

+ 8 - 7
apps/filebrowser/src/filebrowser/views_test.py

@@ -236,10 +236,10 @@ def test_listdir_sort_and_filter():
     assert_equal(len(expect), len(listing))
 
     listing = c.get('/filebrowser/view' + BASE + '?pagesize=10').context['files']
-    assert_equal(10, len(listing))
+    assert_equal(11, len(listing))
 
     listing = c.get('/filebrowser/view' + BASE + '?pagesize=10&pagenum=1').context['files']
-    assert_equal(10, len(listing))
+    assert_equal(11, len(listing))
 
     listing = c.get('/filebrowser/view' + BASE + '?pagesize=10&pagenum=2').context['files']
     assert_equal(2, len(listing))
@@ -252,7 +252,8 @@ def test_listdir_sort_and_filter():
     assert_equal(sorted(expect), [ f['name'] for f in listing ])
 
     listing = c.get('/filebrowser/view' + BASE + '?sortby=name&descending=true').context['files']
-    assert_equal(sorted(expect, reverse=True), [ f['name'] for f in listing ])
+    assert_equal("..", listing[0]['name'])
+    assert_equal(FUNNY_NAME, listing[1]['name'])
 
     # Check sorting (size)
     listing = c.get('/filebrowser/view' + BASE + '?sortby=size').context['files']
@@ -265,18 +266,18 @@ def test_listdir_sort_and_filter():
 
     # Check filter
     listing = c.get('/filebrowser/view' + BASE + '?filter=1').context['files']
-    assert_equal(['1', '10'], [ f['name'] for f in listing ])
+    assert_equal(['..', '1', '10'], [ f['name'] for f in listing ])
 
     listing = c.get('/filebrowser/view' + BASE + '?filter=' + FUNNY_NAME).context['files']
-    assert_equal([FUNNY_NAME], [ f['name'] for f in listing ])
+    assert_equal(['..', FUNNY_NAME], [ f['name'] for f in listing ])
 
     # Check filter + sorting
     listing = c.get('/filebrowser/view' + BASE + '?filter=1&sortby=name&descending=true').context['files']
-    assert_equal(['10', '1'], [ f['name'] for f in listing ])
+    assert_equal(['..', '10', '1'], [ f['name'] for f in listing ])
 
     # Check filter + sorting + pagination
     listing = c.get('/filebrowser/view' + BASE + '?filter=1&sortby=name&descending=true&pagesize=1&pagenum=2').context['files']
-    assert_equal(['1'], [ f['name'] for f in listing ])
+    assert_equal(['..', '1'], [ f['name'] for f in listing ])
   finally:
     try:
       cluster.fs.rmtree(BASE)

+ 4 - 0
desktop/core/static/css/hue2.css

@@ -971,3 +971,7 @@ a#advanced-btn:visited {
 a#advanced-btn:hover {
   color: black; text-decoration: none;
 }
+
+.hand {
+    cursor: pointer;
+}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 5 - 0
desktop/core/static/ext/js/moment.min.js


+ 37 - 36
desktop/core/static/js/Source/jHue/jquery.rowselector.js

@@ -1,51 +1,52 @@
 /*
-* jHue row selector plugin
-* 
-*/
-;(function ($, window, document, undefined) {
+ * jHue row selector plugin
+ *
+ */
+;
+(function ($, window, document, undefined) {
 
-	var pluginName = "jHueRowSelector",
-	defaults = {
-	};
+    var pluginName = "jHueRowSelector",
+        defaults = {
+        };
 
-	function Plugin(element, options) {
-		this.element = element;
-		this.options = $.extend({}, defaults, options) ;
-		this._defaults = defaults;
-		this._name = pluginName;
-		this.init();
-	}
-	
-	Plugin.prototype.setOptions = function(options) {
-		this.options = $.extend({}, defaults, options) ;
-	};
+    function Plugin(element, options) {
+        this.element = element;
+        this.options = $.extend({}, defaults, options);
+        this._defaults = defaults;
+        this._name = pluginName;
+        this.init();
+    }
 
-	Plugin.prototype.init = function () {
-		var _this = this;		
-        $(_this.element).closest("tr").click(function(e){
-            if ($(e.target).data("row-selector-exclude")){
+    Plugin.prototype.setOptions = function (options) {
+        this.options = $.extend({}, defaults, options);
+    };
+
+    Plugin.prototype.init = function () {
+        var _this = this;
+        $(_this.element).closest("tr").click(function (e) {
+            if ($(e.target).data("row-selector-exclude")) {
                 return;
             }
-            if (!$(e.target).is("a")){
-                if ($.trim($(_this.element).attr("href")) != ""){
+            if (!$(e.target).is("a")) {
+                if ($.trim($(_this.element).attr("href")) != "" && $.trim($(_this.element).attr("href")) != "#") {
                     location.href = $(_this.element).attr("href");
                 }
                 else {
                     $(_this.element).click();
                 }
             }
-        }).css("cursor","pointer");
-	};
+        }).css("cursor", "pointer");
+    };
 
-	$.fn[pluginName] = function (options) {
-		return this.each(function () {
-			if (!$.data(this, 'plugin_' + pluginName)) {
-				$.data(this, 'plugin_' + pluginName, new Plugin( this, options));
-			}
-			else {
-				$.data(this, 'plugin_' + pluginName).setOptions(options);
-			}
-		});
-	}
+    $.fn[pluginName] = function (options) {
+        return this.each(function () {
+            if (!$.data(this, 'plugin_' + pluginName)) {
+                $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
+            }
+            else {
+                $.data(this, 'plugin_' + pluginName).setOptions(options);
+            }
+        });
+    }
 
 })(jQuery, window, document);

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно