Add overview page
This commit is contained in:
1 parent
a7e07cbde9
commit
93c6a4dfcd
6 files changed
+304
-10
No files matched your search
+1
-1
@@ -23,7 +23,7 @@ function pagify(pageName, page) {
|
|||||||
page
|
page
|
||||||
.init(args)
|
.init(args)
|
||||||
.then(st => {
|
.then(st => {
|
||||||
if (!st) return m.reoute.set("/");
|
if (!st) return m.route.set("/");
|
||||||
state = st;
|
state = st;
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
|
|||||||
+29
-1
@@ -37,5 +37,33 @@ export default {
|
|||||||
parameter:
|
parameter:
|
||||||
"InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress"
|
"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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -149,12 +149,14 @@ h1 {
|
|||||||
h2 {
|
h2 {
|
||||||
color: var(--color5);
|
color: var(--color5);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
font-size: 120%;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
color: var(--color5);
|
color: var(--color5);
|
||||||
|
font-size: 110%;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 0.5em;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -11,7 +11,7 @@ const layout = {
|
|||||||
m("img.logo", { src: "logo.svg" }),
|
m("img.logo", { src: "logo.svg" }),
|
||||||
m(menu, { page: vnode.attrs.page })
|
m(menu, { page: vnode.attrs.page })
|
||||||
]),
|
]),
|
||||||
m("#content", [vnode.children])
|
m("#content", { class: `page-${vnode.attrs.page}` }, [vnode.children])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+91
-7
@@ -1,21 +1,105 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import m from "mithril";
|
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() {
|
const init = function() {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve, reject) => {
|
||||||
resolve({ message: "Hello world!" });
|
queryCharts(config.overview)
|
||||||
|
.then(charts => {
|
||||||
|
resolve({ charts: charts });
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
err.message = err.message || "Unknown error";
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const component = {
|
const component = {
|
||||||
view: vnode => {
|
view: vnode => {
|
||||||
document.title = "Overview - GenieACS";
|
document.title = "Overview - GenieACS";
|
||||||
return m(
|
let children = [];
|
||||||
"a",
|
for (let group of Object.values(vnode.attrs.charts)) {
|
||||||
{ href: "#!/devices/202BC1-BM632w-000000" },
|
if (group.label) children.push(m("h1", group.label));
|
||||||
vnode.attrs.message
|
|
||||||
);
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in new issue
Block a user