Expand the logging mechanism to support alerts overlaid on the main screen

Now we get decent visual indication of sync errors, for instance. Still
work to do to coalesce alerts so that the screen doesn’t fill up with
them after an interval. And probably we should add a button to clear
all alerts.
This commit is contained in:
Jermolene 2014-02-14 07:53:41 +00:00
parent 28212f08b2
commit 70a120d4a6
12 changed files with 118 additions and 70 deletions

View File

@ -219,6 +219,7 @@ exports.compileFilter = function(filterString) {
});
// Return a function that applies the operations to a source array/hashmap of tiddler titles
return function(source,currTiddlerTitle) {
source = source || self.tiddlers;
var results = [];
$tw.utils.each(operationFunctions,function(operationFunction) {
operationFunction(results,source,currTiddlerTitle);

View File

@ -16,11 +16,8 @@ var widget = require("$:/core/modules/widgets/widget.js");
exports.startup = function() {
var modules,n,m,f,commander;
// Load utility modules and initialise the logger
// Load modules
$tw.modules.applyMethods("utils",$tw.utils);
$tw.logger = new $tw.utils.Logger();
$tw.log = $tw.logger.log;
// Load other modules
$tw.modules.applyMethods("global",$tw);
$tw.modules.applyMethods("config",$tw.config);
if($tw.browser) {
@ -111,7 +108,7 @@ exports.startup = function() {
$tw.rootWidget.addEventListener("tw-scroll",function(event) {
$tw.pageScroller.handleEvent(event);
});
// Install the save action handler
// Install the save action handlers
$tw.rootWidget.addEventListener("tw-save-wiki",function(event) {
$tw.syncer.saveWiki({
template: event.param,
@ -132,6 +129,16 @@ exports.startup = function() {
downloadType: "text/plain"
});
});
// Listen out for login/logout/refresh events in the browser
$tw.rootWidget.addEventListener("tw-login",function() {
$tw.syncer.handleLoginEvent();
});
$tw.rootWidget.addEventListener("tw-logout",function() {
$tw.syncer.handleLogoutEvent();
});
$tw.rootWidget.addEventListener("tw-server-refresh",function() {
$tw.syncer.handleRefreshEvent();
});
// Install the crypto event handlers
$tw.rootWidget.addEventListener("tw-set-password",function(event) {
$tw.passwordPrompt.createPrompt({

View File

@ -20,7 +20,7 @@ function Syncer(options) {
var self = this;
this.wiki = options.wiki;
// Make a logger
this.log = $tw.logger.makeLog("syncer");
this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : ""));
// Find a working syncadaptor
this.syncadaptor = undefined;
$tw.modules.forEachModuleOfType("syncadaptor",function(title,module) {
@ -51,18 +51,6 @@ function Syncer(options) {
self.handleLazyLoadEvent(title);
});
}
// Listen out for login/logout/refresh events in the browser
if($tw.browser) {
document.addEventListener("tw-login",function(event) {
self.handleLoginEvent(event);
},false);
document.addEventListener("tw-logout",function(event) {
self.handleLogoutEvent(event);
},false);
document.addEventListener("tw-server-refresh",function(event) {
self.handleRefreshEvent(event);
},false);
}
// Get the login status
this.getStatus(function (err,isLoggedIn) {
// Do a sync from the server
@ -70,13 +58,6 @@ function Syncer(options) {
});
}
/*
Error handling
*/
Syncer.prototype.showError = function(error) {
this.log("Error: " + error);
};
/*
Constants
*/
@ -96,8 +77,10 @@ Syncer.prototype.readTiddlerInfo = function() {
// Hashmap by title of {revision:,changeCount:,adaptorInfo:}
this.tiddlerInfo = {};
// Record information for known tiddlers
var self = this;
this.wiki.forEachTiddler({includeSystem: true},function(title,tiddler) {
var self = this,
tiddlers = this.filterFn.call(this.wiki);
$tw.utils.each(tiddlers,function(title) {
var tiddler = self.wiki.getTiddler(title);
self.tiddlerInfo[title] = {
revision: tiddler.fields["revision"],
adaptorInfo: self.syncadaptor && self.syncadaptor.getTiddlerInfo(tiddler),
@ -165,7 +148,7 @@ Syncer.prototype.saveWiki = function(options) {
for(var t=this.savers.length-1; t>=0; t--) {
var saver = this.savers[t];
if(saver.info.capabilities.indexOf(method) !== -1 && saver.save(text,method,callback)) {
this.log("Saving wiki with method",method,"through saver",saver.info.name);
this.logger.log("Saving wiki with method",method,"through saver",saver.info.name);
// Clear the task queue if we're saving (rather than downloading)
if(method !== "download") {
this.readTiddlerInfo();
@ -208,7 +191,7 @@ Syncer.prototype.getStatus = function(callback) {
// Get login status
this.syncadaptor.getStatus(function(err,isLoggedIn,username) {
if(err) {
self.showError(err);
self.logger.alert(err);
return;
}
// Set the various status tiddlers
@ -233,7 +216,7 @@ Synchronise from the server by reading the skinny tiddler list and queuing up lo
*/
Syncer.prototype.syncFromServer = function() {
if(this.syncadaptor && this.syncadaptor.getSkinnyTiddlers) {
this.log("Retrieving skinny tiddler list");
this.logger.log("Retrieving skinny tiddler list");
var self = this;
if(this.pollTimerId) {
clearTimeout(this.pollTimerId);
@ -247,7 +230,7 @@ Syncer.prototype.syncFromServer = function() {
},self.pollTimerInterval);
// Check for errors
if(err) {
self.log("Error retrieving skinny tiddler list:",err);
self.logger.alert("Error retrieving skinny tiddler list:",err);
return;
}
// Process each incoming tiddler
@ -285,8 +268,8 @@ Syncer.prototype.syncToServer = function(changes) {
now = new Date(),
filteredChanges = this.filterFn.call(this.wiki,changes);
$tw.utils.each(changes,function(change,title,object) {
// Ignore the change if it is a shadow tiddler
if((change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) || (!change.deleted && filteredChanges.indexOf(title) !== -1)) {
// Process the change if it is a deletion of a tiddler we're already syncing, or is on the filtered change list
if((change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) || filteredChanges.indexOf(title) !== -1) {
// Queue a task to sync this tiddler
self.enqueueSyncTask({
type: change.deleted ? "delete" : "save",
@ -300,7 +283,6 @@ Syncer.prototype.syncToServer = function(changes) {
Lazily load a skinny tiddler if we can
*/
Syncer.prototype.handleLazyLoadEvent = function(title) {
console.log("Lazy loading",title)
// Queue up a sync task to load this tiddler
this.enqueueSyncTask({
type: "load",
@ -335,7 +317,7 @@ Attempt to login to TiddlyWeb.
callback: invoked with arguments (err,isLoggedIn)
*/
Syncer.prototype.login = function(username,password,callback) {
this.log("Attempting to login as",username);
this.logger.log("Attempting to login as",username);
var self = this;
if(this.syncadaptor.login) {
this.syncadaptor.login(username,password,function(err) {
@ -357,12 +339,12 @@ Syncer.prototype.login = function(username,password,callback) {
Attempt to log out of TiddlyWeb
*/
Syncer.prototype.handleLogoutEvent = function() {
this.log("Attempting to logout");
this.logger.log("Attempting to logout");
var self = this;
if(this.syncadaptor.logout) {
this.syncadaptor.logout(function(err) {
if(err) {
self.showError(err);
self.logger.alert(err);
} else {
self.getStatus();
}
@ -400,7 +382,7 @@ Syncer.prototype.enqueueSyncTask = function(task) {
}
// Check if this tiddler is already in the queue
if($tw.utils.hop(this.taskQueue,task.title)) {
// this.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
// this.logger.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
var existingTask = this.taskQueue[task.title];
// If so, just update the last modification time
existingTask.lastModificationTime = task.lastModificationTime;
@ -409,7 +391,7 @@ Syncer.prototype.enqueueSyncTask = function(task) {
existingTask.type = task.type;
}
} else {
// this.log("Queuing up sync task with type:",task.type,"title:",task.title);
// this.logger.log("Queuing up sync task with type:",task.type,"title:",task.title);
// If it is not in the queue, insert it
this.taskQueue[task.title] = task;
}
@ -463,7 +445,7 @@ Syncer.prototype.processTaskQueue = function() {
// Dispatch the task
this.dispatchTask(task,function(err) {
if(err) {
self.showError("Sync error while processing '" + task.title + "':\n" + err);
self.logger.alert("Sync error while processing '" + task.title + "':\n" + err);
}
// Mark that this task is no longer in progress
delete self.taskInProgress[task.title];
@ -515,7 +497,7 @@ Syncer.prototype.dispatchTask = function(task,callback) {
if(task.type === "save") {
var changeCount = this.wiki.getChangeCount(task.title),
tiddler = this.wiki.getTiddler(task.title);
this.log("Dispatching 'save' task:",task.title);
this.logger.log("Dispatching 'save' task:",task.title);
if(tiddler) {
this.syncadaptor.saveTiddler(tiddler,function(err,adaptorInfo,revision) {
if(err) {
@ -533,7 +515,7 @@ Syncer.prototype.dispatchTask = function(task,callback) {
}
} else if(task.type === "load") {
// Load the tiddler
this.log("Dispatching 'load' task:",task.title);
this.logger.log("Dispatching 'load' task:",task.title);
this.syncadaptor.loadTiddler(task.title,function(err,tiddlerFields) {
if(err) {
return callback(err);
@ -547,7 +529,7 @@ Syncer.prototype.dispatchTask = function(task,callback) {
});
} else if(task.type === "delete") {
// Delete the tiddler
this.log("Dispatching 'delete' task:",task.title);
this.logger.log("Dispatching 'delete' task:",task.title);
this.syncadaptor.deleteTiddler(task.title,function(err) {
if(err) {
return callback(err);

View File

@ -45,7 +45,7 @@ exports.httpRequest = function(options) {
return;
}
// Something went wrong
options.callback(new Error("XMLHttpRequest error: " + this.status));
options.callback("XMLHttpRequest error code: " + this.status);
}
};
// Make the request

View File

@ -15,29 +15,36 @@ A basic logging implementation
/*
Make a new logger
*/
function Logger() {
function Logger(componentName) {
this.componentName = componentName || "";
}
/*
Make a log function for a particular component
*/
Logger.prototype.makeLog = function(componentName) {
var self = this;
return function(/* args */) {
self.log.apply(self.log,[componentName + ":"].concat(Array.prototype.slice.call(arguments,0)));
};
};
/*
Log a message
*/
Logger.prototype.log = function(/* args */) {
if(console !== undefined && console.log !== undefined) {
return Function.apply.call(console.log, console, arguments);
return Function.apply.call(console.log, console, [this.componentName + ":"].concat(Array.prototype.slice.call(arguments,0)));
}
};
/*
Alert a message
*/
Logger.prototype.alert = function(/* args */) {
var text = Array.prototype.join.call(arguments," "),
fields = {
title: $tw.wiki.generateNewTitle("$:/temp/alerts/alert",{prefix: ""}),
text: text,
tags: ["$:/tags/Alert"],
component: this.componentName,
modified: new Date()
};
$tw.wiki.addTiddler(new $tw.Tiddler(fields));
// Log it too
this.log.apply(this,Array.prototype.slice.call(arguments,0));
};
exports.Logger = Logger;
})();

View File

@ -172,11 +172,14 @@ exports.tiddlerExists = function(title) {
/*
Generate an unused title from the specified base
*/
exports.generateNewTitle = function(baseTitle) {
exports.generateNewTitle = function(baseTitle,options) {
options = options || {};
var c = 0,
title = baseTitle;
while(this.tiddlerExists(title)) {
title = baseTitle + " " + (++c);
title = baseTitle +
(options.prefix || " ") +
(++c);
};
return title;
};

View File

@ -0,0 +1,3 @@
title: $:/core/ui/AlertTemplate
<div class="tw-alert"><div class="tw-alert-toolbar"><$button message="tw-delete-tiddler" class="btn-invisible">{{$:/core/images/delete-button}}</$button></div><div class="tw-alert-subtitle"><$view field="component"/> - <$view field="modified" format="relativedate"/></div><div class="tw-alert-body"><$view field="text"/></div></div>

View File

@ -3,6 +3,13 @@ tags: $:/tags/PageTemplate
<section class="story-river">
<!-- Alerts -->
<div class="tw-alerts">
<$list filter="[is[shadow]!has[draft.of]tag[$:/tags/Alert]] [!is[shadow]!has[draft.of]tag[$:/tags/Alert]] +[sort[modified]]" template="$:/core/ui/AlertTemplate" storyview="pop"/>
</div>
<!-- The main story -->
<$list filter="[list[$:/StoryList]]" history="$:/HistoryList" template="$:/core/ui/ViewTemplate" editTemplate="$:/core/ui/EditTemplate" storyview={{$:/view}} />

View File

@ -0,0 +1,17 @@
created: 20140213224306412
modified: 20140213224622441
tags: mechanism
title: AlertMechanism
type: text/vnd.tiddlywiki
Alerts are displayed as yellow boxes overlaying the main TiddlyWiki window. Each one corresponds to a tiddler with the tag [[$:/tags/Alert]]. Clicking the delete icon on an alert deletes the corresponding tiddler.
Alert tiddlers should have the following fields:
|!Field |!Description |
|title |By default, alert titles have the prefix `$:/temp/alerts/` |
|text |The text of the alert message |
|modified |Date of the alert (used for ordering the alerts on screen) |
|component |Component name associated with the alert |
|tags |Must include [[$:/tags/Alert]] |

View File

@ -21,12 +21,12 @@ function FileSystemAdaptor(syncer) {
this.syncer = syncer;
this.watchers = {};
this.pending = {};
this.log = $tw.logger.makeLog("FileSystem");
this.logger = new $tw.utils.Logger("FileSystem");
this.setwatcher = function(filename, title) {
return undefined;
return this.watchers[filename] = this.watchers[filename] ||
fs.watch(filename, {persistent: false}, function(e) {
self.log("Error:",e,filename);
self.logger.log("Error:",e,filename);
if(e === "change") {
var tiddlers = $tw.loadTiddlersFromFile(filename).tiddlers;
for(var t in tiddlers) {
@ -157,7 +157,7 @@ FileSystemAdaptor.prototype.saveTiddler = function(tiddler,callback) {
if(err) {
return callback(err);
}
self.log("Saved file",fileInfo.filepath);
self.logger.log("Saved file",fileInfo.filepath);
_finish();
});
});
@ -169,7 +169,7 @@ FileSystemAdaptor.prototype.saveTiddler = function(tiddler,callback) {
if(err) {
return callback(err);
}
self.log("Saved file",fileInfo.filepath);
self.logger.log("Saved file",fileInfo.filepath);
_finish();
});
}
@ -203,7 +203,7 @@ FileSystemAdaptor.prototype.deleteTiddler = function(title,callback) {
if(err) {
return callback(err);
}
self.log("Deleted file",fileInfo.filepath);
self.logger.log("Deleted file",fileInfo.filepath);
// Delete the metafile if present
if(fileInfo.hasMetaFile) {
fs.unlink(fileInfo.filepath + ".meta",function(err) {

View File

@ -19,7 +19,7 @@ function TiddlyWebAdaptor(syncer) {
this.syncer = syncer;
this.host = this.getHost();
this.recipe = undefined;
this.log = $tw.logger.makeLog("TiddlyWebAdaptor");
this.logger = new $tw.utils.Logger("TiddlyWebAdaptor");
}
TiddlyWebAdaptor.prototype.getHost = function() {
@ -48,7 +48,7 @@ TiddlyWebAdaptor.prototype.getStatus = function(callback) {
// Get status
var self = this,
wiki = self.syncer.wiki;
this.log("Getting status");
this.logger.log("Getting status");
$tw.utils.httpRequest({
url: this.host + "status",
callback: function(err,data) {
@ -63,7 +63,7 @@ TiddlyWebAdaptor.prototype.getStatus = function(callback) {
} catch (e) {
}
if(json) {
self.log("Status:",data);
self.logger.log("Status:",data);
// Record the recipe
if(json.space) {
self.recipe = json.space.recipe;
@ -95,7 +95,7 @@ TiddlyWebAdaptor.prototype.login = function(username,password,callback) {
callback(err);
}
};
this.log("Logging in:",options);
this.logger.log("Logging in:",options);
$tw.utils.httpRequest(options);
};
@ -113,7 +113,7 @@ TiddlyWebAdaptor.prototype.logout = function(callback) {
callback(err);
}
};
this.log("Logging out:",options);
this.logger.log("Logging out:",options);
$tw.utils.httpRequest(options);
};

View File

@ -922,14 +922,35 @@ canvas.tw-edit-bitmapeditor {
** Alerts
*/
.tw-alerts {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20000;
}
.tw-alert {
position: relative;
<<border-radius 6px>>
margin: 7px 7px 7px 7px;
margin: 28px;
padding: 14px 14px 14px 14px;
border: 3px solid #DBB727;
background-color: rgb(250, 210, 50);
}
.tw-alert-toolbar {
position: absolute;
top: 14px;
right: 14px;
}
.tw-alert-subtitle {
color: #DBB727;
font-weight: bold;
}
.tw-static-alert {
position: relative;
}