From 89e2de2f2eeb3f24a91f55f271c94ee04b8506ce Mon Sep 17 00:00:00 2001 From: rottencandy Date: Wed, 10 Jul 2019 23:23:54 +0530 Subject: [PATCH] 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. --- webui-src/app/config.js | 9 +- webui-src/app/downloads.js | 186 +++++++++++++++++++------------------ webui-src/app/home.js | 1 + webui-src/app/login.js | 40 ++++---- webui-src/app/main.js | 24 +++-- webui-src/app/rswebui.js | 124 +++++++++++++++++-------- webui-src/app/theme.css | 35 ++++--- 7 files changed, 244 insertions(+), 175 deletions(-) diff --git a/webui-src/app/config.js b/webui-src/app/config.js index 9270d5e..90de78a 100644 --- a/webui-src/app/config.js +++ b/webui-src/app/config.js @@ -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, }; diff --git a/webui-src/app/downloads.js b/webui-src/app/downloads.js index 6b78bcc..c60e0ec 100644 --- a/webui-src/app/downloads.js +++ b/webui-src/app/downloads.js @@ -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, }; + diff --git a/webui-src/app/home.js b/webui-src/app/home.js index 7d45579..e5052cd 100644 --- a/webui-src/app/home.js +++ b/webui-src/app/home.js @@ -30,6 +30,7 @@ function copyToClipboard() { document.execCommand('copy'); }; +new rs.Tab('home', component); module.exports = { component, }; diff --git a/webui-src/app/login.js b/webui-src/app/login.js index f2be967..bf12203 100644 --- a/webui-src/app/login.js +++ b/webui-src/app/login.js @@ -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, -} +}; diff --git a/webui-src/app/main.js b/webui-src/app/main.js index b523c44..da1ff71 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -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') ]); }; +*/ diff --git a/webui-src/app/rswebui.js b/webui-src/app/rswebui.js index c99e3ba..6202cec 100644 --- a/webui-src/app/rswebui.js +++ b/webui-src/app/rswebui.js @@ -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, }; diff --git a/webui-src/app/theme.css b/webui-src/app/theme.css index b7df714..2988875 100644 --- a/webui-src/app/theme.css +++ b/webui-src/app/theme.css @@ -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 */