Create classed components for tabs

The Tabs class in rswebui.js will handle all common tab boilerplate
and store tabs state as well as route tables for the main routings.
This commit is contained in:
rottencandy 2019-07-10 23:23:54 +05:30
parent 70524f5e10
commit 89e2de2f2e
7 changed files with 244 additions and 175 deletions

View File

@ -5,7 +5,7 @@ function sidebar(links) {
return m('.sidebar',
Object.keys(links)
.map(function(panelName) {
return m('a.sidebar-link' + (Panel.active === panelName ? '#selected' : ''), {
return m('a.sidebar-link' + (Panel.active === panelName ? '#selected-sidebar-link' : ''), {
onclick: function() {
Panel.active = panelName;
},
@ -37,14 +37,14 @@ function setMaxRates() {
let upload = document.getElementById('upload-limit')
.value;
if(isNaN(download) || isNaN(upload)) {
// TODO show proper error
// TODO display error on setting non-numeric value
return;
}
rs.rsJsonApiRequest('/rsConfig/SetMaxDataRates', {
downKb: Number(download),
upKb: Number(upload),
},
//TODO display success animation
//TODO display success animation(fontawesome)
() => {},
);
};
@ -57,7 +57,7 @@ new Panel('Network', {
.value = data.outKb;
});
},
// TODO show info from UI hover message
// TODO show info from UI hover message
view: function() {
return m('.node-panel', [
m('h3', 'Network Configuration'),
@ -142,6 +142,7 @@ let component = {
},
};
new rs.Tab('config', component);
module.exports = {
component,
};

View File

@ -16,96 +16,109 @@ const FT_STATE_PAUSED = 0x0006;
const FT_STATE_CHECKING_HASH = 0x0007;
let Downloads = {
statusMap : new Map(),
hashes : [],
statusMap: new Map(),
hashes: [],
loadHashes() {
rs.rsJsonApiRequest(
'/rsFiles/FileDownloads',
{},
function(d) {
Downloads.hashes = d.hashs;
},
'/rsFiles/FileDownloads', {},
function(d) {
Downloads.hashes = d.hashs;
},
);
},
loadStatus() {
Downloads.loadHashes();
if (Downloads.hashes.length !== Downloads.statusMap.size)
if(Downloads.hashes.length !== Downloads.statusMap.size)
Downloads.statusMap.clear();
for (let hash of Downloads.hashes) {
for(let hash of Downloads.hashes) {
let json_params = {
hash,
hintflags : 16, // RS_FILE_HINTS_DOWNLOAD
hintflags: 16, // RS_FILE_HINTS_DOWNLOAD
};
rs.rsJsonApiRequest(
'/rsFiles/FileDetails',
json_params,
function(fileStat) {
Downloads.statusMap.set(hash, fileStat.info);
},
'/rsFiles/FileDetails',
json_params,
function(fileStat) {
Downloads.statusMap.set(hash, fileStat.info);
},
);
}
},
};
function makeFriendlyUnit(bytes) {
if (bytes < 1e3)
if(bytes < 1e3)
return bytes.toFixed(1) + 'B';
if (bytes < 1e6)
return (bytes / 1e3).toFixed(1) + 'kB';
if (bytes < 1e9)
return (bytes / 1e6).toFixed(1) + 'MB';
if (bytes < 1e12)
return (bytes / 1e9).toFixed(1) + 'GB';
return (bytes / 1e12).toFixed(1) + 'TB';
if(bytes < 1e6)
return (bytes / 1e3)
.toFixed(1) + 'kB';
if(bytes < 1e9)
return (bytes / 1e6)
.toFixed(1) + 'MB';
if(bytes < 1e12)
return (bytes / 1e9)
.toFixed(1) + 'GB';
return (bytes / 1e12)
.toFixed(1) + 'TB';
}
function progressBar(rate) {
console.log('rate: ', rate)
rate = rate.toPrecision(3);
return m('.progressbar[]',
{style : {content : rate + '%'}},
m('span.progress-status', {style : {width : rate + '%'}}, rate + '%')
return m('.progressbar[]', {
style: {
content: rate + '%'
}
},
m('span.progress-status', {
style: {
width: rate + '%'
}
}, rate + '%')
);
};
function fileAction(hash, action) {
let action_header = '';
let json_params = {hash, flags : 0};
let json_params = {
hash,
flags: 0
};
switch (action) {
case 'cancel':
action_header = '/rsFiles/FileCancel';
break;
case 'cancel':
action_header = '/rsFiles/FileCancel';
break;
case 'pause':
action_header = '/rsFiles/FileControl';
json_params.flags = RS_FILE_CTRL_PAUSE;
break;
case 'pause':
action_header = '/rsFiles/FileControl';
json_params.flags = RS_FILE_CTRL_PAUSE;
break;
case 'resume':
action_header = '/rsFiles/FileControl';
json_params.flags = RS_FILE_CTRL_START;
break;
case 'resume':
action_header = '/rsFiles/FileControl';
json_params.flags = RS_FILE_CTRL_START;
break;
case 'force_check':
action_header = '/rsFiles/FileControl';
json_params.flags = RS_FILE_CTRL_FORCE_CHECK;
break;
case 'force_check':
action_header = '/rsFiles/FileControl';
json_params.flags = RS_FILE_CTRL_FORCE_CHECK;
break;
default:
console.error('Unknown action in Downloads.control()');
return;
default:
console.error('Unknown action in Downloads.control()');
return;
};
rs.rsJsonApiRequest(action_header, json_params, () => {}); // false
};
function actionButton(file, action) {
return m('button', {
onclick : function() {
fileAction(file.hash, action);
onclick: function() {
fileAction(file.hash, action);
}
},
},
action);
};
@ -116,59 +129,50 @@ let backgroundCallback = function() {
backgroundCallback = backgroundCallback.bind(Downloads);
let isComponentActive = function() {
if (m.route.get() === '/downloads')
return true;
else
return false;
return (m.route.get() === '/downloads');
}
component = {
oninit : function() {
rs.setBackgroundTask(backgroundCallback, 5000, isComponentActive);
oninit: function() {
rs.setBackgroundTask(backgroundCallback, 1000, isComponentActive);
},
view : function() {
view: function() {
return m('.tab.frame-center', [
m('h3', 'Downloads (' + Downloads.statusMap.size + ')'), m('hr'),
m(
'table',
[
m('tr',
[
m('th', 'Name'),
m('th', 'Size'),
m('th', 'Transfer rate'),
m('th', 'Status'),
m('th', 'Progress'),
m('th', 'Action'),
]),
Array.from(
Downloads.statusMap,
function(fileStatus) {
let info = fileStatus[1];
let progress = info.transfered / info.size * 100;
// Using hash of file as vnode key
return m('tr', {key : fileStatus[0]}, [
m('td', info.name),
m('td', makeFriendlyUnit(info.size)),
m('td', makeFriendlyUnit(info.tfRate * 1024) + '/s'),
m('td', info.download_status),
m('td', progressBar(progress)),
m('td',
[
actionButton(info,
info.downloadStatus === FT_STATE_PAUSED
? 'resume'
: 'pause'),
actionButton(info, 'cancel'),
]),
]);
})
])
m('table', [
m('tr', [
m('th', 'Name'),
m('th', 'Size'),
m('th', 'Transfer rate'),
m('th', 'Status'),
m('th', 'Progress'),
m('th', 'Action'),
]),
Array.from(Downloads.statusMap, function(fileStatus) {
let info = fileStatus[1];
let progress = info.transfered / info.size * 100;
// Using hash of file as vnode key
return m('tr', {
key: fileStatus[0]
}, [
m('td', info.name),
m('td', makeFriendlyUnit(info.size)),
m('td', makeFriendlyUnit(info.tfRate * 1024) + '/s'),
m('td', info.download_status),
m('td', progressBar(progress)),
m('td', [
actionButton(info, info.downloadStatus === FT_STATE_PAUSED ? 'resume' : 'pause'),
actionButton(info, 'cancel'),
]),
]);
}),
]),
]);
},
};
new rs.Tab('downloads', component);
module.exports = {
component,
};

View File

@ -30,6 +30,7 @@ function copyToClipboard() {
document.execCommand('copy');
};
new rs.Tab('home', component);
module.exports = {
component,
};

View File

@ -1,13 +1,13 @@
var m = require('mithril');
var rs = require('rswebui');
let onSuccessCallback = function() {};
let onSuccessCallback = undefined;
function renderLoginPage(callback) {
// Cannot use mount because vDOM will not let any other component overrides
m.render(document.getElementById('main'), m(loginComponent));
onSuccessCallback = callback;
}
};
let loginComponent = {
view: function() {
@ -17,24 +17,21 @@ let loginComponent = {
m('input.field[type=text][placeholder=Username][id=uname]'),
m('input.field[type=password][placeholder=Password][id=passwd]'),
m('button.submit-btn', {
onclick: verifyLogin
onclick: verifyLogin,
}, 'Login'),
m('p.error[id=error]'),
]));
}
};
let uname = '';
let passwd = '';
function verifyLogin() {
[uname, passwd] = getKeys();
let [uname, passwd] = getKeys();
let loginHeader = {
'Authorization': 'Basic ' + btoa(uname + ':' + passwd)
};
rs.rsJsonApiRequest('/rsPeers/GetRetroshareInvite', {}, onResponse, true,
rs.rsJsonApiRequest('/rsPeers/GetRetroshareInvite', {}, loginHandleWrapper(uname, passwd), true,
loginHeader);
}
};
function getKeys() {
let uname = document.getElementById('uname')
@ -42,22 +39,25 @@ function getKeys() {
let passwd = document.getElementById('passwd')
.value;
return [uname, passwd];
}
};
function onResponse(data, successful) {
if(successful) {
rs.setKeys(uname, passwd);
onSuccessCallback();
} else {
displayErrorMessage();
}
}
function loginHandleWrapper(uname, passwd) {
let onResponse = function(data, successful) {
if(successful) {
rs.setKeys(uname, passwd);
onSuccessCallback();
} else {
displayErrorMessage();
}
};
return onResponse;
};
function displayErrorMessage() {
m.render(document.getElementById('error'), 'Incorrect login/password.');
}
};
module.exports = {
renderLoginPage,
}
};

View File

@ -1,5 +1,6 @@
let m = require('mithril');
let login = require('login');
let rs = require('rswebui');
login.renderLoginPage(onSuccess);
@ -8,16 +9,26 @@ function onSuccess() {
let dl = require('downloads');
let config = require('config');
renderMainStructure();
m.route(document.getElementById('tab-section'), '/home', {
'/home': home.component,
'/downloads': dl.component,
'/config': config.component,
});
rs.Tab.active = 'home';
m.route(document.getElementById('main'), '/home', rs.Tab.routeTable);
//renderMainStructure();
//m.route(document.getElementById('tab-section'), '/home', {
// '/home': home.component,
// '/downloads': dl.component,
// '/config': config.component,
//});
};
function renderMainStructure() {
m.render(document.getElementById('main'), [
rs.Tab.menuBar(),
]);
};
/*
function renderMainStructure() {
m.render(document.getElementById('main'), [
rs.Tab.menuBar(),
m('nav.tab-container',
[
m('a.tab-header[href=/home]', {
@ -34,4 +45,5 @@ function renderMainStructure() {
m('div#tab-section')
]);
};
*/

View File

@ -1,14 +1,15 @@
'use strict';
var m = require('mithril');
let m = require('mithril');
const API_URL = 'http://127.0.0.1:9092';
let loginKey = {
username : '',
passwd : '',
isVerified : false,
username: '',
passwd: '',
isVerified: false,
};
// Make this as object property?
function setKeys(username, password, verified = true) {
loginKey.username = username;
loginKey.passwd = password;
@ -16,64 +17,109 @@ function setKeys(username, password, verified = true) {
}
function rsJsonApiRequest(path, data, callback, async = true, headers = {}) {
// retroshare will crash if data is not of object type.
// Retroshare will crash if data is not of object type.
data = data || {};
headers['Accept'] = 'application/json';
if (loginKey.isVerified) {
if(loginKey.isVerified) {
headers['Authorization'] =
'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd);
}
console.info('Sending request: \nPath: ' + path + '\nData: ' + data +
'\nHeaders:' + headers);
'\nHeaders:' + headers);
// TODO: Properly handle types of fail situtations
// Eg. Retroshare switched off, wrong path, incorrect data, etc.
m.request({
method : 'POST',
url : API_URL + path,
async,
extract : (xhr) => {
// empty string is not valid json and fails on parse
if (xhr.responseText === '')
xhr.responseText = '""';
return {
status : xhr.status,
body : JSON.parse(xhr.responseText),
};
},
headers : headers,
data : data,
})
.then((result) => {
if (typeof(callback) === 'function') {
if (result.status === 200) {
callback(result.body, true);
} else {
loginKey.isVerified = false;
callback(result.body, false);
}
method: 'POST',
url: API_URL + path,
async,
extract: (xhr) => {
// Empty string is not valid json and fails on parse
if(xhr.responseText === '')
xhr.responseText = '""';
return {
status: xhr.status,
body: JSON.parse(xhr.responseText),
};
},
headers: headers,
data: data,
})
.then((result) => {
if(typeof(callback) === 'function') {
if(result.status === 200) {
callback(result.body, true);
} else {
loginKey.isVerified = false;
callback(result.body, false);
}
})
.catch(function(e) {
callback({}, false);
console.error('Error sending request: ', e);
});
}
})
.catch(function(e) {
callback({}, false);
console.error('Error sending request: ', e);
});
}
function setBackgroundTask(task, interval, checkTaskScope) {
// Always use bound(.bind) function when accsssing outside objects
// to avoid loss of scope
let taskId;
taskId = setTimeout(function caller() {
if (checkTaskScope()) {
let taskId = setTimeout(function caller() {
if(checkTaskScope()) {
task();
taskId = setTimeout(caller, interval);
} else {
clearTimeout(taskId);
}
}, interval);
return taskId;
};
class Tab {
constructor(name, content) {
Tab.componentList[name] = this;
// Overriding view function to add menubar
this.mainView = content.view;
delete content.view;
// Object itself will be passed as mithril component
Object.assign(this, content);
Tab.routeTable['/' + name] = this;
}
view() {
return m('.content', [
Tab.menuBar(),
m('#tab-content', this.mainView()),
]);
}
static menuBar() {
return m('nav.tab-menu',
Object.keys(this.componentList)
.map(function(tabName) {
return m('a.tab-menu-item' + (tabName === Tab.active ? '#selected-tab-item' : '') + '[href=/' + tabName + ']', {
oncreate: m.route.link,
onclick: function() {
// NOTE: investigate why onclick does not run when clicked on downloads tab
// NOTE 2: does not run ONLY when clicked before its backgroundtask first returns
// Note 3: workaround: not use oncreate, set #! link in href
Tab.active = tabName;
},
},
tabName); //TODO add icons
})
);
}
}
// Static class variables are still experimental
Tab.active = '';
Tab.componentList = {};
Tab.routeTable = {};
module.exports = {
rsJsonApiRequest,
setKeys,
setBackgroundTask,
Tab,
};

View File

@ -51,15 +51,6 @@ li {
padding: 10px;
}
@keyframes fadein {
from {opacity: 0;}
to {opacity: 1;}
}
.tab {
animation: fadein 0.5s;
}
.frame-center {
display: flex;
flex-direction: column;
@ -87,11 +78,25 @@ li {
text-align: center;
}
/******************************
Animations
******************************/
@keyframes fadein {
from {opacity: 0;}
to {opacity: 1;}
}
.tab {
animation: fadein 0.5s;
}
/******************************
Target specific sections
******************************/
#tab-section {
/* Margin on top to avoid getting covered by navbar */
#tab-content {
margin-top: 3em;
}
@ -121,7 +126,7 @@ Target specific sections
}
/* Navbar */
.tab-container {
.tab-menu {
padding: 1em 1em;
background-color: #333;
overflow: hidden;
@ -130,15 +135,15 @@ Target specific sections
z-index: 1;
width: 100%;
}
.tab-header {
.tab-menu-item {
text-align: center;
padding: 1em 1em;
text-decoration: none;
color: white;
transition: all 0.5s;
} .tab-header:hover {
} .tab-menu-item:hover {
background-color: #999;
} .tab-header:focus {
} .tab-menu-item#selected-tab-item {
background-color: #3ba4d7;
}
.tab-section {
@ -161,7 +166,7 @@ Target specific sections
border-bottom: 1px solid lightgrey;
}.sidebar-link:hover{
background-color: #ccc;
}.sidebar-link#selected {
}.sidebar-link#selected-sidebar-link {
background-color: lightgrey;
}
/* Content adjacent to sidebar */