[TASK] add consoleView

This commit is contained in:
Martin Geno 2017-05-30 14:35:11 +02:00
parent 6046e7dd95
commit 2e5873a541
No known key found for this signature in database
GPG Key ID: F0D39A37E925E941
10 changed files with 342 additions and 17 deletions

View File

@ -7,15 +7,15 @@ import (
) )
type List struct { type List struct {
cmd string cmd string `json:"cmd"`
Clients map[string]*ListResult Clients map[string]*ListResult `json:"clients"`
sshManager *Manager sshManager *Manager
} }
type ListResult struct { type ListResult struct {
ssh *ssh.Client ssh *ssh.Client
Runned bool Running bool `json:"running"`
WithError bool WithError bool `json:"with_error"`
Result string Result string `json:"result"`
} }
func (m *Manager) CreateList(cmd string) *List { func (m *Manager) CreateList(cmd string) *List {
@ -25,7 +25,7 @@ func (m *Manager) CreateList(cmd string) *List {
Clients: make(map[string]*ListResult), Clients: make(map[string]*ListResult),
} }
for host, client := range m.clients { for host, client := range m.clients {
list.Clients[host] = &ListResult{Runned: false, ssh: client} list.Clients[host] = &ListResult{Running: true, ssh: client}
} }
return list return list
} }
@ -43,7 +43,7 @@ func (l List) Run() {
func (l List) runlistelement(host string, client *ListResult, wg *sync.WaitGroup) { func (l List) runlistelement(host string, client *ListResult, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
result, err := l.sshManager.run(host, client.ssh, l.cmd) result, err := l.sshManager.run(host, client.ssh, l.cmd)
client.Runned = true client.Running = false
if err != nil { if err != nil {
client.WithError = true client.WithError = true
return return

View File

@ -19,18 +19,18 @@ func TestList(t *testing.T) {
list := mgmt.CreateList("exit 1") list := mgmt.CreateList("exit 1")
assert.Len(list.Clients, 1) assert.Len(list.Clients, 1)
client := list.Clients[addr.IP.String()] client := list.Clients[addr.IP.String()]
assert.False(client.Runned) assert.True(client.Running)
list.Run() list.Run()
assert.True(client.Runned) assert.False(client.Running)
assert.True(client.WithError) assert.True(client.WithError)
assert.Equal("", client.Result) assert.Equal("", client.Result)
list = mgmt.CreateList("echo 15") list = mgmt.CreateList("echo 15")
assert.Len(list.Clients, 1) assert.Len(list.Clients, 1)
client = list.Clients[addr.IP.String()] client = list.Clients[addr.IP.String()]
assert.False(client.Runned) assert.True(client.Running)
list.Run() list.Run()
assert.True(client.Runned) assert.False(client.Running)
assert.False(client.WithError) assert.False(client.WithError)
assert.Equal("15", client.Result) assert.Equal("15", client.Result)
} }

36
webroot/css/console.css Normal file
View File

@ -0,0 +1,36 @@
.prompt {
position: fixed;
bottom: 0px;
background-color: #ccc;
width: 100%;
}
.prompt .btn {
width: 10%;
}
.prompt input {
width: 85%;
margin-left: 2%;
}
.console {
font-family: monospace;
white-space: pre;
}
.console .cmd {
min-height: 22px;
clear: both;
}
.console .cmd > .time, .console .cmd .host{
display: inline-block;
color: #009ee0;
width: 15%;
}
.console .cmd > div {
clear: both;
background-color: #ccc;
margin-bottom: 3px;
}
.console .cmd .status {
width: 15%;
height: 20px;
}

View File

@ -9,14 +9,16 @@ body {
.status { .status {
float: right; float: right;
background: #009ee0; background: #009ee0;
color: white;
width: 50px; width: 50px;
height: 50px; height: 50px;
} }
.status.connecting { .status.connecting,.status.running {
background: #ffb400; background: #ffb400;
} }
.status.offline { .status.offline, .status.failed {
background: #dc0067; background: #dc0067;
color: white;
} }
span.online { span.online {
color: #009ee0; color: #009ee0;

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" href="/css/leaflet.css"> <link rel="stylesheet" href="/css/leaflet.css">
<link rel="stylesheet" href="/css/main.css"> <link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/map.css"> <link rel="stylesheet" href="/css/map.css">
<link rel="stylesheet" href="/css/console.css">
<script src="/js/moment.js"></script> <script src="/js/moment.js"></script>
<script src="/js/navigo.js"></script> <script src="/js/navigo.js"></script>
<script src="/js/leaflet.js"></script> <script src="/js/leaflet.js"></script>
@ -17,6 +18,7 @@
<script src="/js/domlib.js"></script> <script src="/js/domlib.js"></script>
<script src="/js/store.js"></script> <script src="/js/store.js"></script>
<script src="/js/notify.js"></script> <script src="/js/notify.js"></script>
<script src="/js/gui_console.js"></script>
<script src="/js/gui_list.js"></script> <script src="/js/gui_list.js"></script>
<script src="/js/gui_map.js"></script> <script src="/js/gui_map.js"></script>
<script src="/js/gui_node.js"></script> <script src="/js/gui_node.js"></script>

View File

@ -1,5 +1,5 @@
/* exported gui,router */ /* exported gui,router */
/* globals socket,notify,domlib,guiList,guiMap,guiStats,guiNode */ /* globals socket,notify,domlib,guiList,guiMap,guiStats,guiNode,guiConsole */
const gui = {}, const gui = {},
router = new Navigo(null, true, '#'); router = new Navigo(null, true, '#');
@ -58,6 +58,9 @@ const gui = {},
} }
router.on({ router.on({
'/console': function routerConsole () {
setView(guiConsole);
},
'/list': function routerList () { '/list': function routerList () {
setView(guiList); setView(guiList);
}, },
@ -75,6 +78,7 @@ const gui = {},
'/statistics': function routerStats () { '/statistics': function routerStats () {
setView(guiStats); setView(guiStats);
} }
}); });
router.on(() => { router.on(() => {
router.navigate('/list'); router.navigate('/list');

256
webroot/js/gui_console.js Normal file
View File

@ -0,0 +1,256 @@
/* exported guiConsole */
/* globals domlib,store,socket */
const guiConsole = {};
(function init () {
'use strict';
const view = guiConsole,
ownCMDs = ['0'],
cmdRow = {};
let container = null,
el = null,
output = null,
ownfilter = false;
function createID () {
let digit = new Date().getTime();
// Use high-precision timer if available
/* eslint-disable */
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
digit += performance.now();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
const result = (digit + Math.random() * 16) % 16 | 0;
digit = Math.floor(digit / 16);
return (char === 'x'
? result
: result & 0x3 | 0x8).toString(16);
});
/* eslint-enable*/
}
function updateCMD (row, cmd) {
if (cmd.cmd === '' && cmd.timestemp === 0) {
return;
}
row.cmd.innerHTML = cmd.cmd;
row.timestemp.innerHTML = moment(cmd.timestemp).fromNow(true);
let running = 0,
failed = 0,
sum = 0;
if (cmd.clients) {
sum = Object.keys(cmd.clients).length;
Object.keys(cmd.clients).forEach((addr) => {
const client = cmd.clients[addr],
clientRow = row.clients[addr];
clientRow.status.classList.remove('running', 'failed', 'success');
if (client.running) {
running += 1;
clientRow.status.classList.add('running');
} else if (client.with_error) {
failed += 1;
clientRow.status.classList.add('failed');
} else {
clientRow.status.classList.add('success');
}
clientRow.result.innerHTML = client.result;
clientRow.host.innerHTML = addr;
});
}
row.status.classList.remove('running', 'failed', 'success');
if (running > 0) {
row.status.innerHTML = `running (${running}`;
row.status.classList.add('running');
} else if (failed > 0 || sum === 0) {
row.status.innerHTML = `failed (${failed}`;
row.status.classList.add('failed');
} else {
row.status.innerHTML = `success (${sum}`;
row.status.classList.add('success');
}
row.status.innerHTML += `/${sum})`;
}
function createRow (cmd) {
const row = {
'clients': {},
'clientsContainer': document.createElement('div'),
'clientsEl': {},
'el': document.createElement('div')
};
if (cmd.cmd === '' && cmd.timestemp === 0) {
row.el.innerHTML = '\n' +
' _______ ________ __\n' +
' | |.-----.-----.-----.| | | |.----.| |_\n' +
' | - || _ | -__| || | | || _|| _|\n' +
' |_______|| __|_____|__|__||________||__| |____|\n' +
' |__| W I R E L E S S F R E E D O M\n' +
' -----------------------------------------------------\n' +
' FreifunkManager shell for openwrt/Lede/gluon systems \n' +
' -----------------------------------------------------\n' +
' * 1 1/2 oz Gin Shake with a glassful\n' +
' * 1/4 oz Triple Sec of broken ice and pour\n' +
' * 3/4 oz Lime Juice unstrained into a goblet.\n' +
' * 1 1/2 oz Orange Juice\n' +
' * 1 tsp. Grenadine Syrup\n' +
' -----------------------------------------------------\n';
return row;
}
row.timestemp = domlib.newAt(row.el, 'span');
row.cmd = domlib.newAt(row.el, 'span');
row.status = domlib.newAt(row.el, 'span');
row.el.classList.add('cmd');
row.timestemp.classList.add('time');
row.status.classList.add('status');
if (cmd.clients) {
Object.keys(cmd.clients).forEach((addr) => {
const clientEl = domlib.newAt(row.clientsContainer, 'div'),
clients = {
'host': domlib.newAt(clientEl, 'span'),
'result': domlib.newAt(clientEl, 'span'),
'status': domlib.newAt(clientEl, 'span')
};
clients.host.classList.add('host');
clients.status.classList.add('status');
row.clientsEl[addr] = clientEl;
row.clients[addr] = clients;
});
row.cmd.addEventListener('click', () => {
if (row.clientsContainer.parentElement) {
row.el.removeChild(row.clientsContainer);
} else {
row.el.appendChild(row.clientsContainer);
}
});
}
updateCMD(row, cmd);
return row;
}
function update () {
let cmds = store.getCMDs();
if (ownfilter) {
const tmp = cmds;
cmds = {};
Object.keys(tmp).
forEach((id) => {
if (ownCMDs.indexOf(id) >= 0) {
cmds[id] = tmp[id];
}
});
}
Object.keys(cmdRow).forEach((id) => {
if (cmdRow[id].el.parentElement) {
output.removeChild(cmdRow[id].el);
}
});
Object.keys(cmds).forEach((id) => {
const cmd = cmds[id];
if (cmdRow[id]) {
updateCMD(cmdRow[id], cmd);
} else {
cmdRow[id] = createRow(cmd);
}
});
Object.keys(cmdRow).
sort((aID, bID) => {
if (!cmds[aID] || !cmds[bID]) {
return 0;
}
return cmds[aID].timestemp - cmds[bID].timestemp;
}).
forEach((id) => {
if (cmds[id] && !cmdRow[id].el.parentElement) {
output.appendChild(cmdRow[id].el);
}
});
}
view.bind = function bind (bindEl) {
container = bindEl;
};
view.render = function render () {
if (!container) {
return;
} else if (el) {
container.appendChild(el);
update();
return;
}
console.log('generate new view for console');
el = domlib.newAt(container, 'div');
store.updateCMD({
'cmd': '',
'id': '0',
'timestemp': 0
});
output = domlib.newAt(el, 'div');
output.classList.add('console');
const prompt = domlib.newAt(el, 'div'),
filterBtn = domlib.newAt(prompt, 'span'),
promptInput = domlib.newAt(prompt, 'input');
prompt.classList.add('prompt');
promptInput.addEventListener('keyup', (event) => {
// eslint-disable-next-line no-magic-numbers
if (event.keyCode !== 13) {
return;
}
const cmd = {
'cmd': promptInput.value,
'id': createID(),
'timestemp': new Date()
};
ownCMDs.push(cmd.id);
socket.sendcmd(cmd);
promptInput.value = '';
});
filterBtn.classList.add('btn');
filterBtn.innerHTML = 'Show all';
filterBtn.addEventListener('click', () => {
ownfilter = !ownfilter;
filterBtn.classList.toggle('active');
filterBtn.innerHTML = ownfilter
? 'Show own'
: 'Show all';
update();
});
update();
};
})();

View File

@ -35,6 +35,11 @@ let socket = {'readyState': 0};
store.stats = msg.body; store.stats = msg.body;
} }
break; break;
case 'cmd':
if (msg.body) {
store.updateCMD(msg.body);
}
break;
default: default:
notify.send('warn', `unable to identify message: ${raw}`); notify.send('warn', `unable to identify message: ${raw}`);
break; break;
@ -57,10 +62,18 @@ let socket = {'readyState': 0};
'type': 'system' 'type': 'system'
}); });
socket.send(socketMsg);
notify.send('success', notifyMsg);
}
function sendcmd (cmd) {
const notifyMsg = `Befehl '${cmd.cmd}' wird überall ausgeführt.`,
socketMsg = JSON.stringify({
'body': cmd,
'type': 'cmd'
});
socket.send(socketMsg); socket.send(socketMsg);
notify.send('success', notifyMsg); notify.send('success', notifyMsg);
} }
@ -71,6 +84,7 @@ let socket = {'readyState': 0};
socket.onmessage = onmessage; socket.onmessage = onmessage;
socket.onclose = onclose; socket.onclose = onclose;
socket.sendnode = sendnode; socket.sendnode = sendnode;
socket.sendcmd = sendcmd;
} }
connect(); connect();

View File

@ -18,7 +18,8 @@ const store = {
'use strict'; 'use strict';
const current = {}, const current = {},
list = {}; list = {},
cmds = {};
function getNode (nodeid) { function getNode (nodeid) {
let node = {}; let node = {};
@ -51,9 +52,18 @@ const store = {
} }
}; };
store.getNode = getNode; store.getNode = getNode;
store.getNodes = function getNodes () { store.getNodes = function getNodes () {
return Object.keys(list).map(getNode); return Object.keys(list).map(getNode);
}; };
store.updateCMD = function updateCMD (cmd) {
cmds[cmd.id] = cmd;
};
store.getCMDs = function getCMDs () {
return cmds;
};
})(); })();

View File

@ -12,4 +12,5 @@ const (
MessageTypeSystemNode = "system" MessageTypeSystemNode = "system"
MessageTypeCurrentNode = "current" MessageTypeCurrentNode = "current"
MessageTypeStats = "stats" MessageTypeStats = "stats"
MessageTypeCmd = "cmd"
) )