Allow adding metric charts to home page

This commit is contained in:
cuigh 2018-03-26 20:44:42 +08:00
parent 10c70c8285
commit f4546d0888
16 changed files with 904 additions and 486 deletions

View File

@ -176,6 +176,19 @@ var Swirl;
}
}
Core.Tab = Tab;
class FilterBox {
constructor(elem, callback, timeout) {
this.$elem = $(elem);
this.$elem.keyup(() => {
if (this.timer > 0) {
clearTimeout(this.timer);
}
let text = this.$elem.val().toLowerCase();
this.timer = setTimeout(() => callback(text), timeout || 500);
});
}
}
Core.FilterBox = FilterBox;
})(Core = Swirl.Core || (Swirl.Core = {}));
})(Swirl || (Swirl = {}));
/*!
@ -1104,6 +1117,461 @@ var Swirl;
})(Core = Swirl.Core || (Swirl.Core = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Core;
(function (Core) {
class GraphOptions {
constructor() {
this.type = "line";
this.width = 12;
this.height = 150;
}
}
Core.GraphOptions = GraphOptions;
class Graph {
constructor(elem, opts) {
this.$elem = $(elem);
this.opts = $.extend(new GraphOptions(), opts);
this.name = this.$elem.data("chart-name");
}
getName() {
return this.name;
}
getElem() {
return this.$elem;
}
getOptions() {
return this.opts;
}
}
Core.Graph = Graph;
class ValueGraph extends Graph {
constructor(elem, opts) {
super(elem, opts);
}
setData(d) {
}
resize(w, h) {
}
}
Core.ValueGraph = ValueGraph;
class ComplexGraph extends Graph {
constructor(elem, opts) {
super(elem, opts);
if (!this.opts.colors) {
this.opts.colors = ComplexGraph.defaultColors;
}
this.config = {
type: opts.type,
data: {},
options: {
responsive: false,
maintainAspectRatio: false,
animation: {
duration: 0,
},
}
};
this.fillConfig();
this.ctx = $(elem).find("canvas").get(0).getContext('2d');
if (opts.height) {
this.ctx.canvas.width = this.ctx.canvas.parentElement.offsetWidth;
this.ctx.canvas.height = this.ctx.canvas.parentElement.offsetHeight;
}
this.chart = new Chart(this.ctx, this.config);
}
setData(d) {
}
resize(w, h) {
this.ctx.canvas.width = this.ctx.canvas.parentElement.offsetWidth;
this.ctx.canvas.height = this.ctx.canvas.parentElement.offsetHeight;
this.chart.resize();
}
fillConfig() {
}
static formatValue(value, unit) {
switch (unit) {
case "percent:100":
return value.toFixed(1) + "%";
case "percent:1":
return (value * 100).toFixed(1) + "%";
case "time:s":
if (value < 1) {
return (value * 1000).toFixed(0) + 'ms';
}
else {
return value.toFixed(2) + 's';
}
case "size:bytes":
if (value < 1024) {
return value.toString() + 'B';
}
else if (value < 1048576) {
return (value / 1024).toFixed(2) + 'K';
}
else if (value < 1073741824) {
return (value / 1048576).toFixed(2) + 'M';
}
else {
return (value / 1073741824).toFixed(2) + 'G';
}
default:
return value.toFixed(2);
}
}
}
ComplexGraph.defaultColors = [
'rgb(255, 99, 132)',
'rgb(75, 192, 192)',
'rgb(255, 159, 64)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(255, 205, 86)',
'rgb(201, 203, 207)',
];
Core.ComplexGraph = ComplexGraph;
class VectorGraph extends ComplexGraph {
fillConfig() {
this.config.options.legend = {
position: "right"
};
this.config.options.tooltips = {
callbacks: {
label: (tooltipItem, data) => {
let label = data.labels[tooltipItem.index] + ": ";
let p = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
if (typeof p == "number") {
label += ComplexGraph.formatValue(p, this.opts.unit);
}
return label;
}
},
};
}
setData(d) {
this.config.data.datasets = [{
data: d.data,
backgroundColor: this.opts.colors,
}];
this.config.data.labels = d.labels;
this.chart.update();
}
}
Core.VectorGraph = VectorGraph;
class MatrixGraph extends ComplexGraph {
constructor(elem, opts) {
super(elem, opts);
}
fillConfig() {
this.config.options.scales = {
xAxes: [{
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'YYYY/MM/DD HH:mm:ss',
displayFormats: {
minute: 'HH:mm'
}
},
}],
};
if (this.opts.unit) {
this.config.options.scales.yAxes = [{
ticks: {
callback: (n) => ComplexGraph.formatValue(n, this.opts.unit),
}
}];
this.config.options.tooltips = {
callbacks: {
label: (tooltipItem, data) => {
let label = data.datasets[tooltipItem.datasetIndex].label + ": ";
let p = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
label += ComplexGraph.formatValue(p.y, this.opts.unit);
return label;
}
},
};
}
}
setData(d) {
let datasets = (d);
datasets.forEach((ds, i) => {
let color = (i < this.opts.colors.length) ? this.opts.colors[i] : this.opts.colors[0];
ds.backgroundColor = Chart.helpers.color(color).alpha(0.3).rgbString();
ds.borderColor = color;
ds.borderWidth = 2;
ds.pointRadius = 2;
});
this.config.data.datasets = d;
this.chart.update();
}
}
Core.MatrixGraph = MatrixGraph;
class GraphFactory {
static create(elem) {
let $elem = $(elem);
let opts = {
type: $elem.data("chart-type"),
unit: $elem.data("chart-unit"),
width: $elem.data("chart-width"),
height: $elem.data("chart-height"),
colors: $elem.data("chart-colors"),
};
switch (opts.type) {
case "value":
return new ValueGraph($elem, opts);
case "line":
case "bar":
return new MatrixGraph($elem, opts);
case "pie":
return new VectorGraph($elem, opts);
}
return null;
}
}
Core.GraphFactory = GraphFactory;
class GraphPanelOptions {
constructor() {
this.time = "30m";
this.refreshInterval = 15000;
}
}
Core.GraphPanelOptions = GraphPanelOptions;
class GraphPanel {
constructor(elem, opts) {
this.charts = [];
this.opts = $.extend(new GraphPanelOptions(), opts);
this.$panel = $(elem);
this.$panel.children().each((i, e) => {
let g = GraphFactory.create(e);
if (g != null) {
this.charts.push(g);
}
});
$(window).resize(e => {
$.each(this.charts, (i, g) => {
g.resize(0, 0);
});
});
this.refreshData();
}
refreshData() {
this.loadData();
if (this.opts.refreshInterval > 0) {
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval);
}
}
refresh() {
if (!this.timer) {
this.loadData();
if (this.opts.refreshInterval > 0) {
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval);
}
}
}
stop() {
clearTimeout(this.timer);
this.timer = 0;
}
setTime(time) {
this.opts.time = time;
this.loadData();
}
addGraph(c) {
for (let i = 0; i < this.charts.length; i++) {
let chart = this.charts[i];
if (chart.getName() === c.name) {
return;
}
}
let $chart = $(`<div class="column is-${c.width}" data-chart-name="${c.name}" data-chart-type="${c.type}" data-chart-unit="${c.unit}" data-chart-width="${c.width}" data-chart-height="${c.height}">
<div class="card">
<header class="card-header">
<p class="card-header-title">${c.title}</p>
<a data-action="remove-chart" class="card-header-icon" aria-label="remove chart">
<span class="icon">
<i class="fas fa-times has-text-danger" aria-hidden="true"></i>
</span>
</a>
</header>
<div class="card-content">
<div style="height: ${c.height}px">
<canvas id="canvas_${c.name}"></canvas>
</div>
</div>
</div>
</div>`);
this.$panel.append($chart);
let g = GraphFactory.create($chart);
if (g != null) {
this.charts.push(g);
}
this.loadData();
}
removeGraph(name) {
let index;
for (let i = 0; i < this.charts.length; i++) {
let c = this.charts[i];
if (c.getName() === name) {
index = i;
break;
}
}
this.loadData();
}
save() {
let charts = this.charts.map(c => {
return {
name: c.getName(),
width: c.getOptions().width,
height: c.getOptions().height,
};
});
let args = {
name: this.opts.name,
key: this.opts.key || '',
charts: charts,
};
$ajax.post(`/system/chart/save_panel`, args).json((r) => {
if (!r.success) {
Core.Modal.alert(r.message);
}
});
}
loadData() {
let args = {
charts: this.charts.map(c => c.getName()).join(","),
time: this.opts.time,
};
if (this.opts.key) {
args.key = this.opts.key;
}
$ajax.get(`/system/chart/data`, args).json((d) => {
$.each(this.charts, (i, g) => {
if (d[g.getName()]) {
g.setData(d[g.getName()]);
}
});
});
}
}
Core.GraphPanel = GraphPanel;
})(Core = Swirl.Core || (Swirl.Core = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Modal = Swirl.Core.Modal;
var GraphPanel = Swirl.Core.GraphPanel;
var FilterBox = Swirl.Core.FilterBox;
class IndexPage {
constructor() {
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
this.panel = new GraphPanel("#div-charts", { name: "home" });
$("#btn-add").click(this.showAddDlg.bind(this));
$("#btn-add-chart").click(this.addChart.bind(this));
$("#btn-save").click(() => {
this.panel.save();
});
}
showAddDlg() {
let $panel = $("#nav-charts");
$panel.find("label.panel-block").remove();
$ajax.get(`/system/chart/query`, { dashboard: "home" }).json((charts) => {
for (let i = 0; i < charts.length; i++) {
let c = charts[i];
$panel.append(`<label class="panel-block">
<input type="checkbox" value="${c.name}" data-index="${i}">${c.name}: ${c.title}
</label>`);
}
this.charts = charts;
this.$charts = $panel.find("label.panel-block");
});
let dlg = new Modal("#dlg-add-chart");
dlg.show();
}
filterCharts(text) {
if (!text) {
this.$charts.show();
return;
}
this.$charts.each((i, elem) => {
let $elem = $(elem);
let texts = [
this.charts[i].name.toLowerCase(),
this.charts[i].title.toLowerCase(),
this.charts[i].desc.toLowerCase(),
];
for (let i = 0; i < texts.length; i++) {
let index = texts[i].indexOf(text);
if (index >= 0) {
$elem.show();
return;
}
}
$elem.hide();
});
}
addChart() {
this.$charts.each((i, e) => {
if ($(e).find(":checked").length > 0) {
let c = this.charts[i];
this.panel.addGraph(c);
}
});
Modal.close();
}
}
Swirl.IndexPage = IndexPage;
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Metric;
(function (Metric) {
var Modal = Swirl.Core.Modal;
var Dispatcher = Swirl.Core.Dispatcher;
var FilterBox = Swirl.Core.FilterBox;
class ListPage {
constructor() {
this.$charts = $("#div-charts").children();
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
Dispatcher.bind("#div-charts").on("delete-chart", this.deleteChart.bind(this));
}
deleteChart(e) {
let $container = $(e.target).closest("div.column");
let name = $container.data("name");
Modal.confirm(`Are you sure to delete chart: <strong>${name}</strong>?`, "Delete chart", (dlg, e) => {
$ajax.post(name + "/delete").trigger(e.target).json(r => {
$container.remove();
dlg.close();
});
});
}
filterCharts(text) {
if (!text) {
this.$charts.show();
return;
}
this.$charts.each((i, elem) => {
let $elem = $(elem), texts = [
$elem.data("name").toLowerCase(),
$elem.data("title").toLowerCase(),
$elem.data("desc").toLowerCase(),
];
for (let i = 0; i < texts.length; i++) {
let index = texts[i].indexOf(text);
if (index >= 0) {
$elem.show();
return;
}
}
$elem.hide();
});
}
}
Metric.ListPage = ListPage;
})(Metric = Swirl.Metric || (Swirl.Metric = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Config;
(function (Config) {
@ -1857,270 +2325,6 @@ var Swirl;
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Core;
(function (Core) {
class GraphOptions {
constructor() {
this.type = "line";
this.width = 12;
this.height = 50;
}
}
Core.GraphOptions = GraphOptions;
class Graph {
constructor(elem, opts) {
this.$elem = $(elem);
this.opts = $.extend(new GraphOptions(), opts);
this.name = this.$elem.data("chart-name");
}
getName() {
return this.name;
}
}
Core.Graph = Graph;
class ValueGraph extends Graph {
constructor(elem, opts) {
super(elem, opts);
}
setData(d) {
}
setSize(w, h) {
}
}
Core.ValueGraph = ValueGraph;
class ComplexGraph extends Graph {
constructor(elem, opts) {
super(elem, opts);
if (!this.opts.colors) {
this.opts.colors = ComplexGraph.defaultColors;
}
this.config = {
type: opts.type,
data: {},
options: {
animation: {
duration: 0,
},
}
};
this.fillConfig();
this.ctx = $(elem).find("canvas").get(0).getContext('2d');
if (opts.height) {
this.ctx.canvas.height = opts.height;
}
this.chart = new Chart(this.ctx, this.config);
}
setData(d) {
}
setSize(w, h) {
this.ctx.canvas.height = h;
this.chart.update();
}
fillConfig() {
}
static formatValue(value, unit) {
switch (unit) {
case "percent:100":
return value.toFixed(1) + "%";
case "percent:1":
return (value * 100).toFixed(1) + "%";
case "time:s":
if (value < 1) {
return (value * 1000).toFixed(0) + 'ms';
}
else {
return value.toFixed(2) + 's';
}
case "size:bytes":
if (value < 1024) {
return value.toString() + 'B';
}
else if (value < 1048576) {
return (value / 1024).toFixed(2) + 'K';
}
else if (value < 1073741824) {
return (value / 1048576).toFixed(2) + 'M';
}
else {
return (value / 1073741824).toFixed(2) + 'G';
}
default:
return value.toFixed(2);
}
}
}
ComplexGraph.defaultColors = [
'rgb(255, 99, 132)',
'rgb(75, 192, 192)',
'rgb(255, 159, 64)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(255, 205, 86)',
'rgb(201, 203, 207)',
];
Core.ComplexGraph = ComplexGraph;
class VectorGraph extends ComplexGraph {
fillConfig() {
this.config.options.legend = {
position: "right"
};
this.config.options.tooltips = {
callbacks: {
label: (tooltipItem, data) => {
let label = data.labels[tooltipItem.index] + ": ";
let p = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
if (typeof p == "number") {
label += ComplexGraph.formatValue(p, this.opts.unit);
}
return label;
}
},
};
}
setData(d) {
this.config.data.datasets = [{
data: d.data,
backgroundColor: this.opts.colors,
}];
this.config.data.labels = d.labels;
this.chart.update();
}
}
Core.VectorGraph = VectorGraph;
class MatrixGraph extends ComplexGraph {
constructor(elem, opts) {
super(elem, opts);
}
fillConfig() {
this.config.options.scales = {
xAxes: [{
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'YYYY/MM/DD HH:mm:ss',
displayFormats: {
minute: 'HH:mm'
}
},
}],
};
if (this.opts.unit) {
this.config.options.scales.yAxes = [{
ticks: {
callback: (n) => ComplexGraph.formatValue(n, this.opts.unit),
}
}];
this.config.options.tooltips = {
callbacks: {
label: (tooltipItem, data) => {
let label = data.datasets[tooltipItem.datasetIndex].label + ": ";
let p = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
label += ComplexGraph.formatValue(p.y, this.opts.unit);
return label;
}
},
};
}
}
setData(d) {
let datasets = (d);
datasets.forEach((ds, i) => {
let color = (i < this.opts.colors.length) ? this.opts.colors[i] : this.opts.colors[0];
ds.backgroundColor = Chart.helpers.color(color).alpha(0.3).rgbString();
ds.borderColor = color;
ds.borderWidth = 2;
ds.pointRadius = 2;
});
this.config.data.datasets = d;
this.chart.update();
}
}
Core.MatrixGraph = MatrixGraph;
class GraphFactory {
static create(elem) {
let $elem = $(elem);
let opts = {
type: $elem.data("chart-type"),
unit: $elem.data("chart-unit"),
height: $elem.data("chart-height"),
colors: $elem.data("chart-colors"),
};
switch (opts.type) {
case "value":
return new ValueGraph($elem, opts);
case "line":
case "bar":
return new MatrixGraph($elem, opts);
case "pie":
return new VectorGraph($elem, opts);
}
return null;
}
}
Core.GraphFactory = GraphFactory;
class GraphPanelOptions {
constructor() {
this.time = "30m";
this.refreshInterval = 15000;
}
}
Core.GraphPanelOptions = GraphPanelOptions;
class GraphPanel {
constructor(elems, opts) {
this.charts = [];
this.opts = $.extend(new GraphPanelOptions(), opts);
$(elems).each((i, e) => {
let g = GraphFactory.create(e);
if (g != null) {
this.charts.push(g);
}
});
this.refreshData();
}
refreshData() {
this.loadData();
if (this.opts.refreshInterval > 0) {
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval);
}
}
refresh() {
if (!this.timer) {
this.loadData();
if (this.opts.refreshInterval > 0) {
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval);
}
}
}
stop() {
clearTimeout(this.timer);
this.timer = 0;
}
setTime(time) {
this.opts.time = time;
this.loadData();
}
loadData() {
let args = {
dashboard: this.opts.name,
time: this.opts.time,
};
if (this.opts.id) {
args.id = this.opts.id;
}
$ajax.get(`/system/chart/data`, args).json((d) => {
$.each(this.charts, (i, g) => {
if (d[g.getName()]) {
g.setData(d[g.getName()]);
}
});
});
}
}
Core.GraphPanel = GraphPanel;
})(Core = Swirl.Core || (Swirl.Core = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Service;
(function (Service) {
@ -2132,9 +2336,9 @@ var Swirl;
if ($cb_time.length == 0) {
return;
}
this.panel = new GraphPanel($("#div-charts").children("div"), {
this.panel = new GraphPanel("#div-charts", {
name: "service",
id: $("#h2-service-name").text()
key: $("#h2-service-name").text()
});
$("#btn-add").click(() => {
Modal.alert("Coming soon...");
@ -2454,77 +2658,4 @@ var Swirl;
Volume.NewPage = NewPage;
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Modal = Swirl.Core.Modal;
var GraphPanel = Swirl.Core.GraphPanel;
class IndexPage {
constructor() {
this.panel = new GraphPanel($("#div-charts").children("div"), { name: "home" });
$("#btn-add").click(() => {
Modal.alert("Coming soon...");
});
}
}
Swirl.IndexPage = IndexPage;
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Metric;
(function (Metric) {
var Modal = Swirl.Core.Modal;
var Dispatcher = Swirl.Core.Dispatcher;
class FilterBox {
constructor(elem, callback, timeout) {
this.$elem = $(elem);
this.$elem.keyup(() => {
if (this.timer > 0) {
clearTimeout(this.timer);
}
let text = this.$elem.val().toLowerCase();
this.timer = setTimeout(() => callback(text), timeout || 500);
});
}
}
class ListPage {
constructor() {
this.$charts = $("#div-charts").children();
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
Dispatcher.bind("#div-charts").on("delete-chart", this.deleteChart.bind(this));
}
deleteChart(e) {
let $container = $(e.target).closest("div.column");
let name = $container.data("name");
Modal.confirm(`Are you sure to delete chart: <strong>${name}</strong>?`, "Delete chart", (dlg, e) => {
$ajax.post(name + "/delete").trigger(e.target).json(r => {
$container.remove();
dlg.close();
});
});
}
filterCharts(text) {
if (!text) {
this.$charts.show();
return;
}
this.$charts.each((i, elem) => {
let $elem = $(elem), texts = [
$elem.data("name").toLowerCase(),
$elem.data("title").toLowerCase(),
$elem.data("desc").toLowerCase(),
];
for (let i = 0; i < texts.length; i++) {
let index = texts[i].indexOf(text);
if (index >= 0) {
$(elem).show();
return;
}
}
$(elem).hide();
});
}
}
Metric.ListPage = ListPage;
})(Metric = Swirl.Metric || (Swirl.Metric = {}));
})(Swirl || (Swirl = {}));
//# sourceMappingURL=swirl.js.map

File diff suppressed because one or more lines are too long

View File

@ -3,22 +3,7 @@ namespace Swirl.Metric {
import Modal = Swirl.Core.Modal;
import AjaxResult = Swirl.Core.AjaxResult;
import Dispatcher = Swirl.Core.Dispatcher;
class FilterBox {
private $elem: JQuery;
private timer: number;
constructor(elem: string | Element | JQuery, callback: (text: string) => void, timeout?: number) {
this.$elem = $(elem);
this.$elem.keyup(() => {
if (this.timer > 0) {
clearTimeout(this.timer);
}
let text: string = this.$elem.val().toLowerCase();
this.timer = setTimeout(() => callback(text), timeout || 500);
});
}
}
import FilterBox = Swirl.Core.FilterBox;
export class ListPage {
private fb: FilterBox;
@ -59,11 +44,11 @@ namespace Swirl.Metric {
for (let i = 0; i<texts.length; i++) {
let index = texts[i].indexOf(text);
if (index >= 0) {
$(elem).show();
$elem.show();
return;
}
}
$(elem).hide();
$elem.hide();
})
}
}

View File

@ -208,4 +208,20 @@ namespace Swirl.Core {
}
}
}
export class FilterBox {
private $elem: JQuery;
private timer: number;
constructor(elem: string | Element | JQuery, callback: (text: string) => void, timeout?: number) {
this.$elem = $(elem);
this.$elem.keyup(() => {
if (this.timer > 0) {
clearTimeout(this.timer);
}
let text: string = this.$elem.val().toLowerCase();
this.timer = setTimeout(() => callback(text), timeout || 500);
});
}
}
}

View File

@ -3,7 +3,7 @@ namespace Swirl.Core {
type?: string = "line";
unit?: string;
width?: number = 12;
height?: number = 50;
height?: number = 150;
colors?: string[];
}
@ -22,7 +22,15 @@ namespace Swirl.Core {
return this.name;
}
abstract setSize(w: number, h: number): void;
getElem(): JQuery {
return this.$elem;
}
getOptions(): GraphOptions {
return this.opts;
}
abstract resize(w: number, h: number): void;
abstract setData(d: any): void;
}
@ -40,7 +48,7 @@ namespace Swirl.Core {
setData(d: any): void {
}
setSize(w: number, h: number): void {
resize(w: number, h: number): void {
}
}
@ -69,6 +77,8 @@ namespace Swirl.Core {
type: opts.type,
data: {},
options: {
responsive: false,
maintainAspectRatio: false,
// title: {
// // display: true,
// text: opts.title || 'NONE'
@ -106,7 +116,8 @@ namespace Swirl.Core {
this.ctx = (<HTMLCanvasElement>$(elem).find("canvas").get(0)).getContext('2d');
if (opts.height) {
this.ctx.canvas.height = opts.height;
this.ctx.canvas.width = this.ctx.canvas.parentElement.offsetWidth;
this.ctx.canvas.height = this.ctx.canvas.parentElement.offsetHeight;
}
this.chart = new Chart(this.ctx, this.config);
}
@ -114,9 +125,12 @@ namespace Swirl.Core {
setData(d: any): void {
}
setSize(w: number, h: number): void {
this.ctx.canvas.height = h;
this.chart.update();
resize(w: number, h: number): void {
// this.ctx.canvas.style.width = this.ctx.canvas.parentElement.offsetWidth + "px";
// this.ctx.canvas.style.height = this.ctx.canvas.parentElement.offsetWidth + "px";
this.ctx.canvas.width = this.ctx.canvas.parentElement.offsetWidth;
this.ctx.canvas.height = this.ctx.canvas.parentElement.offsetHeight;
this.chart.resize();
}
protected fillConfig() {
@ -266,6 +280,7 @@ namespace Swirl.Core {
let opts: GraphOptions = {
type: $elem.data("chart-type"),
unit: $elem.data("chart-unit"),
width: $elem.data("chart-width"),
height: $elem.data("chart-height"),
colors: $elem.data("chart-colors"),
};
@ -284,26 +299,33 @@ namespace Swirl.Core {
export class GraphPanelOptions {
name: string;
id?: string;
key?: string;
time?: string = "30m";
refreshInterval?: number = 15000; // ms
}
export class GraphPanel {
private $panel: JQuery;
private opts: GraphPanelOptions;
private charts: Graph[] = [];
private timer: number;
constructor(elems: string | Element | JQuery, opts?: GraphPanelOptions) {
constructor(elem: string | Element | JQuery, opts?: GraphPanelOptions) {
this.opts = $.extend(new GraphPanelOptions(), opts);
$(elems).each((i, e) => {
this.$panel = $(elem);
this.$panel.children().each((i, e) => {
let g = GraphFactory.create(e);
if (g != null) {
this.charts.push(g);
}
});
$(window).resize(e => {
$.each(this.charts, (i: number, g: Graph) => {
g.resize(0, 0);
});
});
this.refreshData();
}
@ -333,13 +355,83 @@ namespace Swirl.Core {
this.loadData();
}
addGraph(c: any) {
for (let i =0; i< this.charts.length; i++) {
let chart = this.charts[i];
if (chart.getName() === c.name) {
// chart already added.
return;
}
}
let $chart = $(`<div class="column is-${c.width}" data-chart-name="${c.name}" data-chart-type="${c.type}" data-chart-unit="${c.unit}" data-chart-width="${c.width}" data-chart-height="${c.height}">
<div class="card">
<header class="card-header">
<p class="card-header-title">${c.title}</p>
<a data-action="remove-chart" class="card-header-icon" aria-label="remove chart">
<span class="icon">
<i class="fas fa-times has-text-danger" aria-hidden="true"></i>
</span>
</a>
</header>
<div class="card-content">
<div style="height: ${c.height}px">
<canvas id="canvas_${c.name}"></canvas>
</div>
</div>
</div>
</div>`);
this.$panel.append($chart);
let g = GraphFactory.create($chart);
if (g != null) {
this.charts.push(g);
}
this.loadData();
}
removeGraph(name: string) {
// todo:
let index:number;
for (let i =0; i< this.charts.length; i++) {
let c = this.charts[i];
if (c.getName() === name) {
index = i;
break;
}
}
this.loadData();
}
save() {
let charts = this.charts.map(c => {
return {
name: c.getName(),
width: c.getOptions().width,
height: c.getOptions().height,
// colors: ,
}
});
let args = {
name: this.opts.name,
key: this.opts.key || '',
charts: charts,
};
$ajax.post(`/system/chart/save_panel`, args).json<AjaxResult>((r: AjaxResult) => {
if (!r.success) {
Modal.alert(r.message);
}
});
}
private loadData() {
let args: any = {
dashboard: this.opts.name,
charts: this.charts.map(c => c.getName()).join(","),
time: this.opts.time,
};
if (this.opts.id) {
args.id = this.opts.id;
if (this.opts.key) {
args.key = this.opts.key;
}
$ajax.get(`/system/chart/data`, args).json((d: { [index: string]: Chart.ChartDataSets[] }) => {
$.each(this.charts, (i: number, g: Graph) => {

View File

@ -3,15 +3,76 @@
namespace Swirl {
import Modal = Swirl.Core.Modal;
import GraphPanel = Swirl.Core.GraphPanel;
import FilterBox = Swirl.Core.FilterBox;
export class IndexPage {
private panel: GraphPanel;
private fb: FilterBox;
private charts: any;
private $charts: JQuery;
constructor() {
this.panel = new GraphPanel($("#div-charts").children("div"), {name: "home"});
$("#btn-add").click(() => {
Modal.alert("Coming soon...");
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
this.panel = new GraphPanel("#div-charts", {name: "home"});
$("#btn-add").click(this.showAddDlg.bind(this));
$("#btn-add-chart").click(this.addChart.bind(this));
$("#btn-save").click(() => {
this.panel.save();
});
}
private showAddDlg() {
let $panel = $("#nav-charts");
$panel.find("label.panel-block").remove();
// load charts
$ajax.get(`/system/chart/query`, {dashboard: "home"}).json((charts: any) => {
for (let i = 0; i < charts.length; i++) {
let c = charts[i];
$panel.append(`<label class="panel-block">
<input type="checkbox" value="${c.name}" data-index="${i}">${c.name}: ${c.title}
</label>`);
}
this.charts = charts;
this.$charts = $panel.find("label.panel-block");
});
let dlg = new Modal("#dlg-add-chart");
dlg.show();
}
private filterCharts(text: string) {
if (!text) {
this.$charts.show();
return;
}
this.$charts.each((i, elem) => {
let $elem = $(elem);
let texts: string[] = [
this.charts[i].name.toLowerCase(),
this.charts[i].title.toLowerCase(),
this.charts[i].desc.toLowerCase(),
];
for (let i = 0; i < texts.length; i++) {
let index = texts[i].indexOf(text);
if (index >= 0) {
$elem.show();
return;
}
}
$elem.hide();
})
}
private addChart() {
this.$charts.each((i, e) => {
if ($(e).find(":checked").length > 0) {
let c = this.charts[i];
this.panel.addGraph(c);
}
});
Modal.close();
}
}
}

View File

@ -13,9 +13,9 @@ namespace Swirl.Service {
return;
}
this.panel = new GraphPanel($("#div-charts").children("div"), {
this.panel = new GraphPanel("#div-charts", {
name: "service",
id: $("#h2-service-name").text()
key: $("#h2-service-name").text()
});
$("#btn-add").click(() => {

View File

@ -1,22 +1,44 @@
package biz
import (
"fmt"
"strings"
"os"
"time"
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/log"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/dao"
"github.com/cuigh/swirl/model"
)
// Chart return a chart biz instance.
var Chart = &chartBiz{}
var Chart = newChartBiz()
type chartBiz struct {
builtin []*model.Chart
}
func newChartBiz() *chartBiz {
b := &chartBiz{}
b.builtin = append(b.builtin, model.NewChart("service", "$cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="${service}"}[5m]) * 100`, "percent:100"))
b.builtin = append(b.builtin, model.NewChart("service", "$memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="${service}"}`, "size:bytes"))
b.builtin = append(b.builtin, model.NewChart("service", "$network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes"))
b.builtin = append(b.builtin, model.NewChart("service", "$network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes"))
// for _, c := range categories {
// if c == "java" {
// charts = append(charts, model.NewChart("threads", "Threads", "${instance}", `jvm_threads_current{service="%s"}`, ""))
// charts = append(charts, model.NewChart("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`, "time:s"))
// } else if c == "go" {
// charts = append(charts, model.NewChart("threads", "Threads", "${instance}", `go_threads{service="%s"}`, ""))
// charts = append(charts, model.NewChart("goroutines", "Goroutines", "${instance}", `go_goroutines{service="%s"}`, ""))
// charts = append(charts, model.NewChart("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`, "time:s"))
// }
// }
// for i, c := range charts {
// charts[i].Query = fmt.Sprintf(c.Query, name)
// }
return b
}
func (b *chartBiz) List() (charts []*model.Chart, err error) {
@ -28,8 +50,8 @@ func (b *chartBiz) List() (charts []*model.Chart, err error) {
func (b *chartBiz) Create(chart *model.Chart, user web.User) (err error) {
do(func(d dao.Interface) {
//chart.CreatedAt = time.Now()
//chart.UpdatedAt = chart.CreatedAt
// chart.CreatedAt = time.Now()
// chart.UpdatedAt = chart.CreatedAt
err = d.ChartCreate(chart)
})
return
@ -49,42 +71,31 @@ func (b *chartBiz) Get(name string) (chart *model.Chart, err error) {
return
}
func (b *chartBiz) Batch(names ...string) (charts []*model.Chart, err error) {
do(func(d dao.Interface) {
charts, err = d.ChartBatch(names...)
})
return
}
func (b *chartBiz) Update(chart *model.Chart, user web.User) (err error) {
do(func(d dao.Interface) {
//chart.UpdatedAt = time.Now()
// chart.UpdatedAt = time.Now()
err = d.ChartUpdate(chart)
})
return
}
func (b *chartBiz) GetServiceCharts(name string) (charts []*model.Chart, err error) {
service, _, err := docker.ServiceInspect(name)
if err != nil {
return nil, err
}
// service, _, err := docker.ServiceInspect(name)
// if err != nil {
// return nil, err
// }
var categories []string
if label := service.Spec.Labels["swirl.metrics"]; label != "" {
categories = strings.Split(label, ",")
}
charts = append(charts, model.NewChart("cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`, "percent:100"))
charts = append(charts, model.NewChart("memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`, "size:bytes"))
charts = append(charts, model.NewChart("network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`, "size:bytes"))
charts = append(charts, model.NewChart("network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`, "size:bytes"))
for _, c := range categories {
if c == "java" {
charts = append(charts, model.NewChart("threads", "Threads", "${instance}", `jvm_threads_current{service="%s"}`, ""))
charts = append(charts, model.NewChart("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`, "time:s"))
} else if c == "go" {
charts = append(charts, model.NewChart("threads", "Threads", "${instance}", `go_threads{service="%s"}`, ""))
charts = append(charts, model.NewChart("goroutines", "Goroutines", "${instance}", `go_goroutines{service="%s"}`, ""))
charts = append(charts, model.NewChart("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`, "time:s"))
}
}
for i, c := range charts {
charts[i].Query = fmt.Sprintf(c.Query, name)
}
// if label := service.Spec.Labels["swirl.metrics"]; label != "" {
// names := strings.Split(label, ",")
// }
charts = b.builtin
return
}
@ -130,24 +141,33 @@ func (b *chartBiz) Panel(panel model.ChartPanel) (charts []*model.Chart, err err
return
}
// todo:
func (b *chartBiz) FetchDatas(charts []*model.Chart, period time.Duration) (data.Map, error) {
func (b *chartBiz) FetchDatas(key string, names []string, period time.Duration) (data.Map, error) {
charts, err := b.getCharts(names)
if err != nil {
return nil, err
}
datas := data.Map{}
end := time.Now()
start := end.Add(-period)
for _, chart := range charts {
query, err := b.formatQuery(chart, key)
if err != nil {
return nil, err
}
switch chart.Type {
case "line", "bar":
m, err := Metric.GetMatrix(chart.Query, chart.Label, start, end)
m, err := Metric.GetMatrix(query, chart.Label, start, end)
if err != nil {
log.Get("metric").Error(err)
log.Get("metric").Error(err, query)
} else {
datas[chart.Name] = m
}
case "pie", "table":
m, err := Metric.GetVector(chart.Query, chart.Label, end)
m, err := Metric.GetVector(query, chart.Label, end)
if err != nil {
log.Get("metric").Error(err)
log.Get("metric").Error(err, query)
} else {
datas[chart.Name] = m
}
@ -156,3 +176,49 @@ func (b *chartBiz) FetchDatas(charts []*model.Chart, period time.Duration) (data
}
return datas, nil
}
func (b *chartBiz) formatQuery(chart *model.Chart, key string) (string, error) {
if chart.Dashboard == "home" {
return chart.Query, nil
}
var errs []error
m := map[string]string{chart.Dashboard: key}
query := os.Expand(chart.Query, func(k string) string {
if v, ok := m[k]; ok {
return v
}
errs = append(errs, errors.New("invalid argument in query: "+chart.Query))
return ""
})
if len(errs) == 0 {
return query, nil
}
return "", errs[0]
}
func (b *chartBiz) getCharts(names []string) (charts []*model.Chart, err error) {
var (
customNames []string
customCharts []*model.Chart
)
for _, n := range names {
if n[0] == '$' {
for _, c := range b.builtin {
if c.Name == n {
charts = append(charts, c)
}
}
} else {
customNames = append(customNames, n)
}
}
if len(customNames) > 0 {
if customCharts, err = b.Batch(customNames...); err == nil {
charts = append(charts, customCharts...)
}
}
return
}

View File

@ -2,7 +2,6 @@ package biz
import (
"context"
"fmt"
"os"
"time"
@ -37,26 +36,26 @@ type metricBiz struct {
api lazy.Value
}
func (b *metricBiz) GetServiceCharts(service string, categories []string) (charts []model.ChartInfo) {
charts = append(charts, model.NewChartInfo("cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`))
charts = append(charts, model.NewChartInfo("memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`))
charts = append(charts, model.NewChartInfo("network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
charts = append(charts, model.NewChartInfo("network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
for _, c := range categories {
if c == "java" {
charts = append(charts, model.NewChartInfo("threads", "Threads", "${instance}", `jvm_threads_current{service="%s"}`))
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`))
} else if c == "go" {
charts = append(charts, model.NewChartInfo("threads", "Threads", "${instance}", `go_threads{service="%s"}`))
charts = append(charts, model.NewChartInfo("goroutines", "Goroutines", "${instance}", `go_goroutines{service="%s"}`))
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`))
}
}
for i, c := range charts {
charts[i].Query = fmt.Sprintf(c.Query, service)
}
return
}
// func (b *metricBiz) GetServiceCharts(service string, categories []string) (charts []model.ChartInfo) {
// charts = append(charts, model.NewChartInfo("cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`))
// charts = append(charts, model.NewChartInfo("memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`))
// charts = append(charts, model.NewChartInfo("network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
// charts = append(charts, model.NewChartInfo("network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
// for _, c := range categories {
// if c == "java" {
// charts = append(charts, model.NewChartInfo("threads", "Threads", "${instance}", `jvm_threads_current{service="%s"}`))
// charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`))
// } else if c == "go" {
// charts = append(charts, model.NewChartInfo("threads", "Threads", "${instance}", `go_threads{service="%s"}`))
// charts = append(charts, model.NewChartInfo("goroutines", "Goroutines", "${instance}", `go_goroutines{service="%s"}`))
// charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`))
// }
// }
// for i, c := range charts {
// charts[i].Query = fmt.Sprintf(c.Query, service)
// }
// return
// }
func (b *metricBiz) GetMatrix(query, label string, start, end time.Time) (lines []model.ChartLine, err error) {
api, err := b.getAPI()

View File

@ -33,3 +33,13 @@ func (b *settingBiz) Update(setting *model.Setting, user web.User) (err error) {
})
return
}
func (b *settingBiz) UpdateDashboard(name string, dashboard *model.ChartPanel, user web.User) (err error) {
do(func(d dao.Interface) {
err = d.UpdateDashboard(name, dashboard)
if err == nil {
Event.CreateSetting(model.EventActionUpdate, user)
}
})
return
}

View File

@ -1,8 +1,10 @@
package controller
import (
"strings"
"time"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/util/cast"
"github.com/cuigh/swirl/biz"
@ -11,27 +13,29 @@ import (
// ChartController is a controller of metric chart.
type ChartController struct {
List web.HandlerFunc `path:"/" name:"chart.list" authorize:"!" desc:"chart list page"`
Query web.HandlerFunc `path:"/query" name:"chart.query" authorize:"?" desc:"chart query"`
New web.HandlerFunc `path:"/new" name:"chart.new" authorize:"!" desc:"new chart page"`
Create web.HandlerFunc `path:"/new" method:"post" name:"chart.create" authorize:"!" desc:"create chart"`
Edit web.HandlerFunc `path:"/:name/edit" name:"chart.edit" authorize:"!" desc:"edit chart page"`
Delete web.HandlerFunc `path:"/:name/delete" method:"post" name:"chart.delete" authorize:"!" desc:"delete chart"`
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"chart.update" authorize:"!" desc:"update chart"`
Data web.HandlerFunc `path:"/data" name:"chart.data" authorize:"?" desc:"fetch chart datas"`
List web.HandlerFunc `path:"/" name:"chart.list" authorize:"!" desc:"chart list page"`
Query web.HandlerFunc `path:"/query" name:"chart.query" authorize:"?" desc:"chart query"`
New web.HandlerFunc `path:"/new" name:"chart.new" authorize:"!" desc:"new chart page"`
Create web.HandlerFunc `path:"/new" method:"post" name:"chart.create" authorize:"!" desc:"create chart"`
Edit web.HandlerFunc `path:"/:name/edit" name:"chart.edit" authorize:"!" desc:"edit chart page"`
Delete web.HandlerFunc `path:"/:name/delete" method:"post" name:"chart.delete" authorize:"!" desc:"delete chart"`
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"chart.update" authorize:"!" desc:"update chart"`
Data web.HandlerFunc `path:"/data" name:"chart.data" authorize:"?" desc:"fetch chart datas"`
SavePanel web.HandlerFunc `path:"/save_panel" method:"post" name:"chart.save_panel" authorize:"!" desc:"save panel"`
}
// Chart creates an instance of RoleController
func Chart() (c *ChartController) {
return &ChartController{
List: chartList,
Query: chartQuery,
New: chartNew,
Create: chartCreate,
Edit: chartEdit,
Update: chartUpdate,
Delete: chartDelete,
Data: chartData,
List: chartList,
Query: chartQuery,
New: chartNew,
Create: chartCreate,
Edit: chartEdit,
Update: chartUpdate,
Delete: chartDelete,
Data: chartData,
SavePanel: chartSavePanel,
}
}
@ -64,7 +68,7 @@ func chartQuery(ctx web.Context) error {
func chartNew(ctx web.Context) error {
m := newModel(ctx).Set("Chart", &model.Chart{
Width: 12,
Height: 50,
Height: 150,
Type: "line",
Dashboard: "service",
})
@ -109,35 +113,33 @@ func chartDelete(ctx web.Context) error {
return ajaxResult(ctx, err)
}
// todo:
func chartData(ctx web.Context) error {
period := cast.ToDuration(ctx.Q("time"), time.Hour)
dashboard := ctx.Q("dashboard")
var (
charts []*model.Chart
err error
)
if dashboard == "home" {
var setting *model.Setting
setting, err = biz.Setting.Get()
if err != nil {
return err
}
charts, err = biz.Chart.Panel(setting.Dashboard.Home)
} else if dashboard == "service" {
id := ctx.Q("id")
charts, err = biz.Chart.GetServiceCharts(id)
}
if err != nil {
return err
}
datas, err := biz.Chart.FetchDatas(charts, period)
charts := strings.Split(ctx.Q("charts"), ",")
key := ctx.Q("key")
datas, err := biz.Chart.FetchDatas(key, charts, period)
if err != nil {
return err
}
return ctx.JSON(datas)
}
func chartSavePanel(ctx web.Context) error {
data := struct {
Name string `json:"name"`
Key string `json:"key"`
model.ChartPanel
}{}
err := ctx.Bind(&data)
if err != nil {
return err
}
switch data.Name {
case "home":
err = biz.Setting.UpdateDashboard(data.Name, &data.ChartPanel, ctx.User())
default:
err = errors.New("unknown dashboard: " + data.Name)
}
return ajaxResult(ctx, err)
}

View File

@ -62,6 +62,7 @@ type Interface interface {
SettingGet() (setting *model.Setting, err error)
SettingUpdate(setting *model.Setting) error
UpdateDashboard(name string, dashboard *model.ChartPanel) error
ChartGet(name string) (*model.Chart, error)
ChartBatch(names ...string) ([]*model.Chart, error)

View File

@ -28,3 +28,17 @@ func (d *Dao) SettingUpdate(setting *model.Setting) (err error) {
})
return
}
func (d *Dao) UpdateDashboard(name string, dashboard *model.ChartPanel) (err error) {
d.do(func(db *database) {
update := bson.M{
"$set": bson.M{
"dashboard": bson.M{
name: dashboard,
},
},
}
err = db.C("setting").UpdateId(settingID, update)
})
return
}

View File

@ -21,17 +21,18 @@ type Chart struct {
Options data.Map `json:"options"`
}
func NewChart(name, title, label, query, unit string) *Chart {
func NewChart(dashboard, name, title, label, query, unit string) *Chart {
return &Chart{
Name: name,
Title: title,
Description: title,
Label: label,
Query: query,
Dashboard: dashboard,
Type: "line",
Unit: unit,
Width: 12,
Height: 50,
Height: 150,
}
}

View File

@ -73,13 +73,20 @@
</div>
<div class="level-right">
<div class="level-item">
<button id="btn-add" class="button is-success"><span class="icon"><i class="fas fa-plus"></i></span><span>{{ i18n("button.add") }}</span></button>
<button id="btn-add" class="button is-success">
<span class="icon"><i class="fas fa-plus"></i></span><span>{{ i18n("button.add") }}</span>
</button>
</div>
<div class="level-item">
<button id="btn-save" class="button is-info">
<span class="icon"><i class="fas fa-save"></i></span><span>{{ i18n("button.save") }}</span>
</button>
</div>
</div>
</nav>
<div id="div-charts" class="columns is-multiline">
{{ range .Charts }}
<div class="column is-{{ .Width }}" data-chart-name="{{ .Name }}" data-chart-type="{{ .Type }}" data-chart-unit="{{ .Unit }}" data-chart-height="{{ .Height }}">
<div class="column is-{{ .Width }}" data-chart-name="{{ .Name }}" data-chart-type="{{ .Type }}" data-chart-unit="{{ .Unit }}" data-chart-width="{{ .Width }}" data-chart-height="{{ .Height }}">
<div class="card">
<header class="card-header">
<p class="card-header-title">{{ .Title }}</p>
@ -100,11 +107,39 @@
{*</a>*}
</header>
<div class="card-content">
<canvas id="canvas_{{ .Name }}"></canvas>
<div style="height: {{ .Height }}px">
<canvas id="canvas_{{ .Name }}"></canvas>
</div>
</div>
</div>
</div>
{{ end }}
</div>
</section>
<div id="dlg-add-chart" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Add chart</p>
<button class="delete"></button>
</header>
<section class="modal-card-body" style="max-height: 400px; overflow-y: auto">
<nav id="nav-charts" class="panel">
<div class="panel-block">
<p class="control has-icons-left">
<input id="txt-query" class="input is-small" type="text" placeholder="Searching chart...">
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
</p>
</div>
</nav>
</section>
<footer class="modal-card-foot">
<button id="btn-add-chart" type="button" class="button is-primary">{{ i18n("button.confirm") }}</button>
<button type="button" class="button dismiss">{{ i18n("button.cancel") }}</button>
</footer>
</div>
</div>
{{ end }}

View File

@ -76,11 +76,14 @@
<div class="level-item">
<button id="btn-add" class="button is-success"><span class="icon"><i class="fas fa-plus"></i></span><span>{{ i18n("button.add") }}</span></button>
</div>
<div class="level-item">
<button id="btn-save" class="button is-info"><span class="icon"><i class="fas fa-save"></i></span><span>{{ i18n("button.save") }}</span></button>
</div>
</div>
</nav>
<div id="div-charts" class="columns is-multiline">
{{ range .Charts }}
<div class="column is-{{ .Width }}" data-chart-name="{{ .Name }}" data-chart-type="{{ .Type }}" data-chart-unit="{{ .Unit }}" data-chart-height="{{ .Height }}">
<div class="column is-{{ .Width }}" data-chart-name="{{ .Name }}" data-chart-type="{{ .Type }}" data-chart-unit="{{ .Unit }}" data-chart-width="{{ .Width }}" data-chart-height="{{ .Height }}">
<div class="card">
<header class="card-header">
<p class="card-header-title">{{ .Title }}</p>
@ -96,7 +99,9 @@
</a>
</header>
<div class="card-content">
<canvas id="canvas_{{ .Name }}"></canvas>
<div style="height: {{ .Height }}px">
<canvas id="canvas_{{ .Name }}"></canvas>
</div>
</div>
</div>
</div>