Bläddra i källkod

HUE-767 [jb] Improve the look and feel with bootstraps

Added get_status for status labels
Maps/reduces as progress bars + fix for zero percentage
Removed 'View' links from tables
Updated look to Job list filters
Applied Bootstrap colors to progress bars
Updated css for the progress bar label fix
Resized kill button
Same row height for every job
Killed jobs have now a red progress bar (the only case where Bootstrap is not consistent in class names)
Introduced confirmation popup when killing a job
Statuses are left aligned now
Implemented "show retired" checkbox
Added the username in the user name filter field
Enrico Berti 13 år sedan
förälder
incheckning
a849ded

+ 1 - 1
apps/jobbrowser/src/jobbrowser/templates/attempt.mako

@@ -116,7 +116,7 @@ ${commonheader(_('Task Attempt: %(attemptId)s - Job Browser') % dict(attemptId=a
                 </div>
 
                 <div class="tab-pane" id="counters">
-                    ${comps.task_counters(task.counters, _)}
+                    ${comps.task_counters(task.counters)}
                 </div>
 
                 <div class="tab-pane jt-logs" id="logs">

+ 36 - 39
apps/jobbrowser/src/jobbrowser/templates/job.mako

@@ -30,19 +30,15 @@
         <tr>
             <th>${_('Tasks')}</th>
             <th>${_('Type')}</th>
-            <th>&nbsp;</th>
         </tr>
         </thead>
         <tbody>
-                % for task in tasks:
-                <tr>
-                    <td>${task.taskId_short}</td>
-                    <td>${task.taskType}</td>
-                    <td>
-                        <a title="${_('View this task')}" href="${ url('jobbrowser.views.single_task', jobid=job.jobId, taskid=task.taskId) }">${_('View')}</a>
-                    </td>
-                </tr>
-                % endfor
+            % for task in tasks:
+            <tr>
+                <td><a title="${_('View this task')}" href="${ url('jobbrowser.views.single_task', jobid=job.jobId, taskid=task.taskId) }" data-row-selector="true">${task.taskId_short}</a></td>
+                <td>${task.taskType}</td>
+            </tr>
+            % endfor
         </tbody>
     </table>
 </%def>
@@ -92,25 +88,29 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
                     <li>${job.user}</li>
                     <li class="nav-header">${_('Status')}</li>
                     <li>
-                            % if job.status.lower() == 'running' or job.status.lower() == 'pending':
-                                <span class="label label-warning">${job.status.lower()}</span>
-                            % elif job.status.lower() == 'succeeded':
-                                <span class="label label-success">${job.status.lower()}</span>
-                            % else:
-                                <span class="label">${job.status.lower()}</span>
-                            % endif
-
-                            % if job.is_retired:
-                                <span class="label label-warning">${ _('retired') }</span>
-                            % endif
+                        ${comps.get_status(job)}
                     </li>
                     % if job.status.lower() == 'running' or job.status.lower() == 'pending':
-                            <li class="nav-header">${_('Kill Job')}</li>
-                            <li><a href="#" title="${_('Kill this job')}" onclick="$('#kill-job').submit()">${_('Kill this job')}</a>
-                                <form id="kill-job" action="${url('jobbrowser.views.kill_job', jobid=job.jobId)}?next=${request.get_full_path()|urlencode}" method="POST"></form></li>
+                        <li class="nav-header">${_('Kill Job')}</li>
+                        <li><a href="#" title="${_('Kill this job')}" onclick="$('#kill-job').submit()">${_('Kill this job')}</a>
+                        <form id="kill-job" action="${url('jobbrowser.views.kill_job', jobid=job.jobId)}?next=${request.get_full_path()|urlencode}" method="POST"></form></li>
                     % endif
-                    <li class="nav-header">${_('Output')}</li>
-                    <li>
+                    % if not job.is_retired:
+                        <li class="nav-header">${_('Maps:')}</li>
+                        <li>${comps.mr_graph_maps(job)}</li>
+                        <li class="nav-header">${_('Reduces:')}</li>
+                        <li>${comps.mr_graph_reduces(job)}</li>
+                    % endif
+
+                    <%
+                        output_dir = job.conf_keys.get('mapredOutputDir', "")
+                        location_url = location_to_url(request, output_dir)
+                        basename = os.path.basename(output_dir)
+                        dir_name = basename.split('/')[-1]
+                    %>
+                    % if dir_name != '':
+                        <li class="nav-header">${_('Output')}</li>
+                        <li>
                         <%
                             output_dir = job.conf_keys.get('mapredOutputDir', "")
                             location_url = location_to_url(request, output_dir)
@@ -122,7 +122,8 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
                         % else:
                             ${dir_name}
                         % endif
-                    </li>
+                        </li>
+                    % endif
                 </ul>
             </div>
         </div>
@@ -135,11 +136,8 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
 
             <div class="tab-content">
                 <div class="tab-pane active" id="tasks">
-                    % if not job.is_retired:
-	                    <strong>${_('Maps:')}</strong> ${comps.mr_graph_maps(job)}
-	                    <strong>${_('Reduces:')}</strong> ${comps.mr_graph_reduces(job)}
-	                % else:
-	                   ${ _('This jobs is ')} <span class="label label-warning">${ _('retired') }</span> ${ _(' and so has little information available.') }
+                    % if job.is_retired:
+                       ${ _('This jobs is ')} <span class="label label-warning">${ _('retired') }</span> ${ _(' and so has little information available.') }
                        <br/>
                        <br/>
                     % endif
@@ -174,8 +172,8 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
                     </div>
                     <table id="metadataTable" class="table table-striped table-condensed">
                         <thead>
-	                        <th>${_('Name')}</th>
-	                        <th>${_('Value')}</th>
+                            <th>${_('Name')}</th>
+                            <th>${_('Value')}</th>
                         </thead>
                         <tbody>
                         <tr>
@@ -241,7 +239,7 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
 
                 </div>
                 <div id="counters" class="tab-pane">
-                    ${comps.job_counters(job.counters, _)}
+                    ${comps.job_counters(job.counters)}
                 </div>
             </div>
         </div>
@@ -258,9 +256,8 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
             "bInfo": false,
             "bAutoWidth": false,
             "aoColumns": [
-                { "sWidth": "40%" },
-                { "sWidth": "40%" },
-                { "sWidth": "20%" }
+                { "sWidth": "50%" },
+                { "sWidth": "50%" }
             ]
         });
         var _metadataTable = $("#metadataTable").dataTable({
@@ -304,7 +301,7 @@ ${commonheader(_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId), "jobbr
 
         $(".dataTables_wrapper").css("min-height","0");
         $(".dataTables_filter").hide();
-
+        $("a[data-row-selector='true']").jHueRowSelector();
     });
 </script>
 

+ 49 - 4
apps/jobbrowser/src/jobbrowser/templates/jobbrowser_components.mako

@@ -13,8 +13,11 @@
 ## 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.
+<%!
+    from django.utils.translation import ugettext as _
+%>
 
-<%def name="task_counters(counters,_)">
+<%def name="task_counters(counters)">
     <%
         from jobbrowser.views import format_counter_name
     %>
@@ -39,7 +42,7 @@
     % endfor
 </%def>
 
-<%def name="job_counters(counters,_)">
+<%def name="job_counters(counters)">
     <%
         from jobbrowser.views import format_counter_name
     %>
@@ -88,9 +91,51 @@
 </%def>
 
 <%def name="mr_graph_maps(job)">
-    <div class="bar">${job.finishedMaps} / ${job.desiredMaps}</div>
+    <div class="progress ${get_bootstrap_class(job, 'progress')}">
+        <div class="bar" style="width: ${job.maps_percent_complete}%;"></div>
+        <div class="bar-label">${job.finishedMaps} / ${job.desiredMaps}</div>
+    </div>
 </%def>
 
 <%def name="mr_graph_reduces(job)">
-    <div class="bar">${job.finishedReduces} / ${job.desiredReduces}</div>
+    <div class="progress ${get_bootstrap_class(job, 'progress')}">
+        <div class="bar" style="width: ${job.reduces_percent_complete}%;"></div>
+        <div class="bar-label">${job.finishedReduces} / ${job.desiredReduces}</div>
+    </div>
+</%def>
+
+<%def name="get_status(job)">
+    <%
+    additional_class = get_bootstrap_class(job, 'label')
+    %>
+    % if job.is_retired:
+        <span class="label ${additional_class}"><i class="icon-briefcase icon-white" title="${ _('Retired') }"></i> ${job.status.lower()}</span>
+    % else:
+        <span class="label ${additional_class}">${job.status.lower()}</span>
+    % endif
 </%def>
+
+
+<%def name="get_bootstrap_class(job, prefix)">
+    <%
+    additional_class = prefix
+    status = job.status.lower()
+    if status in ('succeeded', 'ok'):
+        additional_class += '-success'
+    elif status in ('running', 'prep'):
+        additional_class += '-warning'
+    elif status == 'ready':
+        additional_class += '-success'
+    else:
+        if (prefix == 'label'):
+            additional_class += '-important'
+        else:
+            additional_class += '-danger'
+        endif
+    endif
+    if job.is_retired:
+        additional_class += '-warning'
+    endif
+    return additional_class
+    %>
+</%def>

+ 80 - 33
apps/jobbrowser/src/jobbrowser/templates/jobs.mako

@@ -32,26 +32,49 @@
 ${commonheader(_('Job Browser'), "jobbrowser")}
 <div class="container-fluid">
 <h1>${_('Job Browser')}</h1>
-<div class="well hueWell">
-    <form action="/jobbrowser/jobs" method="GET">
-        <b>${_('Filter jobs:')}</b>
-                <select name="state" class="submitter">
-                    <option value="all" ${get_state('all', state_filter)}>${_('All States')}</option>
-                    <option value="running" ${get_state('running', state_filter)}>${_('Running')}</option>
-                    <option value="completed" ${get_state('completed', state_filter)}>${_('Completed')}</option>
-                    <option value="failed" ${get_state('failed', state_filter)}>${_('Failed')}</option>
-                    <option value="killed" ${get_state('killed', state_filter)}>${_('Killed')}</option>
-                </select>
+<form class="well form-inline" action="/jobbrowser/jobs" method="GET">
+    <label>
+    ${_('Job status:')}
+    <select name="state" class="submitter">
+        <option value="all" ${get_state('all', state_filter)}>${_('All States')}</option>
+        <option value="running" ${get_state('running', state_filter)}>${_('Running')}</option>
+        <option value="completed" ${get_state('completed', state_filter)}>${_('Completed')}</option>
+        <option value="failed" ${get_state('failed', state_filter)}>${_('Failed')}</option>
+        <option value="killed" ${get_state('killed', state_filter)}>${_('Killed')}</option>
+    </select>
+    </label>
+    &nbsp;
+    <label class="checkbox">
+        <%
+            checked = ""
+            if retired == "on":
+                checked = 'checked="checked"'
+        %>
+        <input name="retired" type="checkbox" class="submitter" ${checked}> Show retired jobs
+    </label>
 
-                <input type="text" name="user" title="${_('User Name Filter')}" value="${user_filter}" placeholder="${_('User Name Filter')}" class="submitter"/>
-                <input type="text" name="text" title="${_('Text Filter')}" value="${text_filter}" placeholder="${_('Text Filter')}" class="submitter"/>
-    </form>
-</div>
+    <label class="pull-right">
+        &nbsp;
+    ${_('Text:')}
+        <input type="text" name="text" title="${_('Text Filter')}" value="${text_filter}" placeholder="${_('Text Filter')}" class="submitter input-medium search-query"/>
+    </label>
+    <label class="pull-right">
+        ${_('User Name:')}
+        <input type="text" name="user" title="${_('User Name Filter')}" value="${user_filter}" placeholder="${_('User Name Filter')}" class="submitter input-medium search-query" />
+    </label>
+
+
+</form>
 
 
 % if len(jobs) == 0:
 <p>${_('There were no jobs that match your search criteria.')}</p>
 % else:
+<style>
+    .job-row {
+        height:45px;
+    }
+</style>
 <table class="datatables table table-striped table-condensed">
     <thead>
         <tr>
@@ -70,29 +93,25 @@ ${commonheader(_('Job Browser'), "jobbrowser")}
     </thead>
     <tbody>
         % for job in jobs:
-        <tr>
+        <tr class="job-row">
             <td>
-                <div class="jobbrowser_jobid_short">${job.jobId_short}</div>
+                <a href="${url('jobbrowser.views.single_job', jobid=job.jobId)}" title="${_('View this job')}" data-row-selector="true">${job.jobId_short}</a>
             </td>
             <td>
                 ${job.jobName}
             </td>
             <td>
-                <a href="${url('jobbrowser.views.jobs')}?${get_state_link(request, 'state', job.status.lower())}" title="${_('Show only %(status)s jobs') % dict(status=job.status.lower())}">
-                  ${job.status.lower()}
+                <a href="${url('jobbrowser.views.jobs')}?${get_state_link(request, 'state', job.status.lower())}" title="${_('Show only %(status)s jobs') % dict(status=job.status.lower())}" class="nounderline">
+                    ${comps.get_status(job)}
                 </a>
-                % if job.is_retired:
-                  </br>
-                  ${_('retired')}
-                % endif
             </td>
-            <td>
+            <td class="center">
                 <a href="${url('jobbrowser.views.jobs')}?${get_state_link(request, 'user', job.user.lower())}" title="${_('Show only %(status)s jobs') % dict(status=job.user.lower())}">${job.user}</a>
             </td>
             <td>
                 <span alt="${job.maps_percent_complete}">
                     % if job.is_retired:
-                        ${_('N/A')}
+                        <div class="center">${_('N/A')}</div>
                     % else:
                     ${comps.mr_graph_maps(job)}
                     % endif
@@ -101,15 +120,15 @@ ${commonheader(_('Job Browser'), "jobbrowser")}
             <td>
                 <span alt="${job.reduces_percent_complete}">
                     % if job.is_retired:
-                        ${_('N/A')}
+                        <div class="center">${_('N/A')}</div>
                     % else:
                         ${comps.mr_graph_reduces(job)}
                     % endif
                 </span>
             </td>
-            <td>${job.queueName}</td>
-            <td>${job.priority.lower()}</td>
-            <td>
+            <td class="center">${job.queueName}</td>
+            <td class="center">${job.priority.lower()}</td>
+            <td class="center">
                 <span alt="${job.finishTimeMs-job.startTimeMs}">
                     % if job.is_retired:
                         ${_('N/A')}
@@ -120,11 +139,9 @@ ${commonheader(_('Job Browser'), "jobbrowser")}
             </td>
             <td><span alt="${job.startTimeMs}">${job.startTimeFormatted}</span></td>
             <td>
-                <a href="${url('jobbrowser.views.single_job', jobid=job.jobId)}" title="${_('View this job')}" data-row-selector="true">${_('View')}</a>
                 % if job.status.lower() == 'running' or job.status.lower() == 'pending':
                 % if request.user.is_superuser or request.user.username == job.user:
-                - <a href="#" title="${_('Kill this job')}" onclick="$('#kill-job').submit()">${_('Kill')}</a>
-                <form id="kill-job" action="${url('jobbrowser.views.kill_job', jobid=job.jobId)}?next=${request.get_full_path()|urlencode}" method="POST"></form>
+                <a href="#" title="${_('Kill this job')}" kill-action="${url('jobbrowser.views.kill_job', jobid=job.jobId)}?next=${request.get_full_path()|urlencode}" data-backdrop="static" data-keyboard="true" class="btn btn-mini kill"><i class="icon-remove"></i> ${_('Kill')}</a>
                 % endif
                 % endif
             </td>
@@ -150,6 +167,22 @@ ${commonheader(_('Job Browser'), "jobbrowser")}
     % endif
 </div>
 
+<div id="killModal" class="modal hide fade">
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3>${_('Please Confirm')}</h3>
+    </div>
+    <div class="modal-body">
+        <p>${_('Are you sure you want to kill this job?')}</p>
+    </div>
+    <div class="modal-footer">
+        <form id="kill-job" action="" method="POST" class="form-stacked">
+            <input type="submit" value="${_('Yes')}" class="btn primary" />
+            <a id="cancelKillBtn" class="btn">${_('No')}</a>
+        </form>
+    </div>
+</div>
+
 <script type="text/javascript" charset="utf-8">
     $(document).ready(function(){
         $(".datatables").dataTable({
@@ -163,8 +196,8 @@ ${commonheader(_('Job Browser'), "jobbrowser")}
                 null,
                 null,
                 null,
-                { "sType": "alt-numeric" },
-                { "sType": "alt-numeric" },
+                { "sType": "alt-numeric", "sWidth": "60px" },
+                { "sType": "alt-numeric", "sWidth": "60px" },
                 null,
                 null,
                 { "sType": "alt-numeric" },
@@ -172,6 +205,20 @@ ${commonheader(_('Job Browser'), "jobbrowser")}
                 {"bSortable":false}
             ]
         });
+
+        $(".kill").live("click", function(e){
+            $("#kill-job").attr("action", $(e.target).attr("kill-action"));
+            $("#killModal").modal({
+                backdrop: "static",
+                keyboard: true,
+                show: true
+            });
+        });
+
+        $("#cancelKillBtn").click(function(){
+            $("#killModal").modal("hide");
+        });
+
         $("a[data-row-selector='true']").jHueRowSelector();
     });
 </script>

+ 3 - 5
apps/jobbrowser/src/jobbrowser/templates/task.mako

@@ -67,13 +67,13 @@ ${commonheader(_('Job Task: %(taskId)s - Job Browser') % dict(taskId=task.taskId
                             <th>${_('Shuffle Finish')}</th>
                             <th>${_('Sort Finish')}</th>
                             <th>${_('Map Finish')}</th>
-                            <th>${_('View')}</th>
                         </tr>
                         </thead>
                         <tbody>
                                 % for attempt in task.attempts:
                                 <tr>
-                                    <td>${attempt.attemptId_short}</td>
+                                    <td><a title="${_('View this attempt')}"
+                                           href="${ url('jobbrowser.views.single_task_attempt', jobid=joblnk.jobId, taskid=task.taskId, attemptid=attempt.attemptId) }" data-row-selector="true">${attempt.attemptId_short}</a></td>
                                     <td>${"%d" % (attempt.progress * 100)}%</td>
                                     <td><span class="status_link ${attempt.state}">${attempt.state}</span></td>
                                     <td><a href="/jobbrowser/trackers/${attempt.taskTrackerId}" class="task_tracker_link">${attempt.taskTrackerId}</a></td>
@@ -84,8 +84,6 @@ ${commonheader(_('Job Task: %(taskId)s - Job Browser') % dict(taskId=task.taskId
                                     <td>${attempt.shuffleFinishTimeFormatted}</td>
                                     <td>${attempt.sortFinishTimeFormatted}</td>
                                     <td>${attempt.mapFinishTimeFormatted}</td>
-                                    <td><a title="${_('View this attempt')}"
-                                           href="${ url('jobbrowser.views.single_task_attempt', jobid=joblnk.jobId, taskid=task.taskId, attemptid=attempt.attemptId) }" data-row-selector="true">${_('View')}</a></td>
                                 </tr>
                                 % endfor
                         </tbody>
@@ -138,7 +136,7 @@ ${commonheader(_('Job Task: %(taskId)s - Job Browser') % dict(taskId=task.taskId
                     </table>
                 </div>
                 <div id="counters" class="tab-pane">
-                    ${comps.task_counters(task.counters, _)}
+                    ${comps.task_counters(task.counters)}
                 </div>
             </div>
         </div>

+ 15 - 2
apps/jobbrowser/src/jobbrowser/views.py

@@ -27,6 +27,7 @@ from desktop.lib.paginator import Paginator
 from desktop.lib.django_util import render_json, MessageException, render
 from desktop.lib.django_util import copy_query_dict
 from desktop.lib.django_util import PopupException
+from desktop.lib.conf import coerce_bool
 
 from django.http import HttpResponseRedirect
 from django.utils.functional import wraps
@@ -102,14 +103,16 @@ def jobs(request):
 
   matching_jobs = sort_if_necessary(request, jobs)
   state = request.GET.get('state', 'all')
-  user = request.GET.get('user', '')
+  user = request.GET.get('user', request.user.username)
   text = request.GET.get('text', '')
+  retired = request.GET.get('retired', '')
   return render("jobs.mako", request, {
     'jobs':matching_jobs,
     'request': request,
     'state_filter': state,
     'user_filter': user,
     'text_filter': text,
+    'retired': retired,
     'filtered': not (state == 'all' and user == '' and text == '')
   })
 
@@ -481,7 +484,17 @@ def get_matching_jobs(request, check_permission=False, **kwargs):
   else:
     selection = request.GET.get("state", "all")
 
-  joblist = jobfunc[selection][0]().jobs + request.jt.retired_jobs(jobfunc[selection][1]).jobs
+  if 'retired' in kwargs:
+    retired_arg = kwargs['retired']
+  else:
+    retired_arg = request.GET.get("retired", None)
+
+  retired = coerce_bool(retired_arg)
+
+  joblist = jobfunc[selection][0]().jobs
+
+  if retired == True:
+    joblist += request.jt.retired_jobs(jobfunc[selection][1]).jobs
 
   return [Job.from_thriftjob(request.jt, j)
           for j in _filter_jobs_by_req(joblist, request, **kwargs)

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

@@ -904,4 +904,17 @@ div.box {
 
 .table tbody tr:hover td, .table tbody tr:hover th {
     background-color: #E8F1FB;
+}
+
+.nounderline:hover {
+    text-decoration: none;
+}
+
+.bar-label {
+    margin-top: -18px;
+    position: relative;
+    text-align: center;
+    color: #FFFFFF;
+    font-size: 12px;
+    text-shadow: -1px 0 1px #333, 0 1px 1px #333, 1px 0 1px #333, 0 -1px 1px #333;
 }