Log tab added to download and remove logs
This commit is contained in:
parent
7231acc236
commit
c5c382ade6
7 changed files with 172 additions and 3 deletions
|
|
@ -177,6 +177,7 @@ class Server():
|
|||
self._tornado_app = Application(self._router.urls + [
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", LargeResponseHandler, {"path": settings().getBaseFolder("timelapse"), "as_attachment": True}),
|
||||
(r"/downloads/files/local/([^/]*\.(gco|gcode))", LargeResponseHandler, {"path": settings().getBaseFolder("uploads"), "as_attachment": True}),
|
||||
(r"/downloads/logs/([^/]*)", LargeResponseHandler, {"path": settings().getBaseFolder("logs"), "as_attachment": True}),
|
||||
(r".*", FallbackHandler, {"fallback": WSGIContainer(app.wsgi_app)})
|
||||
])
|
||||
self._server = HTTPServer(self._tornado_app)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from . import files as api_files
|
|||
from . import settings as api_settings
|
||||
from . import timelapse as api_timelapse
|
||||
from . import users as api_users
|
||||
from . import log as api_logs
|
||||
|
||||
|
||||
#~~ first run setup
|
||||
|
|
|
|||
62
src/octoprint/server/api/log.py
Normal file
62
src/octoprint/server/api/log.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# coding=utf-8
|
||||
__author__ = "Marc Hannappel Salandora"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
|
||||
import os
|
||||
import datetime
|
||||
|
||||
from flask import request, jsonify, make_response, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import octoprint.util as util
|
||||
from octoprint.settings import settings, valid_boolean_trues
|
||||
|
||||
from octoprint.server import restricted_access, admin_permission
|
||||
from octoprint.server.util import redirectToTornado
|
||||
from octoprint.server.api import api
|
||||
|
||||
@api.route("/logs", methods=["GET"])
|
||||
def getLogData():
|
||||
files = _getLogFiles()
|
||||
return jsonify(files=files)
|
||||
|
||||
@api.route("/logs/online/<filename>", methods=["GET"])
|
||||
def onlineLog(filename):
|
||||
secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename))
|
||||
if not os.path.exists(secure):
|
||||
return make_response("Unknown filename: %s" % filename, 404)
|
||||
|
||||
file = open(secure, 'r')
|
||||
return jsonify(data=file.read())
|
||||
|
||||
@api.route("/logs/<filename>", methods=["GET"])
|
||||
def downloadLog(filename):
|
||||
return redirectToTornado(request, url_for("index") + "downloads/logs/" + filename)
|
||||
|
||||
|
||||
@api.route("/logs/<filename>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
def deleteLog(filename):
|
||||
secure = os.path.join(settings().getBaseFolder("logs"), secure_filename(filename))
|
||||
if os.path.exists(secure):
|
||||
os.remove(secure)
|
||||
|
||||
return getLogData()
|
||||
|
||||
def _getLogFiles():
|
||||
files = []
|
||||
basedir = settings().getBaseFolder("logs")
|
||||
for osFile in os.listdir(basedir):
|
||||
statResult = os.stat(os.path.join(basedir, osFile))
|
||||
files.append({
|
||||
"name": osFile,
|
||||
"size": util.getFormattedSize(statResult.st_size),
|
||||
"bytes": statResult.st_size,
|
||||
"date": util.getFormattedDateTime(datetime.datetime.fromtimestamp(statResult.st_ctime))
|
||||
})
|
||||
|
||||
for file in files:
|
||||
file["url"] = url_for("index") + "downloads/logs/" + file["name"]
|
||||
|
||||
return files
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel) {
|
||||
function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewModel, temperatureViewModel, controlViewModel, terminalViewModel, gcodeFilesViewModel, timelapseViewModel, gcodeViewModel, logViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.loginStateViewModel = loginStateViewModel;
|
||||
|
|
@ -10,6 +10,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
self.gcodeFilesViewModel = gcodeFilesViewModel;
|
||||
self.timelapseViewModel = timelapseViewModel;
|
||||
self.gcodeViewModel = gcodeViewModel;
|
||||
self.logViewModel = logViewModel;
|
||||
|
||||
self._socket = undefined;
|
||||
self._autoReconnecting = false;
|
||||
|
|
@ -38,7 +39,8 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
|
|||
self._autoReconnectTrial = 0;
|
||||
|
||||
if ($("#offline_overlay").is(":visible")) {
|
||||
$("#offline_overlay").hide();
|
||||
$("#offline_overlay").hide();
|
||||
self.logViewModel.requestData();
|
||||
self.timelapseViewModel.requestData();
|
||||
self.loginStateViewModel.requestData();
|
||||
self.gcodeFilesViewModel.requestData();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ $(function() {
|
|||
var gcodeFilesViewModel = new GcodeFilesViewModel(printerStateViewModel, loginStateViewModel);
|
||||
var gcodeViewModel = new GcodeViewModel(loginStateViewModel, settingsViewModel);
|
||||
var navigationViewModel = new NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsViewModel, usersViewModel);
|
||||
var logViewModel = new LogViewModel(loginStateViewModel);
|
||||
|
||||
var dataUpdater = new DataUpdater(
|
||||
loginStateViewModel,
|
||||
|
|
@ -24,7 +25,8 @@ $(function() {
|
|||
terminalViewModel,
|
||||
gcodeFilesViewModel,
|
||||
timelapseViewModel,
|
||||
gcodeViewModel
|
||||
gcodeViewModel,
|
||||
logViewModel
|
||||
);
|
||||
|
||||
// work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
|
||||
|
|
@ -302,6 +304,7 @@ $(function() {
|
|||
ko.applyBindings(navigationViewModel, document.getElementById("navbar"));
|
||||
ko.applyBindings(appearanceViewModel, document.getElementsByTagName("head")[0]);
|
||||
ko.applyBindings(printerStateViewModel, document.getElementById("drop_overlay"));
|
||||
ko.applyBindings(logViewModel, document.getElementById("logs"));
|
||||
|
||||
var timelapseElement = document.getElementById("timelapse");
|
||||
if (timelapseElement) {
|
||||
|
|
@ -316,6 +319,7 @@ $(function() {
|
|||
gcodeFilesViewModel.requestData();
|
||||
timelapseViewModel.requestData();
|
||||
settingsViewModel.requestData();
|
||||
logViewModel.requestData();
|
||||
|
||||
loginStateViewModel.subscribe(function(change, data) {
|
||||
if ("login" == change) {
|
||||
|
|
|
|||
62
src/octoprint/static/js/app/viewmodels/log.js
Normal file
62
src/octoprint/static/js/app/viewmodels/log.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
function LogViewModel(loginStateViewModel) {
|
||||
var self = this;
|
||||
|
||||
self.loginState = loginStateViewModel;
|
||||
|
||||
// initialize list helper
|
||||
self.listHelper = new ItemListHelper(
|
||||
"logFiles",
|
||||
{
|
||||
"name": function(a, b) {
|
||||
// sorts ascending
|
||||
if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1;
|
||||
if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
},
|
||||
"creation": function(a, b) {
|
||||
// sorts descending
|
||||
if (a["date"] > b["date"]) return -1;
|
||||
if (a["date"] < b["date"]) return 1;
|
||||
return 0;
|
||||
},
|
||||
"size": function(a, b) {
|
||||
// sorts descending
|
||||
if (a["bytes"] > b["bytes"]) return -1;
|
||||
if (a["bytes"] < b["bytes"]) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
"name",
|
||||
[],
|
||||
[],
|
||||
CONFIG_LOGFILESPERPAGE
|
||||
);
|
||||
|
||||
self.requestData = function() {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "logs",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: self.fromResponse
|
||||
});
|
||||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
var files = response.files;
|
||||
if (files === undefined)
|
||||
return;
|
||||
|
||||
self.listHelper.updateItems(files);
|
||||
}
|
||||
|
||||
self.removeFile = function(filename) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "logs/" + filename,
|
||||
type: "DELETE",
|
||||
dataType: "json",
|
||||
success: self.requestData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
var CONFIG_GCODEFILESPERPAGE = 5;
|
||||
var CONFIG_TIMELAPSEFILESPERPAGE = 10;
|
||||
var CONFIG_LOGFILESPERPAGE = 10;
|
||||
var CONFIG_USERSPERPAGE = 10;
|
||||
var CONFIG_WEBCAM_STREAM = "{{ webcamStream }}";
|
||||
var CONFIG_ACCESS_CONTROL = {% if enableAccessControl -%} true; {% else %} false; {%- endif %}
|
||||
|
|
@ -252,6 +253,7 @@
|
|||
{% if enableGCodeVisualizer %}<li><a href="#gcode" data-toggle="tab">GCode Viewer</a></li>{% endif %}
|
||||
<li><a href="#term" data-toggle="tab">Terminal</a></li>
|
||||
{% if enableTimelapse %}<li><a href="#timelapse" data-toggle="tab">Timelapse</a></li>{% endif %}
|
||||
<li><a href="#logs" data-toggle="tab">Logs</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
|
@ -585,6 +587,40 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tab-pane" id="logs">
|
||||
<h1>Logs</h1>
|
||||
|
||||
<div class="pull-right">
|
||||
<small>Sort by: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">Name (ascending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('creation'); }">Creation date (descending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">Size (descending)</a></small>
|
||||
</div>
|
||||
<table class="table table-striped table-hover table-condensed table-hover" id="log_files">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="log_files_name">Name</th>
|
||||
<th class="log_files_size">Size</th>
|
||||
<th class="log_files_action">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: listHelper.paginatedItems">
|
||||
<tr data-bind="attr: {title: name}">
|
||||
<td class="log_files_name" data-bind="text: name"></td>
|
||||
<td class="log_files_size" data-bind="text: size"></td>
|
||||
<td class="log_files_action"><a href="#" class="icon-trash" data-bind="click: function() { if ($root.loginState.isUser()) { $parent.removeFile($data.name); } else { return; } }, css: {disabled: !$root.loginState.isUser()}"></a> | <a href="#" class="icon-download" data-bind="attr: {href: url}"></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination pagination-mini pagination-centered">
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === 0}"><a href="#" data-bind="click: listHelper.prevPage">«</a></li>
|
||||
</ul>
|
||||
<ul data-bind="foreach: listHelper.pages">
|
||||
<li data-bind="css: { active: $data.number === $root.listHelper.currentPage(), disabled: $data.number === -1 }"><a href="#" data-bind="text: $data.text, click: function() { $root.listHelper.changePage($data.number); }"></a></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-bind="css: {disabled: listHelper.currentPage() === listHelper.lastPage()}"><a href="#" data-bind="click: listHelper.nextPage">»</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -644,6 +680,7 @@
|
|||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/terminal.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/timelapse.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/users.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/viewmodels/log.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue