From 93c6a4dfcd11fa04e8553607b1bdccb82cb700a7 Mon Sep 17 00:00:00 2001 From: Shweelan Samson Date: Tue, 19 Dec 2017 01:35:31 -0600 Subject: [PATCH] Add overview page --- client/app.js | 2 +- client/config.js | 30 ++++++++- client/css/app.css | 62 ++++++++++++++++++ client/layout.js | 2 +- client/overview-page.js | 98 +++++++++++++++++++++++++-- client/pie-chart-component.js | 120 ++++++++++++++++++++++++++++++++++ 6 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 client/pie-chart-component.js diff --git a/client/app.js b/client/app.js index 2781f38..74e0777 100644 --- a/client/app.js +++ b/client/app.js @@ -23,7 +23,7 @@ function pagify(pageName, page) { page .init(args) .then(st => { - if (!st) return m.reoute.set("/"); + if (!st) return m.route.set("/"); state = st; resolve(); }) diff --git a/client/config.js b/client/config.js index 566323d..ba32429 100644 --- a/client/config.js +++ b/client/config.js @@ -37,5 +37,33 @@ export default { parameter: "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress" } - ] + ], + overview: { + online: { + label: "Online devices", + charts: { + all: { + label: "All devices", + slices: { + onlineNow: { + label: "Online now", + color: "#31a354", + filter: "Events.Inform > NOW() - 5 * 60 * 1000" + }, + past24: { + label: "Past 24 hours", + color: "#addd8e", + filter: + "Events.Inform > (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000) AND Events.Inform < (NOW() - 5 * 60 * 1000)" + }, + others: { + label: "Others", + color: "#f7fcb9", + filter: "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)" + } + } + } + } + } + } }; diff --git a/client/css/app.css b/client/css/app.css index 66d33ac..e381ac4 100644 --- a/client/css/app.css +++ b/client/css/app.css @@ -149,12 +149,14 @@ h1 { h2 { color: var(--color5); font-weight: bold; + font-size: 120%; margin-top: 1em; margin-bottom: 0.5em; } h3 { color: var(--color5); + font-size: 110%; margin-top: 1em; margin-bottom: 0.5em; } @@ -226,3 +228,63 @@ button { } } } + +.page-overview { + & > h1 { + text-align: center; + } +} + +.overview-chart-group { + display: flex; + justify-content: center; + + & h2 { + text-align: center; + } +} + +.pie-chart { + margin: 20px; + text-align: center; + + & > svg { + & > path { + stroke: var(--color1); + stroke-width: 1px; + } + + & > a { + transition: opacity var(--fade); + opacity: 0; + + & > text { + font-weight: bold; + opacity: 0.4; + } + + & > path { + stroke: var(--color4); + } + + &:hover { + opacity: 1; + } + } + } + + & > .legend { + text-align: left; + margin: 15px; + + & > .legend-line { + & > .color { + height: 12px; + width: 12px; + margin-right: 5px; + border: 1px solid var(--color1); + display: inline-block; + } + } + } +} diff --git a/client/layout.js b/client/layout.js index bacab47..47d7fb9 100644 --- a/client/layout.js +++ b/client/layout.js @@ -11,7 +11,7 @@ const layout = { m("img.logo", { src: "logo.svg" }), m(menu, { page: vnode.attrs.page }) ]), - m("#content", [vnode.children]) + m("#content", { class: `page-${vnode.attrs.page}` }, [vnode.children]) ]; } }; diff --git a/client/overview-page.js b/client/overview-page.js index 35c091f..38a74f6 100644 --- a/client/overview-page.js +++ b/client/overview-page.js @@ -1,21 +1,105 @@ "use strict"; import m from "mithril"; +import config from "./config"; +import pieChartComponent from "./pie-chart-component"; +import * as filterParser from "../common/filter-parser"; + +function count(filter, limit) { + return new Promise((resolve, reject) => { + function extract(xhr) { + return +xhr.getResponseHeader("x-total-count"); + } + m + .request({ + method: "HEAD", + url: + "/api/devices/?" + + m.buildQueryString({ filter: filter, limit: limit }), + extract: extract + }) + .then(res => { + resolve(res); + }) + .catch(err => { + err.message = err.message || "Unknown error"; + reject(err); + }); + }); +} + +function queryCharts(groups) { + const now = Date.now(); + function evalNow(exp) { + if (exp[0] == "FUNC" && exp[1] === "NOW" && exp.length == 2) return now; + } + + let calls = []; + groups = Object.assign({}, groups); + for (let [groupName, group] of Object.entries(groups)) { + groups[groupName] = group = Object.assign({}, group); + for (let [chartName, chart] of Object.entries(group.charts)) { + group.charts[chartName] = chart = Object.assign({}, chart); + for (let [sliceName, slice] of Object.entries(chart.slices)) { + chart.slices[sliceName] = slice = Object.assign({}, slice); + calls.push( + new Promise((resolve, reject) => { + let f = filterParser.map(filterParser.parse(slice.filter), evalNow); + count(filterParser.stringify(f)) + .then(c => { + slice.count = c; + resolve(); + }) + .catch(reject); + }) + ); + } + } + } + + return new Promise((resolve, reject) => { + Promise.all(calls) + .then(() => { + resolve(groups); + }) + .catch(reject); + }); +} const init = function() { - return new Promise(resolve => { - resolve({ message: "Hello world!" }); + return new Promise((resolve, reject) => { + queryCharts(config.overview) + .then(charts => { + resolve({ charts: charts }); + }) + .catch(err => { + err.message = err.message || "Unknown error"; + reject(err); + }); }); }; const component = { view: vnode => { document.title = "Overview - GenieACS"; - return m( - "a", - { href: "#!/devices/202BC1-BM632w-000000" }, - vnode.attrs.message - ); + let children = []; + for (let group of Object.values(vnode.attrs.charts)) { + if (group.label) children.push(m("h1", group.label)); + + let groupChildren = []; + for (let chart of Object.values(group.charts)) { + let chartChildren = []; + if (chart.label) chartChildren.push(m("h2", chart.label)); + + chartChildren.push(m(pieChartComponent, { chart: chart })); + + groupChildren.push(m(".overview-chart", chartChildren)); + } + + children.push(m(".overview-chart-group", groupChildren)); + } + + return children; } }; diff --git a/client/pie-chart-component.js b/client/pie-chart-component.js new file mode 100644 index 0000000..f21d511 --- /dev/null +++ b/client/pie-chart-component.js @@ -0,0 +1,120 @@ +"use strict"; + +import m from "mithril"; + +const drawChart = function(chartData) { + let slices = chartData.slices; + let total = Array.from(Object.values(chartData.slices)).reduce( + (a, s) => a + s.count, + 0 + ); + let legend = []; + let paths = []; + let links = []; + let currentProgressPercentage = 0; + let startX = Math.cos(2 * Math.PI * currentProgressPercentage) * 100; + let startY = Math.sin(2 * Math.PI * currentProgressPercentage) * 100; + let endX, endY; + + for (let slice of Object.values(slices)) { + let percent = total > 0 ? slice.count / total : 0; + legend.push( + m(".legend-line", [ + m("span.color", { + style: `background-color: ${slice.color} !important;` + }), + `${slice.label}: `, + m( + "a", + { + href: `/#!/devices/?${m.buildQueryString({ + filter: slice.filter + })}` + }, + slice.count + ), + ` (${(percent * 100).toFixed(2)}%)` + ]) + ); + + if (percent > 0) { + currentProgressPercentage += percent; + endX = Math.cos(2 * Math.PI * currentProgressPercentage) * 100; + endY = Math.sin(2 * Math.PI * currentProgressPercentage) * 100; + let isBigArc = percent > 0.5 ? 1 : 0; + + let sketch = + `M ${startX} ${startY} ` + // Move to the starting point + `A 100 100 0 ${isBigArc} 1 ${endX} ${endY} ` + // Draw an Arc from starting point to ending point + `L 0 0 z`; // complete the shape by drawing a line to the center of circle + + startX = endX; + startY = endY; + + paths.push( + m("path", { + d: sketch, + fill: slice.color + }) + ); + + let percentageX = + Math.cos(2 * Math.PI * (currentProgressPercentage - percent / 2)) * 50; + let percentageY = + Math.sin(2 * Math.PI * (currentProgressPercentage - percent / 2)) * 50; + + links.push( + m( + "a", + { + href: `/#!/devices/?${m.buildQueryString({ + filter: slice.filter + })}` + }, + [ + m("path", { + d: sketch, + "fill-opacity": 0 + }), + m( + "text", + { + x: percentageX, + y: percentageY, + "dominant-baseline": "middle", + "text-anchor": "middle" + }, + `${(percent * 100).toFixed(2)}%` + ) + ] + ) + ); + } + } + + legend.push(m("span.legend-total", `Total: ${total}`)); + + return m("div", { class: "pie-chart" }, [ + m( + "svg", + { + // Adding 2 as padding; strokes must not be more than 2 + viewBox: "-102 -102 204 204", + width: "204px", + height: "204px", + xmlns: "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink" + }, + paths.concat(links) + ), + m(".legend", legend) + ]); +}; + +const component = { + view: vnode => { + return drawChart(vnode.attrs.chart); + } +}; + +export default component;