Restructured Web UI. Does not change the logic of the Web UI, only updates the structure to have fewer nested directories and a more logical grouping of files.
Review: https://reviews.apache.org/r/66553/ Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/c7685917 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/c7685917 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/c7685917 Branch: refs/heads/master Commit: c76859170c9f7fe36d8479ad6c36f0f69b2f2f5d Parents: 28998f7 Author: Armand Grillet <agril...@mesosphere.io> Authored: Wed Apr 11 15:36:23 2018 -0700 Committer: Benjamin Mahler <bmah...@apache.org> Committed: Wed Apr 11 15:37:02 2018 -0700 ---------------------------------------------------------------------- src/Makefile.am | 78 +- src/master/master.cpp | 5 +- src/webui/app/agents/agent-browse.html | 87 ++ src/webui/app/agents/agent-executor.html | 204 +++ src/webui/app/agents/agent-framework.html | 164 +++ src/webui/app/agents/agent.html | 293 +++++ src/webui/app/agents/agents.html | 56 + src/webui/app/app.js | 371 ++++++ src/webui/app/controllers.js | 1179 ++++++++++++++++++ src/webui/app/frameworks/framework.html | 261 ++++ src/webui/app/frameworks/frameworks.html | 179 +++ src/webui/app/home.html | 334 +++++ src/webui/app/maintenance/maintenance.html | 36 + src/webui/app/offers/offers.html | 47 + src/webui/app/roles/roles.html | 60 + src/webui/app/services.js | 309 +++++ src/webui/app/shared/pagination.html | 6 + src/webui/app/shared/pailer.html | 80 ++ src/webui/app/shared/table-header.html | 20 + src/webui/app/shared/timestamp.html | 5 + src/webui/assets/css/bootstrap-3.3.6.min.css | 6 + .../assets/css/bootstrap-table-1.11.1.min.css | 1 + src/webui/assets/css/mesos.css | 232 ++++ .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 +++++ .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes src/webui/assets/ico/favicon.ico | Bin 0 -> 5430 bytes src/webui/assets/img/loading.gif | Bin 0 -> 4710 bytes src/webui/assets/img/mesos-logo.png | Bin 0 -> 7888 bytes src/webui/assets/libs/angular-1.2.32.min.js | 218 ++++ .../assets/libs/angular-route-1.2.32.min.js | 14 + .../assets/libs/bootstrap-table-1.11.1.min.js | 8 + src/webui/assets/libs/clipboard-1.5.16.min.js | 7 + src/webui/assets/libs/jquery-3.2.1.min.js | 4 + src/webui/assets/libs/jquery.pailer.js | 350 ++++++ src/webui/assets/libs/relative-date.js | 48 + .../assets/libs/ui-bootstrap-tpls-0.9.0.min.js | 2 + src/webui/assets/libs/underscore-1.4.3.min.js | 1 + src/webui/index.html | 128 ++ src/webui/master/static/agent.html | 293 ----- src/webui/master/static/agent_executor.html | 204 --- src/webui/master/static/agent_framework.html | 164 --- src/webui/master/static/agents.html | 56 - src/webui/master/static/browse.html | 87 -- .../master/static/css/bootstrap-3.3.6.min.css | 6 - .../static/css/bootstrap-table-1.11.1.min.css | 1 - src/webui/master/static/css/mesos.css | 232 ---- .../master/static/directives/pagination.html | 6 - .../master/static/directives/tableHeader.html | 20 - .../master/static/directives/timestamp.html | 5 - .../fonts/glyphicons-halflings-regular.eot | Bin 20127 -> 0 bytes .../fonts/glyphicons-halflings-regular.svg | 288 ----- .../fonts/glyphicons-halflings-regular.ttf | Bin 45404 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 23424 -> 0 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 18028 -> 0 bytes src/webui/master/static/framework.html | 261 ---- src/webui/master/static/frameworks.html | 179 --- src/webui/master/static/home.html | 334 ----- src/webui/master/static/ico/favicon.ico | Bin 5430 -> 0 bytes src/webui/master/static/img/loading.gif | Bin 4710 -> 0 bytes src/webui/master/static/img/mesos_logo.png | Bin 7888 -> 0 bytes src/webui/master/static/index.html | 128 -- .../master/static/js/angular-1.2.32.min.js | 218 ---- .../static/js/angular-route-1.2.32.min.js | 14 - src/webui/master/static/js/app.js | 371 ------ .../static/js/bootstrap-table-1.11.1.min.js | 8 - .../master/static/js/clipboard-1.5.16.min.js | 7 - src/webui/master/static/js/controllers.js | 1179 ------------------ src/webui/master/static/js/jquery-3.2.1.min.js | 4 - src/webui/master/static/js/jquery.pailer.js | 350 ------ src/webui/master/static/js/relative-date.js | 48 - src/webui/master/static/js/services.js | 309 ----- .../static/js/ui-bootstrap-tpls-0.9.0.min.js | 2 - .../master/static/js/underscore-1.4.3.min.js | 1 - src/webui/master/static/maintenance.html | 36 - src/webui/master/static/offers.html | 47 - src/webui/master/static/pailer.html | 80 -- src/webui/master/static/roles.html | 60 - 80 files changed, 5040 insertions(+), 5039 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/Makefile.am ---------------------------------------------------------------------- diff --git a/src/Makefile.am b/src/Makefile.am index 9f4b6d3..257ff0e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1778,60 +1778,60 @@ dist_bin_SCRIPTS += \ # that 'datadir' (e.g., /usr/local/share) is for read-only "data" and # 'sysconfdir' (e.g., /usr/local/var) is for modifiable "data". nobase_dist_pkgdata_DATA += \ - webui/master/static/js/app.js \ - webui/master/static/js/controllers.js \ - webui/master/static/js/jquery.pailer.js \ - webui/master/static/js/services.js + webui/app/app.js \ + webui/app/controllers.js \ + webui/app/services.js # Need to distribute/install webui CSS. nobase_dist_pkgdata_DATA += \ - webui/master/static/css/bootstrap-3.3.6.min.css \ - webui/master/static/css/bootstrap-table-1.11.1.min.css \ - webui/master/static/css/mesos.css + webui/assets/css/bootstrap-3.3.6.min.css \ + webui/assets/css/bootstrap-table-1.11.1.min.css\ + webui/assets/css/mesos.css # Need to distribute/install webui HTML. nobase_dist_pkgdata_DATA += \ - webui/master/static/agent.html \ - webui/master/static/agents.html \ - webui/master/static/agent_executor.html \ - webui/master/static/agent_framework.html \ - webui/master/static/browse.html \ - webui/master/static/framework.html \ - webui/master/static/frameworks.html \ - webui/master/static/home.html \ - webui/master/static/index.html \ - webui/master/static/maintenance.html \ - webui/master/static/offers.html \ - webui/master/static/pailer.html \ - webui/master/static/roles.html \ - webui/master/static/directives/pagination.html \ - webui/master/static/directives/tableHeader.html \ - webui/master/static/directives/timestamp.html + webui/index.html \ + webui/app/home.html \ + webui/app/agents/agent.html \ + webui/app/agents/agent-browse.html \ + webui/app/agents/agent-executor.html \ + webui/app/agents/agent-framework.html \ + webui/app/agents/agents.html \ + webui/app/frameworks/framework.html \ + webui/app/frameworks/frameworks.html \ + webui/app/maintenance/maintenance.html \ + webui/app/offers/offers.html \ + webui/app/roles/roles.html \ + webui/app/shared/pagination.html \ + webui/app/shared/pailer.html \ + webui/app/shared/table-header.html \ + webui/app/shared/timestamp.html # Need to distribute/install webui images. nobase_dist_pkgdata_DATA += \ - webui/master/static/ico/favicon.ico \ - webui/master/static/img/loading.gif \ - webui/master/static/img/mesos_logo.png + webui/assets/ico/favicon.ico \ + webui/assets/img/loading.gif \ + webui/assets/img/mesos-logo.png # Need to distribute/install webui fonts. nobase_dist_pkgdata_DATA += \ - webui/master/static/fonts/glyphicons-halflings-regular.eot \ - webui/master/static/fonts/glyphicons-halflings-regular.svg \ - webui/master/static/fonts/glyphicons-halflings-regular.ttf \ - webui/master/static/fonts/glyphicons-halflings-regular.woff \ - webui/master/static/fonts/glyphicons-halflings-regular.woff2 + webui/assets/fonts/glyphicons-halflings-regular.eot \ + webui/assets/fonts/glyphicons-halflings-regular.svg \ + webui/assets/fonts/glyphicons-halflings-regular.ttf \ + webui/assets/fonts/glyphicons-halflings-regular.woff \ + webui/assets/fonts/glyphicons-halflings-regular.woff2 # Need to distribute/install third-party javascript. nobase_dist_pkgdata_DATA += \ - webui/master/static/js/angular-1.2.32.min.js \ - webui/master/static/js/angular-route-1.2.32.min.js \ - webui/master/static/js/bootstrap-table-1.11.1.min.js \ - webui/master/static/js/clipboard-1.5.16.min.js \ - webui/master/static/js/jquery-3.2.1.min.js \ - webui/master/static/js/relative-date.js \ - webui/master/static/js/ui-bootstrap-tpls-0.9.0.min.js \ - webui/master/static/js/underscore-1.4.3.min.js + webui/assets/libs/angular-1.2.32.min.js \ + webui/assets/libs/angular-route-1.2.32.min.js \ + webui/assets/libs/bootstrap-table-1.11.1.min.js \ + webui/assets/libs/clipboard-1.5.16.min.js \ + webui/assets/libs/jquery-3.2.1.min.js \ + webui/assets/libs/jquery.pailer.js \ + webui/assets/libs/relative-date.js \ + webui/assets/libs/ui-bootstrap-tpls-0.9.0.min.js \ + webui/assets/libs/underscore-1.4.3.min.js # And the deploy related stuff. nodist_sbin_SCRIPTS += \ http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/master/master.cpp ---------------------------------------------------------------------- diff --git a/src/master/master.cpp b/src/master/master.cpp index f7da675..767ad8c 100644 --- a/src/master/master.cpp +++ b/src/master/master.cpp @@ -1092,8 +1092,9 @@ void Master::initialize() // build directory before 'make install') or determined at build // time via the preprocessor macro '-DMESOS_WEBUI_DIR' set in the // Makefile. - provide("", path::join(flags.webui_dir, "master/static/index.html")); - provide("static", path::join(flags.webui_dir, "master/static")); + provide("", path::join(flags.webui_dir, "index.html")); + provide("app", path::join(flags.webui_dir, "app")); + provide("assets", path::join(flags.webui_dir, "assets")); const PID<Master> masterPid = self(); http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/agents/agent-browse.html ---------------------------------------------------------------------- diff --git a/src/webui/app/agents/agent-browse.html b/src/webui/app/agents/agent-browse.html new file mode 100644 index 0000000..b7ac395 --- /dev/null +++ b/src/webui/app/agents/agent-browse.html @@ -0,0 +1,87 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#">Master</a> + </li> + <li> + <a class="badge badge-type" href="#/agents/{{agent_id}}" title="{{agent_id}}"> + Agent</a> + </li> + <li class="active"> + Browse + </li> +</ol> + +<ol class="breadcrumb"> + <!-- We want to ensure that if the user highlights the path breadcrumb, + and copies it, they will receive a /path/without/spaces that they + can then paste into a terminal, or elsewhere. In order to do this, + we have to ensure there is no whitespace within the <a> tag contents. + Also, we have to inject a hidden '/' character because the slashes + in the breadcrumb are not copied. + --> + <li ng-repeat="dir in path.split('/')"><a + href="#/agents/{{agent_id}}/browse?path={{ + encodeURIComponent(path.split('/').slice(0, $index + 1).join('/')) + }}">{{dir}}</a><span class="hidden-text">/</span></li> +</ol> + +<div class="alert alert-error hidden" id="alert"> + <button class="close" data-dismiss="alert">Ã</button> + <strong>{{alert_message}}</strong> +</div> + +<div class="row" id="listing"> + <div class="col-md-9"> + <div class="well"> + <div data-ng-show="listing.length == 0"> + No files in this directory. + </div> + <table class="table table-condensed" data-ng-show="listing.length > 0"> + <thead> + <tr> + <th>mode</th> + <th class="text-right">nlink</th> + <th>uid</th> + <th>gid</th> + <th class="text-right">size</th> + <th class="text-right">mtime</th> + <th></th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="file in listing | orderBy:['-mode', 'path']"> + <td>{{file.mode}}</td> + <td class="text-right">{{file.nlink}}</td> + <td>{{file.uid}}</td> + <td>{{file.gid}}</td> + <td class="text-right">{{file.size | dataSize}}</td> + <td class="text-right">{{file.mtime * 1000 | unixDate}}</td> + <td> + <span data-ng-show="file.mode[0] == 'd'"> + <i class="glyphicon glyphicon-folder-close"></i> + <a href="#/agents/{{agent_id}}/browse?path={{encodeURIComponent(file.path)}}"> + {{basename(file.path)}} + </a> + </span> + <span data-ng-show="file.mode[0] != 'd'"> + <i class="glyphicon glyphicon-file" style="visibility: hidden;"></i> + <a href="" ng-click="pail($event, encodeURIComponent(file.path))"> + {{basename(file.path)}} + </a> + </span> + </td> + <td> + <a data-ng-show="file.mode[0] != 'd'" + href="{{agent_url_prefix}}/files/download?path={{encodeURIComponent(file.path)}}"> + <button class="btn btn-xs btn-default" type="button"> + Download + </button> + </a> + </td> + </tr> + </tbody> + </table> + </div> + </div> +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/agents/agent-executor.html ---------------------------------------------------------------------- diff --git a/src/webui/app/agents/agent-executor.html b/src/webui/app/agents/agent-executor.html new file mode 100644 index 0000000..7ec56c3 --- /dev/null +++ b/src/webui/app/agents/agent-executor.html @@ -0,0 +1,204 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#">Master</a> + </li> + <li> + <a class="badge badge-type" href="#/agents/{{agent_id}}" title="{{agent_id}}"> + Agent</a> + </li> + <li> + <a class="badge badge-type" href="#/agents/{{agent_id}}/frameworks/{{framework_id}}" title="{{framework_id}}"> + Framework</a> + </li> + <li class="active"> + <span class="badge badge-type">Executor</span> + {{executor_id}} + </li> +</ol> + +<div class="alert alert-error hidden" id="alert"> + <button class="close" data-dismiss="alert">Ã</button> + <strong>{{alert_message}}</strong> +</div> + +<div class="row" id="agent"> + <div class="col-md-3"> + <div class="well"> + <dl class="inline clearfix"> + <dt>Executor Name:</dt> + <dd>{{executor.name}}</dd> + <dt>Executor Source:</dt> + <dd>{{executor.source}}</dd> + <dt>Executor Role:</dt> + <dd>{{executor.role}}</dd> + </dl> + + <dl class="inline clearfix"> + <dt>Cluster:</dt> + <dd> + <span ng-show="clusterNamed">{{cluster}}</span> + <span ng-show="!clusterNamed"> + (Unnamed) + <i class="icon-info-sign" + tooltip="To name this cluster, set the --cluster flag when starting the master." + tooltip-placement="right"></i> + </span> + </dd> + <dt>Master:</dt> + <dd>{{state.master_hostname}}</dd> + <dt>Agent:</dt> + <dd>{{state.hostname}}</dd> + </dl> + + <dl class="inline clearfix"> + <dt>Active Tasks:</dt> + <dd>{{executor.tasks.length | number}}</dd> + </dl> + + <h4>Resources</h4> + <table class="table table-condensed"> + <thead> + <tr> + <td></td> + <td class="text-right">Used</td> + <td class="text-right">Allocated</td> + </tr> + </thead> + <tbody> + <tr> + <td>CPUs</td> + <td class="text-right"> + {{monitor.frameworks[framework.id].executors[executor.id].statistics.cpus_total_usage | number}} + </td> + <td class="text-right">{{executor.resources.cpus | number}}</td> + </tr> + <tr> + <td>GPUs</td> + <td class="text-right"> + N/A + </td> + <td class="text-right">{{executor.resources.gpus | number}}</td> + </tr> + <tr> + <td>Mem</td> + <td class="text-right"> + {{monitor.frameworks[framework.id].executors[executor.id].statistics.mem_rss_bytes | dataSize}} + </td> + <td class="text-right"> + {{executor.resources.mem * (1024 * 1024) | dataSize}} + </td> + </tr> + <tr> + <td>Disk</td> + <td class="text-right"> + {{monitor.frameworks[framework.id].executors[executor.id].statistics.disk_used_bytes | dataSize}}</td> + <td class="text-right"> + {{(executor.resources.disk || 0) * (1024 * 1024) | dataSize}} + </td> + </tr> + </tbody> + </table> + </div> + </div> + + <div class="col-md-9"> + <table m-table table-content="executor.queued_tasks" title="Queued Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="role">Role</th> + <th data-key="resources.cpus">CPUs</th> + <th data-key="resources.gpus">GPUs</th> + <th data-key="resources.mem">Mem</th> + <th data-key="resources.disk">Disk</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="queued_task in $data"> + <td>{{queued_task.id}}</td> + <td>{{queued_task.name}}</td> + <td>{{queued_task.role}}</td> + <td>{{queued_task.resources.cpus | number}}</td> + <td>{{queued_task.resources.gpus | number}}</td> + <td>{{queued_task.resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{queued_task.resources.disk * (1024 * 1024) | dataSize}}</td> + </tr> + </tbody> + </table> + + <table m-table table-content="executor.tasks" title="Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="role">Role</th> + <th data-key="state">State</th> + <th data-key="healthy">Health</th> + <th data-key="resources.cpus">CPUs (allocated)</th> + <th data-key="resources.gpus">GPUs (allocated)</th> + <th data-key="resources.mem">Mem (allocated)</th> + <th data-key="resources.disk">Disk (allocated)</th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="task in $data"> + <td>{{task.id}}</td> + <td>{{task.name}}</td> + <td>{{task.role}}</td> + <td>{{task.state}}</td> + <td class="task-{{task.healthy | taskHealth}}">{{task.healthy | taskHealth}}</td> + <td>{{task.resources.cpus | number}}</td> + <td>{{task.resources.gpus | number}}</td> + <td>{{task.resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{task.resources.disk * (1024 * 1024) | dataSize}}</td> + <td> + <a href="{{'#/agents/' + agent_id + '/browse?path=' + + encodeURIComponent(task.directory)}}"> + Sandbox + </a> + </td> + </tr> + </tbody> + </table> + + <table m-table table-content="executor.completed_tasks" title="Completed Tasks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="role">Role</th> + <th data-key="state">State</th> + <th data-key="resources.cpus">CPUs (allocated)</th> + <th data-key="resources.gpus">GPUs (allocated)</th> + <th data-key="resources.mem">Mem (allocated)</th> + <th data-key="resources.disk">Disk (allocated)</th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="completed_task in $data"> + <td>{{completed_task.id}}</td> + <td>{{completed_task.name}}</td> + <td>{{completed_task.role}}</td> + <td>{{completed_task.state}}</td> + <td>{{completed_task.resources.cpus | number}}</td> + <td>{{completed_task.resources.gpus | number}}</td> + <td>{{completed_task.resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{completed_task.resources.disk * (1024 * 1024) | dataSize}}</td> + <td> + <a href="{{'#/agents/' + agent_id + '/browse?path=' + + encodeURIComponent(completed_task.directory)}}"> + Sandbox + </a> + </td> + </tr> + </tbody> + </table> + </div> + +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/agents/agent-framework.html ---------------------------------------------------------------------- diff --git a/src/webui/app/agents/agent-framework.html b/src/webui/app/agents/agent-framework.html new file mode 100644 index 0000000..06f1697 --- /dev/null +++ b/src/webui/app/agents/agent-framework.html @@ -0,0 +1,164 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#">Master</a> + </li> + <li> + <a class="badge badge-type" href="#/agents/{{agent_id}}" title="{{agent_id}}"> + Agent</a> + </li> + <li class="active"> + <span class="badge badge-type">Framework</span> + {{framework.id}} + </li> +</ol> + +<div class="alert alert-error hidden" id="alert"> + <button class="close" data-dismiss="alert">Ã</button> + <strong>{{alert_message}}</strong> +</div> + +<div class="row" id="agent"> + <div class="col-md-3"> + <div class="well"> + <dl class="inline clearfix"> + <dt>Name:</dt><dd>{{framework.name}}</dd> + <dt>Master:</dt><dd>{{state.master_hostname}}</dd> + <!-- TODO(bmahler): Consider having a break between each role + in order to increase readability. Also, this doesn't + display well when there are a lot of roles (e.g. a large + organization with a lot of teams & services, using roles + like /engineering/frontend/webserver, etc). --> + <dt>Roles:</dt><dd>{{framework.roles.toString()}}</dd> + </dl> + + <dl class="inline clearfix"> + <dt>Active Tasks:</dt> + <dd>{{framework.num_tasks | number}}</dd> + </dl> + + <h4>Resources</h4> + <table class="table table-condensed"> + <thead> + <tr> + <td></td> + <td class="text-right">Used</td> + <td class="text-right">Allocated</td> + </tr> + </thead> + <tbody> + <tr> + <td>CPUs</td> + <td class="text-right"> + {{monitor.frameworks[framework.id].statistics.cpus_total_usage | number}} + </td> + <td class="text-right">{{framework.cpus | number}}</td> + </tr> + <tr> + <td>GPUs</td> + <td class="text-right"> + N/A + </td> + <td class="text-right">{{framework.gpus | number}}</td> + </tr> + <tr> + <td>Memory</td> + <td class="text-right"> + {{monitor.frameworks[framework.id].statistics.mem_rss_bytes | dataSize}} + </td> + <td class="text-right"> + {{framework.mem * (1024 * 1024) | dataSize}} + </td> + </tr> + <tr> + <td>Disk</td> + <td class="text-right">-</td> + <td class="text-right"> + {{(framework.disk || 0) * (1024 * 1024) | dataSize}} + </td> + </tr> + </tbody> + </table> + </div> + </div> + + <div class="col-md-9"> + <table m-table table-content="framework.executors" title="Executors" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="source">Source</th> + <th data-key="role">Role</th> + <th data-key="tasks.length">Active Tasks</th> + <th data-key="queued_tasks.length">Queued Tasks</th> + <th data-key="resources.cpus">CPUs (Used / Allocated)</th> + <th data-key="resources.gpus">GPUs (Used / Allocated)</th> + <th data-key="resources.mem">Mem (Used / Allocated)</th> + <th data-key="resources.disk">Disk (Used / Allocated)</th> + <th></th> + </tr> + </thead> + <tbody> + <tr ng-repeat="executor in $data"> + <td> + <a href="{{'#/agents/' + agent_id + '/frameworks/' + framework.id + '/executors/' + executor.id}}"> + {{executor.id}} + </a> + </td> + <td>{{executor.name}}</td> + <td>{{executor.source}}</td> + <td>{{executor.role}}</td> + <td>{{executor.tasks.length | number}}</td> + <td>{{executor.queued_tasks.length | number}}</td> + <td>{{monitor.frameworks[framework.id].executors[executor.id].statistics.cpus_total_usage | number}} / + {{executor.resources.cpus | number}}</td> + <!-- TODO(haosdent): We need to show statistics for gpu once it is provided in monitor endpoint. --> + <td>N/A</td> + <td>{{monitor.frameworks[framework.id].executors[executor.id].statistics.mem_rss_bytes | dataSize}} / + {{executor.resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{monitor.frameworks[framework.id].executors[executor.id].statistics.disk_used_bytes | dataSize}} / + {{executor.resources.disk * (1024 * 1024) | dataSize}}</td> + <td> + <a href="{{'#/agents/' + agent_id + '/browse?path=' + + encodeURIComponent(executor.directory)}}"> + Sandbox + </a> + </td> + </tr> + </tbody> + </table> + + <table m-table table-content="framework.completed_executors" title="Completed Executors" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="name">Name</th> + <th data-key="source">Source</th> + <th data-key="role">Role</th> + <th data-key="sandbox">Sandbox</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="completed_executor in $data"> + <td> + <a href="{{'#/agents/' + agent_id + '/frameworks/' + framework.id + '/executors/' + completed_executor.id}}"> + {{completed_executor.id}} + </a> + </td> + <td>{{completed_executor.name}}</td> + <td>{{completed_executor.source}}</td> + <td>{{completed_executor.role}}</td> + <td> + <a href="{{'#/agents/' + agent_id + '/browse?path=' + + encodeURIComponent(completed_executor.directory)}}"> + browse + </a> + </td> + </tr> + </tbody> + </table> + </div> + +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/agents/agent.html ---------------------------------------------------------------------- diff --git a/src/webui/app/agents/agent.html b/src/webui/app/agents/agent.html new file mode 100644 index 0000000..a101a93 --- /dev/null +++ b/src/webui/app/agents/agent.html @@ -0,0 +1,293 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#">Master</a> + </li> + <li class="active"> + <span class="badge badge-type">Agent</span> + {{agent_id}} + </li> +</ol> + +<div class="alert alert-error hidden" id="alert"> + <button class="close" data-dismiss="alert">Ã</button> + <strong>{{alert_message}}</strong> +</div> + +<div class="row" id="agent"> + <div class="col-md-3"> + <div class="well"> + <dl class="inline clearfix"> + <dt>Cluster:</dt> + <dd> + <span ng-show="clusterNamed">{{cluster}}</span> + <span ng-show="!clusterNamed"> + (Unnamed) + <i class="icon-info-sign" + tooltip="To name this cluster, set the --cluster flag when starting the master." + tooltip-placement="right"></i> + </span> + </dd> + <dt>Agent:</dt> + <dd>{{state.hostname}}</dd> + <dt>Version:</dt> + <dd>{{state.version}}</dd> + <dt>Built:</dt> + <dd> + <m-timestamp value="{{state.build_time * 1000}}"></m-timestamp> + </dd> + <dt>Started:</dt> + <dd> + <m-timestamp value="{{state.start_time * 1000}}"></m-timestamp> + </dd> + <dt>Master:</dt> + <dd>{{state.master_hostname}}</dd> + </dl> + + <p ng-if="agent.log_file_attached"> + <b>Agent Log:</b> + <span class="btn-group"> + <!-- Links can look like buttons using Bootstrap classes. --> + <a class="btn btn-xs btn-default" href="{{agent.url_prefix}}/files/download?path=/slave/log"> + Download + </a> + <button class="btn btn-xs btn-default" ng-click="streamLogs($event)"> + View + </button> + </span> + </p> + + <h4>Tasks</h4> + <table class="table table-condensed"> + <tbody> + <tr> + <td>Staging</td> + <td class="text-right">{{staging_tasks | number}}</td> + </tr> + <tr> + <td>Starting</td> + <td class="text-right">{{starting_tasks | number}}</td> + </tr> + <tr> + <td>Running</td> + <td class="text-right">{{running_tasks | number}}</td> + </tr> + <tr> + <td>Killing</td> + <td class="text-right">{{killing_tasks | number}}</td> + </tr> + <tr> + <td>Finished</td> + <td class="text-right">{{finished_tasks | number}}</td> + </tr> + <tr> + <td>Killed</td> + <td class="text-right">{{killed_tasks | number}}</td> + </tr> + <tr> + <td>Failed</td> + <td class="text-right">{{failed_tasks | number}}</td> + </tr> + <tr> + <td>Lost</td> + <td class="text-right">{{lost_tasks | number}}</td> + </tr> + </tbody> + </table> + + <h4>Resources</h4> + <table class="table table-condensed"> + <thead> + <tr> + <td></td> + <td class="text-right">Used</td> + <td class="text-right">Allocated</td> + <td class="text-right">Available</td> + <td class="text-right">Total</td> + </tr> + </thead> + <tbody> + <tr> + <td>CPUs</td> + <td class="text-right"> + {{monitor.statistics.cpus_total_usage | number}} + </td> + <td class="text-right"> + {{state.allocated_resources.cpus | number}} + </td> + <td class="text-right"> + {{state.resources.cpus - state.allocated_resources.cpus | number}} + </td> + <td class="text-right"> + {{state.resources.cpus | number}} + </td> + </tr> + <tr> + <td>GPUs</td> + <td class="text-right"> + N/A + </td> + <td class="text-right"> + {{state.allocated_resources.gpus | number}} + </td> + <td class="text-right"> + {{state.resources.gpus - state.allocated_resources.gpus | number}} + </td> + <td class="text-right"> + {{state.resources.gpus | number}} + </td> + </tr> + <tr> + <td>Memory</td> + <td class="text-right"> + {{monitor.statistics.mem_rss_bytes | dataSize}} + </td> + <td class="text-right"> + {{state.allocated_resources.mem * (1024 * 1024) | dataSize}} + </td> + <td class="text-right"> + {{(state.resources.mem - state.allocated_resources.mem) * (1024 * 1024) | dataSize}} + </td> + <td class="text-right"> + {{state.resources.mem * (1024 * 1024) | dataSize}} + </td> + </tr> + <tr> + <td>Disk</td> + <td class="text-right"> + {{monitor.statistics.disk_used_bytes | dataSize}} + </td> + <td class="text-right"> + {{state.allocated_resources.disk * (1024 * 1024) | dataSize}} + </td> + <td class="text-right"> + {{(state.resources.disk - state.allocated_resources.disk) * (1024 * 1024) | dataSize}} + </td> + <td class="text-right"> + {{state.resources.disk * (1024 * 1024) | dataSize}} + </td> + </tr> + </tbody> + </table> + </div> + + </div> + <div class="col-md-9"> + <table m-table table-content="agent.reserved_resources_as_array" title="Resource Reservations" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="role">Reservation Role</th> + <th data-key="cpus">CPUs (Allocated / Total)</th> + <th data-key="gpus">GPUs (Allocated / Total)</th> + <th data-key="mem">Mem (Allocated / Total)</th> + <th data-key="disk">Disk (Allocated / Total)</th> + </tr> + </thead> + <tbody> + <tr> + <td><em>Unreserved</em></td> + <td>{{state.unreserved_resources_allocated.cpus | number}} / {{state.unreserved_resources.cpus | number}}</td> + <td>{{state.unreserved_resources_allocated.gpus | number}} / {{state.unreserved_resources.gpus | number}}</td> + <td>{{state.unreserved_resources_allocated.mem * (1024 * 1024) | dataSize}} / {{state.unreserved_resources.mem * (1024 * 1024) | dataSize}}</td> + <td>{{state.unreserved_resources_allocated.disk * (1024 * 1024) | dataSize}} / {{state.unreserved_resources.disk * (1024 * 1024) | dataSize}}</td> + </tr> + <tr ng-repeat="reservation in $data"> + <td>{{reservation.role}}</td> + <td>{{(state.reserved_resources_allocated[reservation.role].cpus || 0) | number}} / {{reservation.cpus | number}}</td> + <td>{{(state.reserved_resources_allocated[reservation.role].gpus || 0) | number}} / {{reservation.gpus | number}}</td> + <td>{{(state.reserved_resources_allocated[reservation.role].mem * (1024 * 1024) || 0) | dataSize}} / {{reservation.mem * (1024 * 1024) | dataSize}}</td> + <td>{{(state.reserved_resources_allocated[reservation.role].disk * (1024 * 1024) || 0) | dataSize}} / {{reservation.disk * (1024 * 1024) | dataSize}}</td> + </tr> + </tbody> + </table> + + <table m-table table-content="agent.frameworks" title="Frameworks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="user">User</th> + <th data-key="name">Name</th> + <th data-key="roles">Roles</th> + <th data-key="num_tasks">Active Tasks</th> + <th data-key="cpus">CPUs (Used / Allocated)</th> + <th data-key="gpus">GPUs (Used / Allocated)</th> + <th data-key="mem">Mem (Used / Allocated)</th> + <th data-key="disk">Disk (Used / Allocated)</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="framework in $data"> + <td> + <a href="{{'#/agents/' + agent_id + '/frameworks/' + framework.id}}"> + {{(framework.id | truncateMesosID) || framework.name}}</a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{framework.id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td>{{framework.user}}</td> + <td>{{framework.name}}</td> + <!-- TODO(bmahler): This doesn't display well when there are a lot + of roles (e.g. a large organization with a lot of teams & + services, using roles like /engineering/frontend/webserver, etc). + Figure out a way to display this without bloating the table. --> + <td>{{framework.roles.toString()}}</td> + <td>{{framework.num_tasks | number}}</td> + <td>{{monitor.frameworks[framework.id].statistics.cpus_total_usage | number}} / {{framework.cpus | number}}</td> + <!-- TODO(haosdent): We need to show statistics for gpu once it is provided in monitor endpoint. --> + <td>N/A</td> + <td>{{monitor.frameworks[framework.id].statistics.mem_rss_bytes | dataSize}} / {{framework.mem * (1024 * 1024) | dataSize}}</td> + <td>{{monitor.frameworks[framework.id].statistics.disk_used_bytes | dataSize}} / {{framework.disk * (1024 * 1024) | dataSize}}</td> + </tr> + </tbody> + </table> + + <table m-table table-content="agent.completed_frameworks" title="Completed Frameworks" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="user">User</th> + <th data-key="name">Name</th> + <th data-key="roles">Roles</th> + <th data-key="tasks.length">Active Tasks</th> + <th data-key="resources.cpus">CPUs</th> + <th data-key="resources.gpus">GPUs</th> + <th data-key="resources.mem">Mem</th> + <th data-key="resources.disk">Disk</th> + </tr> + </thead> + <tbody> + <tr ng-repeat="completed_framework in $data"> + <td> + <a href="{{'#/agents/' + agent_id + '/frameworks/' + completed_framework.id}}"> + {{completed_framework.id | truncateMesosID}}</a> + <button class="btn btn-xs btn-toggle btn-default" + clipboard + data-clipboard-text="{{framework.id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + </button> + </td> + <td>{{completed_framework.user}}</td> + <td>{{completed_framework.name}}</td> + <!-- TODO(bmahler): This doesn't display well when there are a lot + of roles (e.g. a large organization with a lot of teams & + services, using roles like /engineering/frontend/webserver, etc). + Figure out a way to display this without bloating the table. --> + <td>{{completed_framework.roles.toString()}}</td> + <td>{{completed_framework.num_tasks | number}}</td> + <td>{{completed_framework.cpus | number}}</td> + <td>{{completed_framework.gpus | number}}</td> + <td>{{completed_framework.mem * (1024 * 1024) | dataSize}}</td> + <td>{{completed_framework.disk * (1024 * 1024) | dataSize}}</td> + </tr> + </tbody> + </table> + </div> +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/agents/agents.html ---------------------------------------------------------------------- diff --git a/src/webui/app/agents/agents.html b/src/webui/app/agents/agents.html new file mode 100644 index 0000000..98712c6 --- /dev/null +++ b/src/webui/app/agents/agents.html @@ -0,0 +1,56 @@ +<ol class="breadcrumb"> + <li> + <a class="badge badge-type" href="#">Master</a> + </li> + <li class="active"> + <span class="badge badge-type">Agents</span> + </li> +</ol> + +<table m-table table-content="agents" title="Agents" + class="table table-striped table-bordered table-condensed"> + <thead> + <tr> + <th data-key="id">ID</th> + <th data-key="hostname">Host</th> + <th data-key="resources.cpus">CPUs (Allocated / Total)</th> + <th data-key="resources.gpus">GPUs (Allocated / Total)</th> + <th data-key="resources.mem">Mem (Allocated / Total)</th> + <th data-key="resources.disk">Disk (Allocated / Total)</th> + <th data-key="registered_time">Registered</th> + <th data-key="reregistered_time">Re-Registered</th> + </tr> + </thead> + <tr ng-repeat="agent in $data"> + <td> + <a href="#/agents/{{agent.id}}">{{agent.id | truncateMesosID}}</a> + <button class="btn btn-xs btn-default btn-toggle" + clipboard + data-clipboard-text="{{agent.id}}" + tooltip="Copy ID" + tooltip-placement="right" + tooltip-trigger="clipboardhover"> + <i class="icon-file"></i> + </button> + </td> + <td>{{agent.hostname}}</td> + <td> + {{agent.used_resources.cpus | number}} / {{agent.resources.cpus | number}} + </td> + <td> + {{agent.used_resources.gpus | number}} / {{agent.resources.gpus | number}} + </td> + <td> + {{agent.used_resources.mem * (1024 * 1024) | dataSize}} / {{agent.resources.mem * (1024 * 1024) | dataSize}} + </td> + <td> + {{agent.used_resources.disk * (1024 * 1024) | dataSize}} / {{agent.resources.disk * (1024 * 1024) | dataSize}} + </td> + <td> + <m-timestamp value="{{agent.registered_time * 1000}}"></m-timestamp> + </td> + <td> + <m-timestamp value="{{agent.reregistered_time * 1000}}"></m-timestamp> + </td> + </tr> +</table> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/app.js ---------------------------------------------------------------------- diff --git a/src/webui/app/app.js b/src/webui/app/app.js new file mode 100644 index 0000000..f6f1138 --- /dev/null +++ b/src/webui/app/app.js @@ -0,0 +1,371 @@ +// 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. + +(function() { + 'use strict'; + + angular.module('mesos', ['ngRoute', 'mesos.services', 'ui.bootstrap', 'ui.bootstrap.dialog']). + config(['paginationConfig', '$routeProvider', function(paginationConfig, $routeProvider) { + $routeProvider + .when('/', + {templateUrl: 'app/home.html', controller: 'HomeCtrl'}) + .when('/agents', + {templateUrl: 'app/agents/agents.html', controller: 'AgentsCtrl'}) + .when('/agents/:agent_id', + {templateUrl: 'app/agents/agent.html', controller: 'AgentCtrl'}) + .when('/agents/:agent_id/frameworks/:framework_id', + {templateUrl: 'app/agents/agent-framework.html', controller: 'AgentFrameworkCtrl'}) + .when('/agents/:agent_id/frameworks/:framework_id/executors/:executor_id', + {templateUrl: 'app/agents/agent-executor.html', controller: 'AgentExecutorCtrl'}) + .when('/frameworks', + {templateUrl: 'app/frameworks/frameworks.html', controller: 'FrameworksCtrl'}) + .when('/frameworks/:id', + {templateUrl: 'app/frameworks/framework.html', controller: 'FrameworkCtrl'}) + .when('/maintenance', + {templateUrl: 'app/maintenance/maintenance.html', controller: 'MaintenanceCtrl'}) + .when('/offers', + {templateUrl: 'app/offers/offers.html', controller: 'OffersCtrl'}) + .when('/roles', + {templateUrl: 'app/roles/roles.html', controller: 'RolesCtrl'}) + + // TODO(tomxing): Remove the following '/slaves/*' paths once the + // slave->agent rename is complete(MESOS-3779). + .when('/slaves', {redirectTo: '/agents'}) + .when('/slaves/:agent_id', {redirectTo: '/agents/:agent_id'}) + .when('/slaves/:agent_id/frameworks/:framework_id', + {redirectTo: '/agents/:agent_id/frameworks/:framework_id'}) + .when('/slaves/:agent_id/frameworks/:framework_id/executors/:executor_id', + {redirectTo: '/agents/:agent_id/frameworks/:framework_id/executors/:executor_id'}) + + // Use a non-falsy template so the controller will still be executed. + // Since the controller is intended only to redirect, the blank template + // is fine. + // + // By design, controllers currently will not handle routes if the + // template is falsy. There is an issue open in Angular to add that + // feature: + // + // https://github.com/angular/angular.js/issues/1838 + .when('/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/browse', + {template: ' ', controller: 'AgentTaskAndExecutorRerouterCtrl'}) + .when('/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/tasks/:task_id/browse', + {template: ' ', controller: 'AgentTaskAndExecutorRerouterCtrl'}) + .when('/agents/:agent_id/browse', + {templateUrl: 'app/agents/agent-browse.html', controller: 'BrowseCtrl'}) + + // TODO(tomxing): Remove the following '/slaves/*' paths once the + // slave->agent rename is complete(MESOS-3779). + .when('/slaves/:agent_id/frameworks/:framework_id/executors/:executor_id/browse', + {redirectTo: '/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/browse'}) + .when('/slaves/:agent_id/browse', + {redirectTo: '/agents/:agent_id/browse'}) + .otherwise({redirectTo: '/'}); + + // Configure [Angular UI Pagination][1]: + // * Show first/last buttons + // * Show 50 items per page + // * Show "..." when there are pages beyond the max shown + // + // [1] http://angular-ui.github.io/bootstrap/#/pagination + paginationConfig.boundaryLinks = true; + paginationConfig.rotate = false; + }]) + .filter('truncateMesosID', function() { + // Returns a truncated ID, for example: + // Input: 9d4b2f2b-a759-4458-bebf-7d3507a6f0ca-S9 + // Output: ...7d3507a6f0ca-S9 + // + // Note that an ellipsis is used for display purposes. + return function(id) { + if (id) { + var truncatedIdParts = id.split('-'); + + if (truncatedIdParts.length > 4) { + return '\u2026' + truncatedIdParts.splice(4).join('-'); + } else { + return id; + } + } else { + return ''; + } + }; + }) + .filter('truncateMesosState', function() { + return function(state) { + // Remove the "TASK_" prefix. + return state.substring(5); + }; + }) + .filter('taskHealth', function() { + return function(healthy) { + if (healthy == null) { + return "-"; + } + + // Note that this string value is relied on to match + // against CSS classes to color the UI. Changing this + // also requires an update to the CSS. + return healthy ? "healthy" : "unhealthy"; + } + }) + .filter('isoDate', function($filter) { + return function(date) { + var i = parseInt(date, 10); + if (_.isNaN(i)) { return '' } + return $filter('date')(i, 'yyyy-MM-ddTHH:mm:ssZ'); + }; + }) + .filter('relativeDate', function() { + return function(date, refDate) { + var i = parseInt(date, 10); + if (_.isNaN(i)) { return '' } + return relativeDate(i, refDate); + }; + }) + .filter('slice', function() { + return function(array, begin, end) { + if (_.isArray(array)) { + return array.slice(begin, end); + } + }; + }) + .filter('unixDate', function($filter) { + return function(date) { + if ((new Date(date)).getFullYear() == (new Date()).getFullYear()) { + return $filter('date')(date, 'MMM dd HH:mm'); + } else { + return $filter('date')(date, 'MMM dd yyyy'); + } + }; + }) + // A filter that uses to convert small float number to decimal string. + .filter('decimalFloat', function() { + return function(num) { + return num ? parseFloat(num.toFixed(4)).toString() : num; + } + }) + .filter('dataSize', function() { + var BYTES_PER_KB = Math.pow(2, 10); + var BYTES_PER_MB = Math.pow(2, 20); + var BYTES_PER_GB = Math.pow(2, 30); + var BYTES_PER_TB = Math.pow(2, 40); + var BYTES_PER_PB = Math.pow(2, 50); + // NOTE: Number.MAX_SAFE_INTEGER is 2^53 - 1 + + return function(bytes) { + if (bytes == null || isNaN(bytes)) { + return ''; + } else if (bytes < BYTES_PER_KB) { + return bytes.toFixed() + ' B'; + } else if (bytes < BYTES_PER_MB) { + return (bytes / BYTES_PER_KB).toFixed() + ' KB'; + } else if (bytes < BYTES_PER_GB) { + return (bytes / BYTES_PER_MB).toFixed() + ' MB'; + } else if (bytes < BYTES_PER_TB) { + return (bytes / BYTES_PER_GB).toFixed(1) + ' GB'; + } else if (bytes < BYTES_PER_PB) { + return (bytes / BYTES_PER_TB).toFixed(1) + ' TB'; + } else { + return (bytes / BYTES_PER_PB).toFixed(1) + ' PB'; + } + }; + }) + .directive('clipboard', [function() { + return { + restrict: 'A', + scope: true, + template: '<i class="glyphicon glyphicon-file"></i>', + + link: function(scope, element, _attrs) { + var clip = new Clipboard(element[0]); + + element.on('mouseenter', function() { + element.addClass('clipboard-is-hover'); + element.triggerHandler('clipboardhover'); + }); + + element.on('mouseleave', function() { + // Restore tooltip content to its original value if it was + // changed by this Clipboard instance. + if (scope && scope.tt_content_orig) { + scope.tt_content = scope.tt_content_orig; + delete scope.tt_content_orig; + } + + element.removeClass('clipboard-is-hover'); + element.triggerHandler('clipboardhover'); + }); + + // Success for browsers with `execCommand` support. + clip.on('success', function () { + // Store the tooltip's original content so it can + // be restored when the tooltip is hidden. + scope.tt_content_orig = scope.tt_content; + + // Angular UI's Tooltip sets content on the element's scope in a + // variable named 'tt_content'. The Tooltip has no public interface, + // so set the value directly here to change the value of the tooltip + // when content is successfully copied. + scope.tt_content = 'Copied!'; + scope.$apply(); + }); + + // Support for all other browsers without `execCommand` + // support. Text will be selected and user will be prompted + // to copy. + clip.on('error', function() { + scope.tt_content_orig = scope.tt_content; + scope.tt_content = 'Press Ctrl/Cmd + C to copy!'; + scope.$apply(); + }); + } + }; + }]) + .directive('mTimestamp', [ '$rootScope', function($rootScope) { + return { + restrict: 'E', + transclude: true, + scope: { + value: '@' + }, + link: function($scope, _element, _attrs) { + $scope.longDate = JSON.parse( + localStorage.getItem('longDate') || false); + + $scope.$on('mTimestamp.toggle', function() { + $scope.longDate = !$scope.longDate; + }); + + $scope.toggle = function() { + localStorage.setItem('longDate', !$scope.longDate); + $rootScope.$broadcast('mTimestamp.toggle'); + }; + }, + templateUrl: 'app/shared/timestamp.html' + } + }]) + .directive('mPagination', function() { + return { templateUrl: 'app/shared/pagination.html' } + }) + .directive('mTableHeader', function() { + return { templateUrl: 'app/shared/table-header.html' } + }) + .directive('mTable', ['$compile', '$filter', function($compile, $filter) { + /* This directive does not have a template. The DOM doesn't like + * having partially defined tables and so they don't work well with + * directives and templates. Because of this, the sub-elements that this + * includes are their own directive/templates and it adds them via. DOM + * manipulation here. + */ + return { + scope: true, + link: function(scope, element, attrs) { + var defaultOrder = true; + + _.extend(scope, { + originalData: [], + columnKey: '', + sortOrder: defaultOrder, + pgNum: 1, + pageLength: 50, + filterTerm: '', + headerTitle: attrs.title + }) + // --- + + // --- Allow sorting by column based on the <th> data-key attribute. + // Does not apply for group columns as their children are sortable. + var th = element.find('th').not('.group-column'); + th.attr('ng-click', 'sortColumn($event)'); + $compile(th)(scope); + + var setSorting = function(el) { + var key = el.attr('data-key'); + + // Prevent sorting when 'data-key' is undefined. + if (!key) { + return; + } + + if (scope.columnKey === key) { + scope.sortOrder = !scope.sortOrder; + } else if (el.hasClass('ascending')) { + // We can order the table the other way around by adding + // 'class="ascending"' to the table header. + scope.sortOrder = !defaultOrder; + } else { + scope.sortOrder = defaultOrder; + } + + scope.columnKey = key; + + th.removeClass('descending ascending'); + el.addClass(scope.sortOrder ? 'descending' : 'ascending'); + }; + + var defaultSortColumn = function() { + var el = element.find('[data-sort]'); + if (el.length === 0) { + el = element.find('th:first'); + } + return el; + }; + + scope.sortColumn = function(ev) { + setSorting(angular.element(ev.target)); + }; + + setSorting(defaultSortColumn()); + // --- + + scope.$watch(attrs.tableContent, function(data) { + if (!data) { scope.originalData = []; return } + if (angular.isObject(data)) { data = _.values(data) } + + scope.originalData = data; + }); + + var setTableData = function() { + scope.filteredData = $filter('filter')(scope.originalData, scope.filterTerm) + scope.$data = $filter('orderBy')( + scope.filteredData, + scope.columnKey, + scope.sortOrder).slice( + (scope.pgNum - 1) * scope.pageLength, + scope.pgNum * scope.pageLength); + }; + + // Reset the page number for each new filtering. + scope.$watch('filterTerm', function() { scope.pgNum = 1; }); + + _.each(['originalData', 'columnKey', 'sortOrder', 'pgNum', 'filterTerm'], + function(k) { scope.$watch(k, setTableData); }); + + // --- Pagination controls + var el = angular.element('<div m-pagination></div>'); + $compile(el)(scope); + element.after(el); + // --- + + // --- Filtering + el = angular.element('<div m-table-header></div>'); + $compile(el)(scope); + element.before(el); + // --- + } + }; + }]); +})();