diff --git a/common/models/project_model.py b/common/models/project_model.py index 3721dbd..8415684 100644 --- a/common/models/project_model.py +++ b/common/models/project_model.py @@ -7,7 +7,6 @@ class ProjectModel(db.Document): user = db.ReferenceField(UserModel, reverse_delete_rule=db.CASCADE, required=True) name = db.StringField(max_length=300, required=True) - created_at = db.DateTimeField(default=datetime.datetime.now) def to_smt_json(self, request): from ..models import RecordModel @@ -15,7 +14,22 @@ def to_smt_json(self, request): records = [r.to_json() for r in query] return json.dumps({'project' : self.name, 'url' : request.url, 'records' : records}) - def _count(self): + @property + def record_count(self): + return self.records.count() + + @property + def records(self): from ..models import RecordModel - return RecordModel.objects(project=self).count() + return RecordModel.objects(project=self) + + @property + def last_updated(self): + return self.records.order_by('-last_updated').limit(1).first().last_updated + + @property + def duration(self): + return self.records.sum('duration') + + diff --git a/common/models/record_model.py b/common/models/record_model.py index 89845e8..d891ea4 100644 --- a/common/models/record_model.py +++ b/common/models/record_model.py @@ -12,8 +12,12 @@ class RecordModel(db.Document): possible_status = ["crashed", "unknown", "started", "running", "finished"] status = db.StringField(default="unknown", choices=possible_status) tags = db.ListField(db.StringField(max_length=100)) - created_at = db.DateTimeField(default=datetime.datetime.now) + last_updated = db.DateTimeField(default=datetime.datetime.now()) + def save(self, *args, **kwargs): + self.last_updated = datetime.datetime.now() + return super(RecordModel, self).save(*args, **kwargs) + def update_fields(self, data): for k, v in self._fields.iteritems(): if not v.required: @@ -23,8 +27,12 @@ def update_fields(self, data): def update(self, data): for k, v in self.update_fields(data): if k in data.keys(): - setattr(self, k, data[k]) + if k == 'timestamp': + self.timestamp = datetime.datetime.strptime(data[k], '%Y-%m-%d %X') + else: + setattr(self, k, data[k]) del data[k] + self.save() if data: body, created = RecordBodyModel.objects.get_or_create(head=self) diff --git a/common/models/user_model.py b/common/models/user_model.py index 93f3e4e..dcb99b6 100644 --- a/common/models/user_model.py +++ b/common/models/user_model.py @@ -1,6 +1,6 @@ import datetime from ..core import db -import flask as fk + class UserModel(db.Document): created_at = db.DateTimeField(default=datetime.datetime.now) @@ -24,5 +24,25 @@ def get_id(self): except NameError: return str(self.id) # python 3 - - + @property + def projects(self): + from common.models import ProjectModel + return ProjectModel.objects(user=self) + + @property + def record_count(self): + return sum([p.record_count for p in self.projects]) + + @property + def records(self): + records = [] + for project in self.projects: + records += project.records + return records + + @property + def duration(self): + return sum([p.duration for p in self.projects]) + + + diff --git a/config.py b/config.py index dfab209..5464c3a 100644 --- a/config.py +++ b/config.py @@ -8,6 +8,8 @@ APP_TITLE = 'Sumatra Cloud' +VERSION = '0.1-dev' + OPENID_PROVIDERS = [ {'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'}, {'name': 'MyOpenID', 'url': 'https://www.myopenid.com'}] diff --git a/smt_view/__init__.py b/smt_view/__init__.py index e23b513..7f29dd5 100644 --- a/smt_view/__init__.py +++ b/smt_view/__init__.py @@ -22,5 +22,6 @@ login_manager.login_view = 'login_view' openid = OpenID(app, os.path.join(basedir, 'tmp')) -import views +from . import views from common import models +from . import filters diff --git a/smt_view/filters.py b/smt_view/filters.py new file mode 100644 index 0000000..cf24b85 --- /dev/null +++ b/smt_view/filters.py @@ -0,0 +1,22 @@ +import datetime +from smt_view import app + +def ff(i, st): + if i == 0: + return + else: + if i == 1: + st = st[:-1] + return '{0} {1}'.format(i, st) + +@app.template_filter('hms') +def _jinja2_filter_hms(time): + td = datetime.timedelta(seconds=time) + days = td.days + hours, remainder = divmod(td.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + ss_ = [ff(i, st) for i, st in [[days, 'days'], [hours, 'hours'], [minutes, 'minutes'], [seconds, 'seconds']]] + ss = [s for s in ss_ if s is not None] + return ', '.join(ss) + + diff --git a/smt_view/static/css/dashboard.css b/smt_view/static/css/dashboard.css deleted file mode 100644 index e0e3632..0000000 --- a/smt_view/static/css/dashboard.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Base structure - */ - -/* Move down content because we have a fixed navbar that is 50px tall */ -body { - padding-top: 50px; -} - - -/* - * Global add-ons - */ - -.sub-header { - padding-bottom: 10px; - border-bottom: 1px solid #eee; -} - -/* - * Top navigation - * Hide default border to remove 1px line. - */ -.navbar-fixed-top { - border: 0; -} - -/* - * Sidebar - */ - -/* Hide for mobile, show later */ -.sidebar { - display: none; -} -@media (min-width: 768px) { - .sidebar { - position: fixed; - top: 51px; - bottom: 0; - left: 0; - z-index: 1000; - display: block; - padding: 20px; - overflow-x: hidden; - overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ - background-color: #f5f5f5; - border-right: 1px solid #eee; - } -} - -/* Sidebar navigation */ -.nav-sidebar { - margin-right: -21px; /* 20px padding + 1px border */ - margin-bottom: 20px; - margin-left: -20px; -} -.nav-sidebar > li > a { - padding-right: 20px; - padding-left: 20px; -} -.nav-sidebar > .active > a, -.nav-sidebar > .active > a:hover, -.nav-sidebar > .active > a:focus { - color: #fff; - background-color: #428bca; -} - - -/* - * Main content - */ - -.main { - padding: 20px; -} -@media (min-width: 768px) { - .main { - padding-right: 40px; - padding-left: 40px; - } -} -.main .page-header { - margin-top: 0; -} - - -/* - * Placeholder dashboard ideas - */ - -.placeholders { - margin-bottom: 30px; - text-align: center; -} -.placeholders h4 { - margin-bottom: 0; -} -.placeholder { - margin-bottom: 20px; -} -.placeholder img { - display: inline-block; - border-radius: 50%; -} diff --git a/smt_view/static/css/psdash.css b/smt_view/static/css/psdash.css new file mode 100644 index 0000000..b29d042 --- /dev/null +++ b/smt_view/static/css/psdash.css @@ -0,0 +1,343 @@ +body, html { + font-family: 'Ubuntu', Arial, sans-serif; + height: 100%; + width: 100%; +} + +a { + color: #10839d; +} +a:hover { + text-decoration: none; + color: #0fa2c2; +} + +#psdash { + height: 100%; + width: 100%; +} + +#psdash .header { +} + +#psdash .header > div { + height: 70px; +} + +#psdash .header .logo a { + color: #ffffff; +} + +#psdash .header .logo { + background-color: #458bbe; + width: 200px; + position: absolute; + text-align: center; +} + +#psdash .header .logo small { + font-size: 12px; +} + +#psdash .header .logo span.app-name { + font-size: 20px; + line-height: 64px; + font-weight: 300; +} + +#psdash .header .top-nav { + width: 100%; + padding-left: 200px; + padding-right: 10px; +} + +#psdash .header .top-nav .dropdown { + padding-top: 18px; + margin-right: 20px; +} + + +#psdash .header .top-nav .host-info { + margin-left: 20px; + padding-top: 8px; + float: left; +} + +#psdash .header .top-nav .host-info .name span.glyphicon { + color: #adadad; + line-height: 24px; + height: 20px; +} + +#psdash .header .top-nav .host-info .name span.hostname { + font-size: 22px; + font-weight: 300; +} + +#psdash .header .top-nav .host-info .info { + color: #777777; + font-size: 12px; +} + +#psdash .header .top-nav .dropdown-text { + color: #898989; + padding: 10px; + font-size: 18px; +} + +#psdash .header .top-nav span.caret { + color: #adadad; +} + +#psdash .table-container { + display: table; + height: 100%; +} + +#psdash .table-container .content { + height: 100%; + padding-top: 70px; + display: table-row; +} + +#psdash .table-container .content > div { + display: table-cell; + vertical-align: top; +} + +#psdash .table-container .content .left-nav { + width: 200px; + background-color: #313443; + color: #ffffff; +} + +#psdash .table-container .content .left-nav ul.menu { + list-style-type: none; + margin: 20px 0 0 0; + padding: 0; +} + +#psdash .table-container .content .left-nav ul.menu li a { + display: block; + width: 200px; + height: 40px; + padding-top: 6px; + padding-left: 20px; + border-left: 4px solid #313443; + color: #adadad; +} + +#psdash .table-container .content .left-nav ul.menu li a span.glyphicon { + font-size: 24px; +} + +#psdash .table-container .content .left-nav ul.menu li a span.option-text { + font-size: 16px; + padding-left: 16px; + line-height: 24px; + vertical-align: 5px; +} + +#psdash .table-container .content .left-nav ul.menu li.active a, #psdash .content .left-nav ul.menu li a:hover { + text-decoration: none; + color: #ffffff; + border-left: 4px solid #d43f3a; +} + +#psdash .table-container .content .main-content { + border-top: 1px solid #c1d0d9; + margin-left: 200px; + background-color: #dee2ea; + width: 100%; + padding: 20px; +} + +#psdash .table-container .content .main-content div.box { + padding: 20px; + background-color: #ffffff; + border-radius: 4px; + border: 1px solid #c2c6ce; + margin-right: 20px; + margin-bottom: 20px; + min-height: 310px; +} + +#dashboard div.box { + float: left; +} + +#dashboard div.box.cpu { + min-width: 300px; +} + +#dashboard div.box.users { + min-width: 500px; +} + +#dashboard div.box.swap { + min-width: 350px; +} + +#dashboard div.box.memory { + min-width: 400px; +} + +#dashboard div.box.network { + min-width: 500px; +} + +#dashboard div.box.disks { + min-width: 500px; +} + +#dashboard div.box table { + margin-bottom: 0; +} + +#dashboard div.box table th { + font-weight: 500; + font-size: 12px; +} + +#psdash .table-container .content .main-content div.box .box-header { + margin-bottom: 10px; +} + +#psdash .table-container .content .main-content div.box .box-header span { + font-size: 20px; + font-weight: 300; + color: #808080; + /* padding: 10px 0; */ +} + +#processes { + width: 100%; +} + +#psdash table th { + /* padding: 13px 6px; */ + font-size: 16px; + font-weight: 300; + /* margin-right: 10px; */ +} + +#psdash table th a { + font-size: 16px; + font-weight: 300; + margin-right: 10px; +} + +#psdash table td { + vertical-align: middle; + font-size: 13px; +} + +#psdash table td a { + font-size: 13px; + font-weight: 300; +} + +#psdash table td small { + font-size: 11px; + color: #868686; +} + +#psdash span.badge { + margin-left: 10px; + border-radius: 2px; + padding: 4px; + font-weight: normal; + background-color: #28a4c9; +} + +#psdash span.badge.all { + background-color: #d43f3a; +} + +#network div.box, #connections div.box, #disks div.box { + width: 100%; +} + +#process { + width: 100%; +} + +#process table tr.skip-border td { + border-top: 0; +} + +#log-content { + height: 700px; + overflow: scroll; + font-size: 12px; + background-color: #222; + color: #eee; + margin-bottom: 0; + border: 0; + border-radius: 0; + overflow-x: hidden; + white-space: pre-wrap; +} + +#log .search-text { + background-color: #2d2d2d; + color: #747474; + border-color: #3d3d3d; +} + +#log span.matching-text { + border: 1px solid #E6E600; + padding: 2px; +} + +#log span.found-text { + background-color: #E6E600; + color: #111; + padding: 2px; +} + +#log .controls { + background-color: #222; + padding: 10px; + color: #eee; +} + +#log .controls .status-text, #log .controls .mode-text { + padding-top: 10px; + font-size: 13px; + font-family: Consolas, "Courier New", monospace; +} + +#log .controls button { + background-color: #458bbe; + border-color: #3b7ba4; +} + +.dataTables_filter { + font-size: 12px; +} + +.dataTables_paginate { + font-size: 12px; +} + +.dataTables_info { + font-size: 12px; + max-width: 170px; + /* display: none; */ +} + +.dataTables_length { + font-size: 12px; +} + +.form-inline .form-control { + display: inline-block; + width: auto; + max-width: 100px; + vertical-align: middle; +} + + + + diff --git a/smt_view/static/js/psdash.js b/smt_view/static/js/psdash.js new file mode 100644 index 0000000..0933806 --- /dev/null +++ b/smt_view/static/js/psdash.js @@ -0,0 +1,161 @@ + +function escape_regexp(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); +} + +function replace_all(find, replace, str) { + return str.replace(new RegExp(escape_regexp(find), 'g'), replace); +} + +function init_log() { + var $log = $("#log"); + function scroll_down($el) { + $el.scrollTop($el[0].scrollHeight); + } + + function read_log() { + var $el = $("#log-content"); + var mode = $el.data("mode"); + if(mode != "tail") { + return; + } + + $.get($log.data("read-log-url"), function (resp) { + // only scroll down if the scroll is already at the bottom. + if(($el.scrollTop() + $el.innerHeight()) >= $el[0].scrollHeight) { + $el.append(resp); + scroll_down($el); + } else { + $el.append(resp); + } + }); + } + + function exit_search_mode() { + var $el = $("#log-content"); + $el.data("mode", "tail"); + var $controls = $("#log").find(".controls"); + $controls.find(".mode-text").text("Tail mode (Press s to search)"); + $controls.find(".status-text").hide(); + + $.get($log.data("read-log-tail-url"), function (resp) { + $el.text(resp); + scroll_down($el); + $("#search-input").val("").blur(); + }); + } + + $("#scroll-down-btn").click(function() { + scroll_down($el); + }); + + $("#search-form").submit(function(e) { + e.preventDefault(); + + var val = $("#search-input").val(); + if(!val) return; + + var $el = $("#log-content"); + var filename = $el.data("filename"); + var params = { + "filename": filename, + "text": val + }; + + $el.data("mode", "search"); + $("#log").find(".controls .mode-text").text("Search mode (Press enter for next, escape to exit)"); + + $.get($log.data("search-log-url"), params, function (resp) { + var $log = $("#log"); + $log.find(".controls .status-text").hide(); + $el.find(".found-text").removeClass("found-text"); + + var $status = $log.find(".controls .status-text"); + + if(resp.position == -1) { + $status.text("EOF Reached."); + } else { + // split up the content on found pos. + var content_before = resp.content.slice(0, resp.buffer_pos); + var content_after = resp.content.slice(resp.buffer_pos + params["text"].length); + + // escape html in log content + resp.content = $('
').text(resp.content).html(); + + // highlight matches + var matched_text = '' + params['text'] + ''; + var found_text = '' + params["text"] + ''; + content_before = replace_all(params["text"], matched_text, content_before); + content_after = replace_all(params["text"], matched_text, content_after); + resp.content = content_before + found_text + content_after; + $el.html(resp.content); + + $status.text("Position " + resp.position + " of " + resp.filesize + "."); + } + + $status.show(); + }); + }); + + $(document).keyup(function(e) { + var mode = $el.data("mode"); + if(mode != "search" && e.which == 83) { + $("#search-input").focus(); + } + // Exit search mode if escape is pressed. + else if(mode == "search" && e.which == 27) { + exit_search_mode(); + } + }); + + setInterval(read_log, 1000); + var $el = $("#log-content"); + scroll_down($el); +} + +var skip_updates = false; + +function init_updater() { + function update() { + if (skip_updates) return; + + $.ajax({ + url: location.href, + cache: false, + dataType: "html", + success: function(resp){ + $("#psdash").find(".main-content").html(resp); + } + }); + } + + setInterval(update, 3000); +} + +function init_connections_filter() { + var $content = $("#psdash"); + $content.on("change", "#connections-form select", function () { + $content.find("#connections-form").submit(); + }); + $content.on("focus", "#connections-form select, #connections-form input", function () { + skip_updates = true; + }); + $content.on("blur", "#connections-form select, #connections-form input", function () { + skip_updates = false; + }); + $content.on("keypress", "#connections-form input[type='text']", function (e) { + if (e.which == 13) { + $content.find("#connections-form").submit(); + } + }); +} + +$(document).ready(function() { + init_connections_filter(); + + if($("#log").length == 0) { + init_updater(); + } else { + init_log(); + } +}); diff --git a/smt_view/templates/base.html b/smt_view/templates/base.html index cecc765..a3c2d83 100644 --- a/smt_view/templates/base.html +++ b/smt_view/templates/base.html @@ -1,92 +1,70 @@ - - - - - - - - -