mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 23:02:02 +00:00
Add chart management page
This commit is contained in:
parent
93be76045c
commit
b2893f5a99
5
Gopkg.lock
generated
5
Gopkg.lock
generated
@ -56,7 +56,8 @@
|
|||||||
"util/cast",
|
"util/cast",
|
||||||
"util/debug",
|
"util/debug",
|
||||||
"util/i18n",
|
"util/i18n",
|
||||||
"util/lazy"
|
"util/lazy",
|
||||||
|
"util/run"
|
||||||
]
|
]
|
||||||
revision = "772f5a654db9ee95a5dc851e9562253bc0b2f11d"
|
revision = "772f5a654db9ee95a5dc851e9562253bc0b2f11d"
|
||||||
|
|
||||||
@ -224,6 +225,6 @@
|
|||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "c829e2bd1f0dc356225caad79652e6732d0572e0e59580a5e6c5b6baab134755"
|
inputs-digest = "dea823c7646b8b71101e0443b1c0c36a723989261f733ad4cb44c6e9b0d3942d"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
@ -171,6 +171,14 @@ dd {
|
|||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header-title, .card-header-icon, .card-footer-item {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
@ -246,7 +254,7 @@ dd {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea.code{
|
textarea.code {
|
||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,7 +451,7 @@ var Swirl;
|
|||||||
}
|
}
|
||||||
class RegexRule {
|
class RegexRule {
|
||||||
validate($form, $input, arg) {
|
validate($form, $input, arg) {
|
||||||
let regex = new RegExp(arg, 'i');
|
let regex = new RegExp(arg);
|
||||||
let value = $.trim($input.val());
|
let value = $.trim($input.val());
|
||||||
return { ok: !value || regex.test(value) };
|
return { ok: !value || regex.test(value) };
|
||||||
}
|
}
|
||||||
@ -1858,149 +1858,298 @@ var Swirl;
|
|||||||
})(Swirl || (Swirl = {}));
|
})(Swirl || (Swirl = {}));
|
||||||
var Swirl;
|
var Swirl;
|
||||||
(function (Swirl) {
|
(function (Swirl) {
|
||||||
var Service;
|
var Core;
|
||||||
(function (Service) {
|
(function (Core) {
|
||||||
class MetricChartOptions {
|
class GraphOptions {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.type = "line";
|
this.type = "line";
|
||||||
|
this.width = 12;
|
||||||
this.height = 50;
|
this.height = 50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class MetricChart {
|
Core.GraphOptions = GraphOptions;
|
||||||
|
class Graph {
|
||||||
constructor(elem, opts) {
|
constructor(elem, opts) {
|
||||||
this.colors = [
|
this.$elem = $(elem);
|
||||||
'rgb(255, 99, 132)',
|
this.opts = $.extend(new GraphOptions(), opts);
|
||||||
'rgb(75, 192, 192)',
|
this.name = this.$elem.data("chart-name");
|
||||||
'rgb(255, 159, 64)',
|
}
|
||||||
'rgb(54, 162, 235)',
|
getName() {
|
||||||
'rgb(153, 102, 255)',
|
return this.name;
|
||||||
'rgb(255, 205, 86)',
|
}
|
||||||
'rgb(201, 203, 207)',
|
}
|
||||||
];
|
Core.Graph = Graph;
|
||||||
opts = $.extend(new MetricChartOptions(), opts);
|
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 = {
|
this.config = {
|
||||||
type: opts.type,
|
type: opts.type,
|
||||||
data: {},
|
data: {},
|
||||||
options: {
|
options: {
|
||||||
title: {
|
|
||||||
text: opts.title || 'NONE'
|
|
||||||
},
|
|
||||||
animation: {
|
animation: {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
},
|
},
|
||||||
scales: {
|
|
||||||
xAxes: [{
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
unit: 'minute',
|
|
||||||
tooltipFormat: 'YYYY/MM/DD HH:mm:ss',
|
|
||||||
displayFormats: {
|
|
||||||
minute: 'HH:mm'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
yAxes: [{}]
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (opts.labelX) {
|
this.fillConfig();
|
||||||
this.config.options.scales.xAxes[0].scaleLabel = {
|
this.ctx = $(elem).find("canvas").get(0).getContext('2d');
|
||||||
display: true,
|
|
||||||
labelString: opts.labelX,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (opts.labelY) {
|
|
||||||
this.config.options.scales.yAxes[0].scaleLabel = {
|
|
||||||
display: true,
|
|
||||||
labelString: opts.labelY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (opts.tickY) {
|
|
||||||
this.config.options.scales.yAxes[0].ticks = {
|
|
||||||
callback: opts.tickY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let ctx = $(elem).get(0).getContext('2d');
|
|
||||||
if (opts.height) {
|
if (opts.height) {
|
||||||
ctx.canvas.height = opts.height;
|
this.ctx.canvas.height = opts.height;
|
||||||
}
|
}
|
||||||
this.chart = new Chart(ctx, this.config);
|
this.chart = new Chart(this.ctx, this.config);
|
||||||
}
|
}
|
||||||
setData(datasets) {
|
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) => {
|
datasets.forEach((ds, i) => {
|
||||||
let color = (i < this.colors.length) ? this.colors[i] : this.colors[0];
|
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.backgroundColor = Chart.helpers.color(color).alpha(0.3).rgbString();
|
||||||
ds.borderColor = color;
|
ds.borderColor = color;
|
||||||
ds.borderWidth = 2;
|
ds.borderWidth = 2;
|
||||||
ds.pointRadius = 2;
|
ds.pointRadius = 2;
|
||||||
});
|
});
|
||||||
this.config.data.datasets = datasets;
|
this.config.data.datasets = d;
|
||||||
this.chart.update();
|
this.chart.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class StatsPage {
|
Core.MatrixGraph = MatrixGraph;
|
||||||
constructor() {
|
class GraphFactory {
|
||||||
this.chartOptions = {
|
static create(elem) {
|
||||||
"cpu": { tickY: (value) => value + '%' },
|
let $elem = $(elem);
|
||||||
"memory": { tickY: StatsPage.formatSize },
|
let opts = {
|
||||||
"network_in": { tickY: StatsPage.formatSize },
|
type: $elem.data("chart-type"),
|
||||||
"network_out": { tickY: StatsPage.formatSize },
|
unit: $elem.data("chart-unit"),
|
||||||
"threads": {},
|
height: $elem.data("chart-height"),
|
||||||
"goroutines": {},
|
colors: $elem.data("chart-colors"),
|
||||||
"gc_duration": { tickY: (value) => value * 1000 + 'ms' },
|
|
||||||
};
|
};
|
||||||
this.charts = {};
|
switch (opts.type) {
|
||||||
let $cb_time = $("#cb-time");
|
case "value":
|
||||||
if ($cb_time.length == 0) {
|
return new ValueGraph($elem, opts);
|
||||||
return;
|
case "line":
|
||||||
|
case "bar":
|
||||||
|
return new MatrixGraph($elem, opts);
|
||||||
|
case "pie":
|
||||||
|
return new VectorGraph($elem, opts);
|
||||||
}
|
}
|
||||||
$cb_time.change(this.loadData.bind(this));
|
return null;
|
||||||
$("#cb-refresh").change(() => {
|
}
|
||||||
if (this.timer) {
|
}
|
||||||
clearTimeout(this.timer);
|
Core.GraphFactory = GraphFactory;
|
||||||
this.timer = null;
|
class GraphPanelOptions {
|
||||||
}
|
constructor() {
|
||||||
else {
|
this.time = "30m";
|
||||||
this.refreshData();
|
this.refreshInterval = 15000;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
$.each(this.chartOptions, (name, opt) => {
|
Core.GraphPanelOptions = GraphPanelOptions;
|
||||||
let $el = $("#canvas_" + name);
|
class GraphPanel {
|
||||||
if ($el.length > 0) {
|
constructor(elems, opts) {
|
||||||
this.charts[name] = new MetricChart($el, opt);
|
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();
|
this.refreshData();
|
||||||
}
|
}
|
||||||
refreshData() {
|
refreshData() {
|
||||||
this.loadData();
|
this.loadData();
|
||||||
if ($("#cb-refresh").prop("checked")) {
|
if (this.opts.refreshInterval > 0) {
|
||||||
this.timer = setTimeout(this.refreshData.bind(this), 15000);
|
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() {
|
loadData() {
|
||||||
let time = $("#cb-time").val();
|
let args = {
|
||||||
$ajax.get(`metrics`, { time: time }).json((d) => {
|
dashboard: this.opts.name,
|
||||||
$.each(this.charts, (name, chart) => {
|
time: this.opts.time,
|
||||||
if (d[name]) {
|
};
|
||||||
chart.setData(d[name]);
|
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()]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
static formatSize(size) {
|
}
|
||||||
if (size < 1024) {
|
Core.GraphPanel = GraphPanel;
|
||||||
return size.toString() + 'B';
|
})(Core = Swirl.Core || (Swirl.Core = {}));
|
||||||
}
|
})(Swirl || (Swirl = {}));
|
||||||
else if (size < 1048576) {
|
var Swirl;
|
||||||
return (size / 1024).toFixed(2) + 'K';
|
(function (Swirl) {
|
||||||
}
|
var Service;
|
||||||
else if (size < 1073741824) {
|
(function (Service) {
|
||||||
return (size / 1048576).toFixed(2) + 'M';
|
var Modal = Swirl.Core.Modal;
|
||||||
}
|
var GraphPanel = Swirl.Core.GraphPanel;
|
||||||
else {
|
class StatsPage {
|
||||||
return (size / 1073741824).toFixed(2) + 'G';
|
constructor() {
|
||||||
|
let $cb_time = $("#cb-time");
|
||||||
|
if ($cb_time.length == 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.panel = new GraphPanel($("#div-charts").children("div"), {
|
||||||
|
name: "service",
|
||||||
|
id: $("#h2-service-name").text()
|
||||||
|
});
|
||||||
|
$("#btn-add").click(() => {
|
||||||
|
Modal.alert("Coming soon...");
|
||||||
|
});
|
||||||
|
$cb_time.change(e => {
|
||||||
|
this.panel.setTime($(e.target).val());
|
||||||
|
});
|
||||||
|
$("#cb-refresh").change(e => {
|
||||||
|
if ($(e.target).prop("checked")) {
|
||||||
|
this.panel.refresh();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.panel.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Service.StatsPage = StatsPage;
|
Service.StatsPage = StatsPage;
|
||||||
@ -2305,4 +2454,77 @@ var Swirl;
|
|||||||
Volume.NewPage = NewPage;
|
Volume.NewPage = NewPage;
|
||||||
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
|
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
|
||||||
})(Swirl || (Swirl = {}));
|
})(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
|
//# sourceMappingURL=swirl.js.map
|
File diff suppressed because one or more lines are too long
70
assets/swirl/ts/chart/list.ts
Normal file
70
assets/swirl/ts/chart/list.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
///<reference path="../core/core.ts" />
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListPage {
|
||||||
|
private fb: FilterBox;
|
||||||
|
private $charts: JQuery;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.$charts = $("#div-charts").children();
|
||||||
|
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
|
||||||
|
|
||||||
|
// bind events
|
||||||
|
Dispatcher.bind("#div-charts").on("delete-chart", this.deleteChart.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteChart(e: JQueryEventObject) {
|
||||||
|
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<AjaxResult>(r => {
|
||||||
|
$container.remove();
|
||||||
|
dlg.close();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterCharts(text: string) {
|
||||||
|
if (!text) {
|
||||||
|
this.$charts.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$charts.each((i, elem) => {
|
||||||
|
let $elem = $(elem),
|
||||||
|
texts: string[] = [
|
||||||
|
$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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
353
assets/swirl/ts/core/graph.ts
Normal file
353
assets/swirl/ts/core/graph.ts
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
namespace Swirl.Core {
|
||||||
|
export class GraphOptions {
|
||||||
|
type?: string = "line";
|
||||||
|
unit?: string;
|
||||||
|
width?: number = 12;
|
||||||
|
height?: number = 50;
|
||||||
|
colors?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class Graph {
|
||||||
|
protected $elem: JQuery;
|
||||||
|
protected opts?: GraphOptions;
|
||||||
|
private readonly name: string;
|
||||||
|
|
||||||
|
protected constructor(elem: string | Element | JQuery, opts?: GraphOptions) {
|
||||||
|
this.$elem = $(elem);
|
||||||
|
this.opts = $.extend(new GraphOptions(), opts);
|
||||||
|
this.name = this.$elem.data("chart-name");
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract setSize(w: number, h: number): void;
|
||||||
|
|
||||||
|
abstract setData(d: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple value
|
||||||
|
*/
|
||||||
|
export class ValueGraph extends Graph {
|
||||||
|
private $canvas: JQuery;
|
||||||
|
|
||||||
|
constructor(elem: string | Element | JQuery, opts?: GraphOptions) {
|
||||||
|
super(elem, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(d: any): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(w: number, h: number): void {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComplexGraph extends Graph {
|
||||||
|
protected chart: Chart;
|
||||||
|
protected ctx: CanvasRenderingContext2D;
|
||||||
|
protected config: Chart.ChartConfiguration;
|
||||||
|
protected static defaultColors = [
|
||||||
|
'rgb(255, 99, 132)', // red
|
||||||
|
'rgb(75, 192, 192)', // green
|
||||||
|
'rgb(255, 159, 64)', // orange
|
||||||
|
'rgb(54, 162, 235)', // blue
|
||||||
|
'rgb(153, 102, 255)', // purple
|
||||||
|
'rgb(255, 205, 86)', // yellow
|
||||||
|
'rgb(201, 203, 207)', // grey
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(elem: string | Element | JQuery, opts?: GraphOptions) {
|
||||||
|
super(elem, opts);
|
||||||
|
|
||||||
|
if (!this.opts.colors) {
|
||||||
|
this.opts.colors = ComplexGraph.defaultColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.config = {
|
||||||
|
type: opts.type,
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
// title: {
|
||||||
|
// // display: true,
|
||||||
|
// text: opts.title || 'NONE'
|
||||||
|
// },
|
||||||
|
// legend: {
|
||||||
|
// position: "bottom"
|
||||||
|
// },
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
// easing: 'easeOutBounce',
|
||||||
|
},
|
||||||
|
// tooltips: {
|
||||||
|
// callbacks: {
|
||||||
|
// label: function (tooltipItem: Chart.ChartTooltipItem, data: Chart.ChartData) {
|
||||||
|
// let label = data.datasets[tooltipItem.datasetIndex].label || '';
|
||||||
|
// if (label) {
|
||||||
|
// label += ': ';
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let p = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
|
||||||
|
// if (typeof p == "number") {
|
||||||
|
// label += ComplexGraph.formatValue(p, opts.unit);
|
||||||
|
// } else {
|
||||||
|
// label += ComplexGraph.formatValue(p.y, opts.unit);
|
||||||
|
// }
|
||||||
|
// return label;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.fillConfig();
|
||||||
|
|
||||||
|
this.ctx = (<HTMLCanvasElement>$(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: any): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(w: number, h: number): void {
|
||||||
|
this.ctx.canvas.height = h;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fillConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static formatValue(value: number, unit: string): string {
|
||||||
|
switch (unit) {
|
||||||
|
case "percent:100":
|
||||||
|
return value.toFixed(1) + "%";
|
||||||
|
case "percent:1":
|
||||||
|
return (value * 100).toFixed(1) + "%";
|
||||||
|
case "time:s":
|
||||||
|
if (value < 1) { // 1
|
||||||
|
return (value * 1000).toFixed(0) + 'ms';
|
||||||
|
} else {
|
||||||
|
return value.toFixed(2) + 's';
|
||||||
|
}
|
||||||
|
case "size:bytes":
|
||||||
|
if (value < 1024) { // 1K
|
||||||
|
return value.toString() + 'B';
|
||||||
|
} else if (value < 1048576) { // 1M
|
||||||
|
return (value / 1024).toFixed(2) + 'K';
|
||||||
|
} else if (value < 1073741824) { // 1G
|
||||||
|
return (value / 1048576).toFixed(2) + 'M';
|
||||||
|
} else {
|
||||||
|
return (value / 1073741824).toFixed(2) + 'G';
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pie etc.
|
||||||
|
*/
|
||||||
|
export class VectorGraph extends ComplexGraph {
|
||||||
|
protected fillConfig() {
|
||||||
|
this.config.options.legend = {
|
||||||
|
position: "right"
|
||||||
|
};
|
||||||
|
this.config.options.tooltips = {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem: Chart.ChartTooltipItem, data: Chart.ChartData): string => {
|
||||||
|
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: any): void {
|
||||||
|
// data = {
|
||||||
|
// datasets: [{
|
||||||
|
// data: [10, 20, 30]
|
||||||
|
// }],
|
||||||
|
//
|
||||||
|
// // These labels appear in the legend and in the tooltips when hovering different arcs
|
||||||
|
// labels: [
|
||||||
|
// 'Red',
|
||||||
|
// 'Yellow',
|
||||||
|
// 'Blue'
|
||||||
|
// ]
|
||||||
|
// };
|
||||||
|
this.config.data.datasets = [{
|
||||||
|
data: d.data,
|
||||||
|
backgroundColor: this.opts.colors,
|
||||||
|
}];
|
||||||
|
this.config.data.labels = d.labels;
|
||||||
|
// this.config.data.datasets = [{
|
||||||
|
// data: [10, 20, 30],
|
||||||
|
// backgroundColor: this.opts.colors,
|
||||||
|
// }];
|
||||||
|
// this.config.data.labels = [
|
||||||
|
// 'Red',
|
||||||
|
// 'Yellow',
|
||||||
|
// 'Blue'
|
||||||
|
// ];
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Line/Bar etc.
|
||||||
|
*/
|
||||||
|
export class MatrixGraph extends ComplexGraph {
|
||||||
|
constructor(elem: string | Element | JQuery, opts?: GraphOptions) {
|
||||||
|
super(elem, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fillConfig() {
|
||||||
|
this.config.options.scales = {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
// min: new Date(),
|
||||||
|
// max: new Date(),
|
||||||
|
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: number) => ComplexGraph.formatValue(n, this.opts.unit),
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
this.config.options.tooltips = {
|
||||||
|
callbacks: {
|
||||||
|
label: (tooltipItem: Chart.ChartTooltipItem, data: Chart.ChartData): string => {
|
||||||
|
let label = data.datasets[tooltipItem.datasetIndex].label + ": ";
|
||||||
|
let p: any = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
|
||||||
|
label += ComplexGraph.formatValue(p.y, this.opts.unit);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(d: any): void {
|
||||||
|
let datasets = <Chart.ChartDataSets[]>(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;
|
||||||
|
// ds.fill = false;
|
||||||
|
});
|
||||||
|
this.config.data.datasets = d;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphFactory {
|
||||||
|
static create(elem: string | Element | JQuery): Graph {
|
||||||
|
let $elem = $(elem);
|
||||||
|
let opts: GraphOptions = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphPanelOptions {
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
time?: string = "30m";
|
||||||
|
refreshInterval?: number = 15000; // ms
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphPanel {
|
||||||
|
private opts: GraphPanelOptions;
|
||||||
|
private charts: Graph[] = [];
|
||||||
|
private timer: number;
|
||||||
|
|
||||||
|
constructor(elems: string | Element | JQuery, opts?: GraphPanelOptions) {
|
||||||
|
this.opts = $.extend(new GraphPanelOptions(), opts);
|
||||||
|
|
||||||
|
$(elems).each((i, e) => {
|
||||||
|
let g = GraphFactory.create(e);
|
||||||
|
if (g != null) {
|
||||||
|
this.charts.push(g);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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: string) {
|
||||||
|
this.opts.time = time;
|
||||||
|
this.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadData() {
|
||||||
|
let args: any = {
|
||||||
|
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: { [index: string]: Chart.ChartDataSets[] }) => {
|
||||||
|
$.each(this.charts, (i: number, g: Graph) => {
|
||||||
|
if (d[g.getName()]) {
|
||||||
|
g.setData(d[g.getName()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
// namespace Swirl.Core {
|
|
||||||
// export class ListBasePage {
|
|
||||||
// protected $table: ListTable;
|
|
||||||
//
|
|
||||||
// constructor() {
|
|
||||||
// this.$table = new ListTable("#table-items");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// export class TestListPage extends ListBasePage {
|
|
||||||
// constructor() {
|
|
||||||
// super();
|
|
||||||
// this.$table.on("", null);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -188,7 +188,7 @@ namespace Swirl.Core {
|
|||||||
*/
|
*/
|
||||||
class RegexRule implements ValidationRule {
|
class RegexRule implements ValidationRule {
|
||||||
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
validate($form: JQuery, $input: JQuery, arg?: string): {ok: boolean, error?: string} {
|
||||||
let regex = new RegExp(arg, 'i');
|
let regex = new RegExp(arg);
|
||||||
let value = $.trim($input.val());
|
let value = $.trim($input.val());
|
||||||
return {ok: !value || regex.test(value)};
|
return {ok: !value || regex.test(value)};
|
||||||
}
|
}
|
||||||
|
17
assets/swirl/ts/index.ts
Normal file
17
assets/swirl/ts/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
///<reference path="core/core.ts" />
|
||||||
|
///<reference path="core/graph.ts" />
|
||||||
|
namespace Swirl {
|
||||||
|
import Modal = Swirl.Core.Modal;
|
||||||
|
import GraphPanel = Swirl.Core.GraphPanel;
|
||||||
|
|
||||||
|
export class IndexPage {
|
||||||
|
private panel: GraphPanel;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.panel = new GraphPanel($("#div-charts").children("div"), {name: "home"});
|
||||||
|
$("#btn-add").click(() => {
|
||||||
|
Modal.alert("Coming soon...");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,118 +1,11 @@
|
|||||||
///<reference path="../core/core.ts" />
|
///<reference path="../core/core.ts" />
|
||||||
|
///<reference path="../core/graph.ts" />
|
||||||
namespace Swirl.Service {
|
namespace Swirl.Service {
|
||||||
class MetricChartOptions {
|
import Modal = Swirl.Core.Modal;
|
||||||
type?: string = "line";
|
import GraphPanel = Swirl.Core.GraphPanel;
|
||||||
height?: number = 50;
|
|
||||||
title?: string;
|
|
||||||
labelX?: string;
|
|
||||||
labelY?: string;
|
|
||||||
tickY?: (value: number) => string;
|
|
||||||
// tooltipLabel?: (tooltipItem: Chart.ChartTooltipItem, data: Chart.ChartData) => string | string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class MetricChart {
|
|
||||||
private chart: any;
|
|
||||||
private readonly config: any;
|
|
||||||
private colors = [
|
|
||||||
'rgb(255, 99, 132)', // red
|
|
||||||
'rgb(75, 192, 192)', // green
|
|
||||||
'rgb(255, 159, 64)', // orange
|
|
||||||
'rgb(54, 162, 235)', // blue
|
|
||||||
'rgb(153, 102, 255)', // purple
|
|
||||||
'rgb(255, 205, 86)', // yellow
|
|
||||||
'rgb(201, 203, 207)', // grey
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(elem: string | Element | JQuery, opts?: MetricChartOptions) {
|
|
||||||
opts = $.extend(new MetricChartOptions(), opts);
|
|
||||||
this.config = {
|
|
||||||
type: opts.type,
|
|
||||||
data: {},
|
|
||||||
options: {
|
|
||||||
title: {
|
|
||||||
// display: true,
|
|
||||||
text: opts.title || 'NONE'
|
|
||||||
},
|
|
||||||
// legend: {
|
|
||||||
// position: "bottom"
|
|
||||||
// },
|
|
||||||
animation: {
|
|
||||||
duration: 0,
|
|
||||||
// easing: 'easeOutBounce',
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
xAxes: [{
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
// min: new Date(),
|
|
||||||
// max: new Date(),
|
|
||||||
unit: 'minute',
|
|
||||||
tooltipFormat: 'YYYY/MM/DD HH:mm:ss',
|
|
||||||
displayFormats: {
|
|
||||||
minute: 'HH:mm'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
yAxes: [{}]
|
|
||||||
},
|
|
||||||
// tooltips: {
|
|
||||||
// callbacks: {
|
|
||||||
// label: opts.tooltipLabel,
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (opts.labelX) {
|
|
||||||
this.config.options.scales.xAxes[0].scaleLabel = {
|
|
||||||
display: true,
|
|
||||||
labelString: opts.labelX,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (opts.labelY) {
|
|
||||||
this.config.options.scales.yAxes[0].scaleLabel = {
|
|
||||||
display: true,
|
|
||||||
labelString: opts.labelY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (opts.tickY) {
|
|
||||||
this.config.options.scales.yAxes[0].ticks = {
|
|
||||||
callback: opts.tickY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctx = (<HTMLCanvasElement>$(elem).get(0)).getContext('2d');
|
|
||||||
if (opts.height) {
|
|
||||||
ctx.canvas.height = opts.height;
|
|
||||||
}
|
|
||||||
this.chart = new Chart(ctx, this.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setData(datasets: Chart.ChartDataSets[]) {
|
|
||||||
datasets.forEach((ds, i) => {
|
|
||||||
let color = (i < this.colors.length) ? this.colors[i] : this.colors[0];
|
|
||||||
ds.backgroundColor = Chart.helpers.color(color).alpha(0.3).rgbString();
|
|
||||||
ds.borderColor = color;
|
|
||||||
ds.borderWidth = 2;
|
|
||||||
ds.pointRadius = 2;
|
|
||||||
// ds.fill = false;
|
|
||||||
});
|
|
||||||
this.config.data.datasets = datasets;
|
|
||||||
this.chart.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StatsPage {
|
export class StatsPage {
|
||||||
private chartOptions: { [index: string]: MetricChartOptions } = {
|
private panel: GraphPanel;
|
||||||
"cpu": {tickY: (value: number): string => value + '%'},
|
|
||||||
"memory": {tickY: StatsPage.formatSize},
|
|
||||||
"network_in": {tickY: StatsPage.formatSize},
|
|
||||||
"network_out": {tickY: StatsPage.formatSize},
|
|
||||||
"threads": {},
|
|
||||||
"goroutines": {},
|
|
||||||
"gc_duration": {tickY: (value: number): string => value * 1000 + 'ms'},
|
|
||||||
};
|
|
||||||
private charts: { [index: string]: MetricChart } = {};
|
|
||||||
private timer: number;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let $cb_time = $("#cb-time");
|
let $cb_time = $("#cb-time");
|
||||||
@ -120,53 +13,24 @@ namespace Swirl.Service {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$cb_time.change(this.loadData.bind(this));
|
this.panel = new GraphPanel($("#div-charts").children("div"), {
|
||||||
$("#cb-refresh").change(() => {
|
name: "service",
|
||||||
if (this.timer) {
|
id: $("#h2-service-name").text()
|
||||||
clearTimeout(this.timer);
|
});
|
||||||
this.timer = null;
|
|
||||||
|
$("#btn-add").click(() => {
|
||||||
|
Modal.alert("Coming soon...");
|
||||||
|
});
|
||||||
|
$cb_time.change(e => {
|
||||||
|
this.panel.setTime($(e.target).val());
|
||||||
|
});
|
||||||
|
$("#cb-refresh").change(e => {
|
||||||
|
if ($(e.target).prop("checked")) {
|
||||||
|
this.panel.refresh();
|
||||||
} else {
|
} else {
|
||||||
this.refreshData();
|
this.panel.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$.each(this.chartOptions, (name, opt) => {
|
|
||||||
let $el = $("#canvas_" + name);
|
|
||||||
if ($el.length > 0) {
|
|
||||||
this.charts[name] = new MetricChart($el, opt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
private refreshData() {
|
|
||||||
this.loadData();
|
|
||||||
if ($("#cb-refresh").prop("checked")) {
|
|
||||||
this.timer = setTimeout(this.refreshData.bind(this), 15000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadData() {
|
|
||||||
let time = $("#cb-time").val();
|
|
||||||
$ajax.get(`metrics`, {time: time}).json((d: {[index: string]: Chart.ChartDataSets[]}) => {
|
|
||||||
$.each(this.charts, (name: string, chart: MetricChart) => {
|
|
||||||
if (d[name]) {
|
|
||||||
chart.setData(d[name]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static formatSize(size: number): string {
|
|
||||||
if (size < 1024) { // 1K
|
|
||||||
return size.toString() + 'B';
|
|
||||||
} else if (size < 1048576) { // 1M
|
|
||||||
return (size / 1024).toFixed(2) + 'K';
|
|
||||||
} else if (size < 1073741824) { // 1G
|
|
||||||
return (size / 1048576).toFixed(2) + 'M';
|
|
||||||
} else {
|
|
||||||
return (size / 1073741824).toFixed(2) + 'G';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
158
biz/chart.go
Normal file
158
biz/chart.go
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
package biz
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cuigh/auxo/data"
|
||||||
|
"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{}
|
||||||
|
|
||||||
|
type chartBiz struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chartBiz) List() (charts []*model.Chart, err error) {
|
||||||
|
do(func(d dao.Interface) {
|
||||||
|
charts, err = d.ChartList()
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
err = d.ChartCreate(chart)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chartBiz) Delete(id string, user web.User) (err error) {
|
||||||
|
do(func(d dao.Interface) {
|
||||||
|
err = d.ChartDelete(id)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chartBiz) Get(name string) (chart *model.Chart, err error) {
|
||||||
|
do(func(d dao.Interface) {
|
||||||
|
chart, err = d.ChartGet(name)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chartBiz) Update(chart *model.Chart, user web.User) (err error) {
|
||||||
|
do(func(d dao.Interface) {
|
||||||
|
//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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint: gocyclo
|
||||||
|
func (b *chartBiz) Panel(panel model.ChartPanel) (charts []*model.Chart, err error) {
|
||||||
|
do(func(d dao.Interface) {
|
||||||
|
if len(panel.Charts) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
names := make([]string, len(panel.Charts))
|
||||||
|
for i, c := range panel.Charts {
|
||||||
|
names[i] = c.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var cs []*model.Chart
|
||||||
|
cs, err = d.ChartBatch(names...)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cs) > 0 {
|
||||||
|
m := make(map[string]*model.Chart)
|
||||||
|
for _, c := range cs {
|
||||||
|
m[c.Name] = c
|
||||||
|
}
|
||||||
|
for _, c := range panel.Charts {
|
||||||
|
if chart := m[c.Name]; chart != nil {
|
||||||
|
if c.Width > 0 {
|
||||||
|
chart.Width = c.Width
|
||||||
|
}
|
||||||
|
if c.Height > 0 {
|
||||||
|
chart.Height = c.Height
|
||||||
|
}
|
||||||
|
if len(c.Colors) > 0 {
|
||||||
|
chart.Colors = c.Colors
|
||||||
|
}
|
||||||
|
charts = append(charts, chart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo:
|
||||||
|
func (b *chartBiz) FetchDatas(charts []*model.Chart, period time.Duration) (data.Map, error) {
|
||||||
|
datas := data.Map{}
|
||||||
|
end := time.Now()
|
||||||
|
start := end.Add(-period)
|
||||||
|
for _, chart := range charts {
|
||||||
|
switch chart.Type {
|
||||||
|
case "line", "bar":
|
||||||
|
m, err := Metric.GetMatrix(chart.Query, chart.Label, start, end)
|
||||||
|
if err != nil {
|
||||||
|
log.Get("metric").Error(err)
|
||||||
|
} else {
|
||||||
|
datas[chart.Name] = m
|
||||||
|
}
|
||||||
|
case "pie", "table":
|
||||||
|
m, err := Metric.GetVector(chart.Query, chart.Label, end)
|
||||||
|
if err != nil {
|
||||||
|
log.Get("metric").Error(err)
|
||||||
|
} else {
|
||||||
|
datas[chart.Name] = m
|
||||||
|
}
|
||||||
|
case "value":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return datas, nil
|
||||||
|
}
|
@ -3,6 +3,7 @@ package biz
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cuigh/auxo/ext/times"
|
"github.com/cuigh/auxo/ext/times"
|
||||||
@ -37,18 +38,18 @@ type metricBiz struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *metricBiz) GetServiceCharts(service string, categories []string) (charts []model.ChartInfo) {
|
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("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("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_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)`))
|
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 {
|
for _, c := range categories {
|
||||||
if c == "java" {
|
if c == "java" {
|
||||||
charts = append(charts, model.NewChartInfo("threads", "Threads", "instance", `jvm_threads_current{service="%s"}`))
|
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])`))
|
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`))
|
||||||
} else if c == "go" {
|
} else if c == "go" {
|
||||||
charts = append(charts, model.NewChartInfo("threads", "Threads", "instance", `go_threads{service="%s"}`))
|
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("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)`))
|
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, c := range charts {
|
for i, c := range charts {
|
||||||
@ -75,7 +76,7 @@ func (b *metricBiz) GetMatrix(query, label string, start, end time.Time) (lines
|
|||||||
|
|
||||||
matrix := value.(pmodel.Matrix)
|
matrix := value.(pmodel.Matrix)
|
||||||
for _, stream := range matrix {
|
for _, stream := range matrix {
|
||||||
line := model.ChartLine{Label: string(stream.Metric[pmodel.LabelName(label)])}
|
line := model.ChartLine{Label: b.formatLabel(label, stream.Metric)}
|
||||||
for _, v := range stream.Values {
|
for _, v := range stream.Values {
|
||||||
p := model.ChartPoint{
|
p := model.ChartPoint{
|
||||||
X: int64(v.Timestamp),
|
X: int64(v.Timestamp),
|
||||||
@ -103,20 +104,25 @@ func (b *metricBiz) GetScalar(query string, t time.Time) (v float64, err error)
|
|||||||
return float64(scalar.Value), nil
|
return float64(scalar.Value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *metricBiz) GetVector(query string, t time.Time) (values []float64, err error) {
|
func (b *metricBiz) GetVector(query, label string, t time.Time) (cv model.ChartVector, err error) {
|
||||||
api, err := b.getAPI()
|
var api papi.API
|
||||||
|
api, err = b.getAPI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := api.Query(context.Background(), query, t)
|
var value pmodel.Value
|
||||||
|
value, err = api.Query(context.Background(), query, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vector := value.(pmodel.Vector)
|
vector := value.(pmodel.Vector)
|
||||||
for _, sample := range vector {
|
for _, sample := range vector {
|
||||||
values = append(values, float64(sample.Value))
|
cv.Data = append(cv.Data, float64(sample.Value))
|
||||||
|
if label != "" {
|
||||||
|
cv.Labels = append(cv.Labels, b.formatLabel(label, sample.Metric))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -143,3 +149,12 @@ func (b *metricBiz) getAPI() (api papi.API, err error) {
|
|||||||
}
|
}
|
||||||
return v.(papi.API), nil
|
return v.(papi.API), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *metricBiz) formatLabel(label string, metric pmodel.Metric) string {
|
||||||
|
return os.Expand(label, func(key string) string {
|
||||||
|
if s := string(metric[pmodel.LabelName(key)]); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "[" + key + "]"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -45,6 +45,12 @@ field.driver: Driver
|
|||||||
field.scope: Scope
|
field.scope: Scope
|
||||||
field.role: Roles
|
field.role: Roles
|
||||||
field.user: Users
|
field.user: Users
|
||||||
|
field.width: Width
|
||||||
|
field.height: Height
|
||||||
|
field.unit: Unit
|
||||||
|
field.title: Title
|
||||||
|
field.label: Label
|
||||||
|
field.dashboard: Dashboard
|
||||||
|
|
||||||
# menu
|
# menu
|
||||||
menu.home: Home
|
menu.home: Home
|
||||||
@ -69,6 +75,7 @@ menu.role: Roles
|
|||||||
menu.user: Users
|
menu.user: Users
|
||||||
menu.setting: Settings
|
menu.setting: Settings
|
||||||
menu.event: Events
|
menu.event: Events
|
||||||
|
menu.chart: Charts
|
||||||
menu.version: Version
|
menu.version: Version
|
||||||
menu.profile: Profile
|
menu.profile: Profile
|
||||||
menu.password: Password
|
menu.password: Password
|
||||||
@ -90,7 +97,7 @@ home.node: Nodes
|
|||||||
home.network: Networks
|
home.network: Networks
|
||||||
home.service: Services
|
home.service: Services
|
||||||
home.stack: Stacks
|
home.stack: Stacks
|
||||||
home.feature: Features
|
home.monitor: Monitors
|
||||||
|
|
||||||
# image pages
|
# image pages
|
||||||
image.title: Image
|
image.title: Image
|
||||||
@ -169,6 +176,10 @@ setting.description: Manage Swirl system settings.
|
|||||||
event.title: Event
|
event.title: Event
|
||||||
event.description: Manage all user events.
|
event.description: Manage all user events.
|
||||||
|
|
||||||
|
# chart pages
|
||||||
|
chart.title: Chart
|
||||||
|
chart.description: Manage all metric charts.
|
||||||
|
|
||||||
# profile pages
|
# profile pages
|
||||||
profile.title: Profile
|
profile.title: Profile
|
||||||
profile.description: User profiles.
|
profile.description: User profiles.
|
||||||
|
@ -45,6 +45,12 @@ field.driver: 驱动
|
|||||||
field.scope: 范围
|
field.scope: 范围
|
||||||
field.role: 角色
|
field.role: 角色
|
||||||
field.user: 用户
|
field.user: 用户
|
||||||
|
field.width: 宽度
|
||||||
|
field.height: 高度
|
||||||
|
field.unit: 单位
|
||||||
|
field.title: 标题
|
||||||
|
field.label: 标签
|
||||||
|
field.dashboard: 仪表盘
|
||||||
|
|
||||||
# menu
|
# menu
|
||||||
menu.home: 首页
|
menu.home: 首页
|
||||||
@ -69,6 +75,7 @@ menu.role: 角色
|
|||||||
menu.user: 用户
|
menu.user: 用户
|
||||||
menu.setting: 设置
|
menu.setting: 设置
|
||||||
menu.event: 事件
|
menu.event: 事件
|
||||||
|
menu.chart: 图表
|
||||||
menu.version: 版本
|
menu.version: 版本
|
||||||
menu.profile: 资料
|
menu.profile: 资料
|
||||||
menu.password: 密码
|
menu.password: 密码
|
||||||
@ -90,7 +97,7 @@ home.node: 节点
|
|||||||
home.network: 网络
|
home.network: 网络
|
||||||
home.service: 服务
|
home.service: 服务
|
||||||
home.stack: 编排
|
home.stack: 编排
|
||||||
home.feature: 功能
|
home.monitor: 监控
|
||||||
|
|
||||||
# image pages
|
# image pages
|
||||||
image.title: 镜像
|
image.title: 镜像
|
||||||
@ -169,6 +176,10 @@ setting.description: 管理 Swirl 系统设置。
|
|||||||
event.title: 事件
|
event.title: 事件
|
||||||
event.description: 管理用户操作日志。
|
event.description: 管理用户操作日志。
|
||||||
|
|
||||||
|
# chart pages
|
||||||
|
chart.title: 图表
|
||||||
|
chart.description: 管理监控图表。
|
||||||
|
|
||||||
# profile pages
|
# profile pages
|
||||||
profile.title: 资料
|
profile.title: 资料
|
||||||
profile.description: 用户个人信息。
|
profile.description: 用户个人信息。
|
||||||
|
143
controller/chart.go
Normal file
143
controller/chart.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cuigh/auxo/net/web"
|
||||||
|
"github.com/cuigh/auxo/util/cast"
|
||||||
|
"github.com/cuigh/swirl/biz"
|
||||||
|
"github.com/cuigh/swirl/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartList(ctx web.Context) error {
|
||||||
|
charts, err := biz.Chart.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := newModel(ctx).Set("Charts", charts)
|
||||||
|
return ctx.Render("system/chart/list", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartQuery(ctx web.Context) error {
|
||||||
|
charts, err := biz.Chart.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard := ctx.Q("dashboard")
|
||||||
|
var list []*model.Chart
|
||||||
|
for _, c := range charts {
|
||||||
|
if c.Dashboard == dashboard || c.Dashboard == "" {
|
||||||
|
list = append(list, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.JSON(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartNew(ctx web.Context) error {
|
||||||
|
m := newModel(ctx).Set("Chart", &model.Chart{
|
||||||
|
Width: 12,
|
||||||
|
Height: 50,
|
||||||
|
Type: "line",
|
||||||
|
Dashboard: "service",
|
||||||
|
})
|
||||||
|
return ctx.Render("system/chart/edit", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartCreate(ctx web.Context) error {
|
||||||
|
chart := &model.Chart{}
|
||||||
|
err := ctx.Bind(chart, true)
|
||||||
|
if err == nil {
|
||||||
|
err = biz.Chart.Create(chart, ctx.User())
|
||||||
|
}
|
||||||
|
return ajaxResult(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartEdit(ctx web.Context) error {
|
||||||
|
name := ctx.P("name")
|
||||||
|
chart, err := biz.Chart.Get(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if chart == nil {
|
||||||
|
return web.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
m := newModel(ctx).Set("Chart", chart)
|
||||||
|
return ctx.Render("system/chart/edit", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartUpdate(ctx web.Context) error {
|
||||||
|
chart := &model.Chart{}
|
||||||
|
err := ctx.Bind(chart)
|
||||||
|
if err == nil {
|
||||||
|
err = biz.Chart.Update(chart, ctx.User())
|
||||||
|
}
|
||||||
|
return ajaxResult(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartDelete(ctx web.Context) error {
|
||||||
|
name := ctx.P("name")
|
||||||
|
err := biz.Chart.Delete(name, ctx.User())
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.JSON(datas)
|
||||||
|
}
|
@ -32,8 +32,10 @@ func Home() (c *HomeController) {
|
|||||||
|
|
||||||
func homeIndex(ctx web.Context) (err error) {
|
func homeIndex(ctx web.Context) (err error) {
|
||||||
var (
|
var (
|
||||||
count int
|
count int
|
||||||
m = newModel(ctx)
|
setting *model.Setting
|
||||||
|
charts []*model.Chart
|
||||||
|
m = newModel(ctx)
|
||||||
)
|
)
|
||||||
|
|
||||||
if count, err = docker.NodeCount(); err != nil {
|
if count, err = docker.NodeCount(); err != nil {
|
||||||
@ -56,6 +58,15 @@ func homeIndex(ctx web.Context) (err error) {
|
|||||||
}
|
}
|
||||||
m.Set("StackCount", count)
|
m.Set("StackCount", count)
|
||||||
|
|
||||||
|
if setting, err = biz.Setting.Get(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
charts, err = biz.Chart.Panel(setting.Dashboard.Home)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Set("Charts", charts)
|
||||||
|
|
||||||
return ctx.Render("index", m)
|
return ctx.Render("index", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cuigh/auxo/data"
|
|
||||||
"github.com/cuigh/auxo/data/set"
|
"github.com/cuigh/auxo/data/set"
|
||||||
"github.com/cuigh/auxo/errors"
|
"github.com/cuigh/auxo/errors"
|
||||||
"github.com/cuigh/auxo/net/web"
|
"github.com/cuigh/auxo/net/web"
|
||||||
@ -32,7 +31,6 @@ type ServiceController struct {
|
|||||||
PermEdit web.HandlerFunc `path:"/:name/perm" name:"service.perm.edit" authorize:"!" perm:"write,service,name"`
|
PermEdit web.HandlerFunc `path:"/:name/perm" name:"service.perm.edit" authorize:"!" perm:"write,service,name"`
|
||||||
PermUpdate web.HandlerFunc `path:"/:name/perm" method:"post" name:"service.perm.update" authorize:"!" perm:"write,service,name"`
|
PermUpdate web.HandlerFunc `path:"/:name/perm" method:"post" name:"service.perm.update" authorize:"!" perm:"write,service,name"`
|
||||||
Stats web.HandlerFunc `path:"/:name/stats" name:"service.stats" authorize:"!" perm:"read,service,name"`
|
Stats web.HandlerFunc `path:"/:name/stats" name:"service.stats" authorize:"!" perm:"read,service,name"`
|
||||||
Metrics web.HandlerFunc `path:"/:name/metrics" name:"service.metrics" authorize:"?"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service creates an instance of ServiceController
|
// Service creates an instance of ServiceController
|
||||||
@ -52,7 +50,6 @@ func Service() (c *ServiceController) {
|
|||||||
PermEdit: servicePermEdit,
|
PermEdit: servicePermEdit,
|
||||||
PermUpdate: permUpdate("service", "name"),
|
PermUpdate: permUpdate("service", "name"),
|
||||||
Stats: serviceStats,
|
Stats: serviceStats,
|
||||||
Metrics: serviceMetrics,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,13 +294,9 @@ func serviceStats(ctx web.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var charts []model.ChartInfo
|
var charts []*model.Chart
|
||||||
if setting.Metrics.Prometheus != "" {
|
if setting.Metrics.Prometheus != "" {
|
||||||
var categories []string
|
charts, err = biz.Chart.GetServiceCharts(name)
|
||||||
if label := service.Spec.Labels["swirl.metrics"]; label != "" {
|
|
||||||
categories = strings.Split(label, ",")
|
|
||||||
}
|
|
||||||
charts = biz.Metric.GetServiceCharts(name, categories)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
period := cast.ToDuration(ctx.Q("time"), time.Hour)
|
period := cast.ToDuration(ctx.Q("time"), time.Hour)
|
||||||
@ -312,31 +305,3 @@ func serviceStats(ctx web.Context) error {
|
|||||||
Set("Refresh", refresh).Set("Charts", charts)
|
Set("Refresh", refresh).Set("Charts", charts)
|
||||||
return ctx.Render("service/stats", m)
|
return ctx.Render("service/stats", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceMetrics(ctx web.Context) error {
|
|
||||||
name := ctx.P("name")
|
|
||||||
service, _, err := docker.ServiceInspect(name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var categories []string
|
|
||||||
if label := service.Spec.Labels["swirl.metrics"]; label != "" {
|
|
||||||
categories = strings.Split(label, ",")
|
|
||||||
}
|
|
||||||
charts := biz.Metric.GetServiceCharts(name, categories)
|
|
||||||
|
|
||||||
period := cast.ToDuration(ctx.Q("time"), time.Hour)
|
|
||||||
end := time.Now()
|
|
||||||
start := end.Add(-period)
|
|
||||||
|
|
||||||
m := data.Map{}
|
|
||||||
for _, c := range charts {
|
|
||||||
matrix, err := biz.Metric.GetMatrix(c.Query, c.Label, start, end)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m[c.Name] = matrix
|
|
||||||
}
|
|
||||||
return ctx.JSON(m)
|
|
||||||
}
|
|
||||||
|
@ -62,6 +62,13 @@ type Interface interface {
|
|||||||
|
|
||||||
SettingGet() (setting *model.Setting, err error)
|
SettingGet() (setting *model.Setting, err error)
|
||||||
SettingUpdate(setting *model.Setting) error
|
SettingUpdate(setting *model.Setting) error
|
||||||
|
|
||||||
|
ChartGet(name string) (*model.Chart, error)
|
||||||
|
ChartBatch(names ...string) ([]*model.Chart, error)
|
||||||
|
ChartList() (charts []*model.Chart, err error)
|
||||||
|
ChartCreate(chart *model.Chart) error
|
||||||
|
ChartUpdate(chart *model.Chart) error
|
||||||
|
ChartDelete(name string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get return a dao instance according to DB_TYPE.
|
// Get return a dao instance according to DB_TYPE.
|
||||||
|
65
dao/mongo/chart.go
Normal file
65
dao/mongo/chart.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package mongo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cuigh/swirl/model"
|
||||||
|
"github.com/globalsign/mgo"
|
||||||
|
"github.com/globalsign/mgo/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Dao) ChartList() (charts []*model.Chart, err error) {
|
||||||
|
d.do(func(db *database) {
|
||||||
|
charts = []*model.Chart{}
|
||||||
|
err = db.C("chart").Find(nil).All(&charts)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dao) ChartCreate(chart *model.Chart) (err error) {
|
||||||
|
d.do(func(db *database) {
|
||||||
|
err = db.C("chart").Insert(chart)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dao) ChartGet(name string) (chart *model.Chart, err error) {
|
||||||
|
d.do(func(db *database) {
|
||||||
|
chart = &model.Chart{}
|
||||||
|
err = db.C("chart").FindId(name).One(chart)
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
chart, err = nil, nil
|
||||||
|
} else if err != nil {
|
||||||
|
chart = nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dao) ChartBatch(names ...string) (charts []*model.Chart, err error) {
|
||||||
|
d.do(func(db *database) {
|
||||||
|
q := bson.M{"_id": bson.M{"$in": names}}
|
||||||
|
charts = make([]*model.Chart, 0)
|
||||||
|
err = db.C("chart").Find(q).All(&charts)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dao) ChartUpdate(chart *model.Chart) (err error) {
|
||||||
|
d.do(func(db *database) {
|
||||||
|
//update := bson.M{
|
||||||
|
// "$set": bson.M{
|
||||||
|
// "name": chart.Name,
|
||||||
|
// "desc": chart.Description,
|
||||||
|
// },
|
||||||
|
//}
|
||||||
|
err = db.C("chart").UpdateId(chart.Name, chart)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dao) ChartDelete(name string) (err error) {
|
||||||
|
d.do(func(db *database) {
|
||||||
|
err = db.C("chart").RemoveId(name)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
1
main.go
1
main.go
@ -116,6 +116,7 @@ func server(setting *model.Setting) *web.Server {
|
|||||||
g.Handle("/system/role", controller.Role())
|
g.Handle("/system/role", controller.Role())
|
||||||
g.Handle("/system/setting", controller.Setting())
|
g.Handle("/system/setting", controller.Setting())
|
||||||
g.Handle("/system/event", controller.Event())
|
g.Handle("/system/event", controller.Event())
|
||||||
|
g.Handle("/system/chart", controller.Chart())
|
||||||
|
|
||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
87
model/metric.go
Normal file
87
model/metric.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cuigh/auxo/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chart represents a dashboard chart.
|
||||||
|
type Chart struct {
|
||||||
|
Name string `json:"name" bson:"_id" valid:"required"` // unique, the name of build-in charts has '$' prefix.
|
||||||
|
Title string `json:"title" valid:"required"`
|
||||||
|
Description string `json:"desc"`
|
||||||
|
Label string `json:"label"` // ${name} - ${instance}
|
||||||
|
Query string `json:"query" valid:"required"`
|
||||||
|
Kind string `json:"kind"` // builtin/custom
|
||||||
|
Dashboard string `json:"dashboard"` // home/service/task...
|
||||||
|
Type string `json:"type"` // pie/line...
|
||||||
|
Unit string `json:"unit"` // bytes/milliseconds/percent:100...
|
||||||
|
Width int32 `json:"width"` // 1-12(12 columns total)
|
||||||
|
Height int32 `json:"height"` // default 50
|
||||||
|
Colors []string `json:"colors"`
|
||||||
|
Options data.Map `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChart(name, title, label, query, unit string) *Chart {
|
||||||
|
return &Chart{
|
||||||
|
Name: name,
|
||||||
|
Title: title,
|
||||||
|
Description: title,
|
||||||
|
Label: label,
|
||||||
|
Query: query,
|
||||||
|
Type: "line",
|
||||||
|
Unit: unit,
|
||||||
|
Width: 12,
|
||||||
|
Height: 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Colors []string `json:"colors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartPanel struct {
|
||||||
|
Refresh bool `json:"refresh"`
|
||||||
|
Period int32 `json:"period"` // minutes
|
||||||
|
Charts []ChartItem `json:"charts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//type ChartPanel []ChartItem
|
||||||
|
|
||||||
|
type ChartPoint struct {
|
||||||
|
X int64 `json:"x"`
|
||||||
|
Y float64 `json:"y"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartLine struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Data []ChartPoint `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartValue struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Data float64 `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartVector struct {
|
||||||
|
Data []float64 `json:"data"`
|
||||||
|
Labels []string `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChartInfo(name, title, label, query string) ChartInfo {
|
||||||
|
return ChartInfo{
|
||||||
|
Name: name,
|
||||||
|
Title: title,
|
||||||
|
Label: label,
|
||||||
|
Query: query,
|
||||||
|
}
|
||||||
|
}
|
@ -33,29 +33,3 @@ type TemplateListArgs struct {
|
|||||||
PageIndex int `bind:"page"`
|
PageIndex int `bind:"page"`
|
||||||
PageSize int `bind:"size"`
|
PageSize int `bind:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartPoint struct {
|
|
||||||
X int64 `json:"x"`
|
|
||||||
Y float64 `json:"y"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartLine struct {
|
|
||||||
Label string `json:"label"`
|
|
||||||
Data []ChartPoint `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Query string `json:"query"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChartInfo(name, title, label, query string) ChartInfo {
|
|
||||||
return ChartInfo{
|
|
||||||
Name: name,
|
|
||||||
Title: title,
|
|
||||||
Label: label,
|
|
||||||
Query: query,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -47,6 +47,9 @@ type Setting struct {
|
|||||||
Metrics struct {
|
Metrics struct {
|
||||||
Prometheus string `bson:"prometheus" json:"prometheus"`
|
Prometheus string `bson:"prometheus" json:"prometheus"`
|
||||||
} `bson:"metrics" json:"metrics"`
|
} `bson:"metrics" json:"metrics"`
|
||||||
|
Dashboard struct {
|
||||||
|
Home ChartPanel `bson:"home" json:"home"`
|
||||||
|
} `bson:"dashboard" json:"dashboard"`
|
||||||
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
|
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
|
||||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
|
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -159,16 +159,16 @@ func checkArg(service string, arg data.Option) (scaleType, float64) {
|
|||||||
|
|
||||||
func cpuChecker(service string, low, high float64) (scaleType, float64) {
|
func cpuChecker(service string, low, high float64) (scaleType, float64) {
|
||||||
query := fmt.Sprintf(`avg(rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100)`, service)
|
query := fmt.Sprintf(`avg(rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100)`, service)
|
||||||
values, err := biz.Metric.GetVector(query, time.Now())
|
vector, err := biz.Metric.GetVector(query, "", time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Get("scaler").Error("scaler > Failed to query metrics: ", err)
|
log.Get("scaler").Error("scaler > Failed to query metrics: ", err)
|
||||||
return scaleNone, 0
|
return scaleNone, 0
|
||||||
}
|
}
|
||||||
if len(values) == 0 {
|
if len(vector.Data) == 0 {
|
||||||
return scaleNone, 0
|
return scaleNone, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
value := values[0]
|
value := vector.Data[0]
|
||||||
if value <= low {
|
if value <= low {
|
||||||
return scaleDown, value
|
return scaleDown, value
|
||||||
} else if value >= high {
|
} else if value >= high {
|
||||||
|
@ -221,4 +221,15 @@ var Perms = []PermGroup{
|
|||||||
{Key: "event.list", Text: "View list"},
|
{Key: "event.list", Text: "View list"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Chart",
|
||||||
|
Perms: []Perm{
|
||||||
|
{Key: "chart.list", Text: "View list"},
|
||||||
|
{Key: "chart.new", Text: "View new"},
|
||||||
|
{Key: "chart.edit", Text: "View edit"},
|
||||||
|
{Key: "chart.create", Text: "Create"},
|
||||||
|
{Key: "chart.delete", Text: "Delete"},
|
||||||
|
{Key: "chart.update", Text: "Update"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
<a class="navbar-item" href="/system/user/">{{ i18n("menu.user") }}</a>
|
<a class="navbar-item" href="/system/user/">{{ i18n("menu.user") }}</a>
|
||||||
<a class="navbar-item" href="/system/setting/">{{ i18n("menu.setting") }}</a>
|
<a class="navbar-item" href="/system/setting/">{{ i18n("menu.setting") }}</a>
|
||||||
<a class="navbar-item" href="/system/event/">{{ i18n("menu.event") }}</a>
|
<a class="navbar-item" href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||||
|
<a class="navbar-item" href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<div class="navbar-content">
|
<div class="navbar-content">
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
{{ extends "_layouts/default" }}
|
{{ extends "_layouts/default" }}
|
||||||
|
|
||||||
|
{{ block script() }}
|
||||||
|
<script src="/assets/chart/chart.bundle.min.js?v=2.7.2"></script>
|
||||||
|
<script>$(() => new Swirl.IndexPage())</script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ block infobox(style, icon, text, url, count) }}
|
{{ block infobox(style, icon, text, url, count) }}
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="notification is-{{style}}">
|
<div class="notification is-{{style}}">
|
||||||
@ -28,7 +33,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{ block body() }}
|
{{ block body() }}
|
||||||
<section class="hero is-primary is-medium is-bold">
|
<section class="hero is-primary is-bold">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container has-text-centered">
|
<div class="container has-text-centered">
|
||||||
<h1 class="title is-2">
|
<h1 class="title is-2">
|
||||||
@ -36,18 +41,12 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<h2 class="subtitle is-5">{{ i18n("description") }}</h2>
|
<h2 class="subtitle is-5">{{ i18n("description") }}</h2>
|
||||||
<p>
|
<p>
|
||||||
<a id="btn-add" class="button is-primary" href="https://github.com/cuigh/swirl" target="_blank">
|
<a class="button is-primary" href="https://github.com/cuigh/swirl" target="_blank">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fab fa-github"></i>
|
<i class="fab fa-github"></i>
|
||||||
</span>
|
</span>
|
||||||
<span>GitHub</span>
|
<span>GitHub</span>
|
||||||
</a>
|
</a>
|
||||||
{*<a id="btn-add" class="button is-success" href="https://cuigh.tech/swirl" target="_blank">*}
|
|
||||||
{*<span class="icon">*}
|
|
||||||
{*<i class="fas fa-book"></i>*}
|
|
||||||
{*</span>*}
|
|
||||||
{*<span>Document</span>*}
|
|
||||||
{*</a>*}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -64,51 +63,48 @@
|
|||||||
{{yield infobox(style="primary", icon="server", text=i18n("home.stack"), url="/stack/task/", count=.StackCount)}}
|
{{yield infobox(style="primary", icon="server", text=i18n("home.stack"), url="/stack/task/", count=.StackCount)}}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<h3 class="title is-5">{{ i18n("home.feature") }}</h3>
|
<nav class="level">
|
||||||
<div class="columns">
|
<div class="level-left">
|
||||||
<div class="column is-4">
|
<div class="level-item">
|
||||||
<div class="card is-fullwidth">
|
<p class="subtitle is-5">
|
||||||
|
<strong>{{ i18n("home.monitor") }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</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="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<h4 class="card-header-title"><strong>{{ i18n("menu.local") }}</strong></h4>
|
<p class="card-header-title">{{ .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>
|
||||||
|
{*<a data-action="remove-chart" class="card-header-icon is-paddingless" aria-label="remove chart">*}
|
||||||
|
{*<span class="icon">*}
|
||||||
|
{*<i class="fas fa-times has-text-danger" aria-hidden="true"></i>*}
|
||||||
|
{*</span>*}
|
||||||
|
{*</a>*}
|
||||||
|
{*<a data-action="edit-options" class="card-header-icon" aria-label="edit options">*}
|
||||||
|
{*<span class="icon">*}
|
||||||
|
{*<i class="fas fa-ellipsis-h has-text-info" aria-hidden="true"></i>*}
|
||||||
|
{*</span>*}
|
||||||
|
{*</a>*}
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{{yield progress(title=i18n("menu.image"), percent=100)}}
|
<canvas id="canvas_{{ .Name }}"></canvas>
|
||||||
{{yield progress(title=i18n("menu.container"), percent=80)}}
|
|
||||||
{{yield progress(title=i18n("menu.volume"), percent=100)}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="card is-fullwidth">
|
|
||||||
<header class="card-header">
|
|
||||||
<h4 class="card-header-title"><strong>{{ i18n("menu.swarm") }}</strong></h4>
|
|
||||||
</header>
|
|
||||||
<div class="card-content">
|
|
||||||
{{yield progress(title=i18n("menu.registry"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.node"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.network"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.service"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.stack"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.task"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.secret"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.config"), percent=100)}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="column is-4">
|
|
||||||
<div class="card is-fullwidth">
|
|
||||||
<header class="card-header">
|
|
||||||
<h4 class="card-header-title"><strong>{{ i18n("menu.system") }}</strong></h4>
|
|
||||||
</header>
|
|
||||||
<div class="card-content">
|
|
||||||
{{yield progress(title=i18n("menu.profile"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.role"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.user"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.setting"), percent=100)}}
|
|
||||||
{{yield progress(title=i18n("menu.event"), percent=80)}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{ end }}
|
{{ end }}
|
@ -21,9 +21,7 @@
|
|||||||
<section class="hero is-small is-light">
|
<section class="hero is-small is-light">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="title is-2">
|
<h2 id="h2-service-name" class="title is-2">{{ .Service.Spec.Name }}</h2>
|
||||||
{{ .Service.Spec.Name }}
|
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -74,21 +72,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="columns is-multiline">
|
<div id="div-charts" class="columns is-multiline">
|
||||||
{{ range .Charts }}
|
{{ range .Charts }}
|
||||||
<div class="column is-12">
|
<div class="column is-{{ .Width }}" data-chart-name="{{ .Name }}" data-chart-type="{{ .Type }}" data-chart-unit="{{ .Unit }}" data-chart-height="{{ .Height }}">
|
||||||
<div class="block">
|
<div class="card">
|
||||||
<div class="block-header">
|
<header class="card-header">
|
||||||
<p>{{ .Title }}</p>
|
<p class="card-header-title">{{ .Title }}</p>
|
||||||
</div>
|
<a data-action="remove-chart" class="card-header-icon is-paddingless" aria-label="remove chart">
|
||||||
<div class="block-body is-bordered">
|
<span class="icon">
|
||||||
|
<i class="fas fa-times has-text-danger" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a data-action="edit-options" class="card-header-icon" aria-label="edit options">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fas fa-ellipsis-h has-text-info" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
<canvas id="canvas_{{ .Name }}"></canvas>
|
<canvas id="canvas_{{ .Name }}"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="notification is-info">
|
<div class="notification is-info">
|
||||||
NOTICE: To enable this feature, you must set <b>Metrics</b> option on <a href="/system/setting/">Setting</a> page first.
|
NOTICE: To enable this feature, you must set <b>Metrics</b> option on <a href="/system/setting/">Setting</a> page first.
|
||||||
|
36
views/system/chart/_base.jet
Normal file
36
views/system/chart/_base.jet
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{{ extends "../../_layouts/default" }}
|
||||||
|
|
||||||
|
{{ block body() }}
|
||||||
|
<section class="hero is-dark">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container has-text-centered">
|
||||||
|
<h1 class="title is-2 is-uppercase">{{ i18n("chart.title") }}</h1>
|
||||||
|
<h2 class="subtitle is-5">{{ i18n("chart.description") }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-foot">
|
||||||
|
<div class="container">
|
||||||
|
<nav class="tabs is-boxed">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/system/role/">{{ i18n("menu.role") }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/user/">{{ i18n("menu.user") }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/setting/">{{ i18n("menu.setting") }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ yield body_content() }}
|
||||||
|
{{ end }}
|
126
views/system/chart/edit.jet
Normal file
126
views/system/chart/edit.jet
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
{{ extends "_base" }}
|
||||||
|
{{ import "../../_modules/form" }}
|
||||||
|
|
||||||
|
{{ block body_content() }}
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title">Create chart</h2>
|
||||||
|
<hr>
|
||||||
|
<form method="post" data-form="ajax-json" data-url="/system/chart/">
|
||||||
|
<div class="columns is-bottom-marginless">
|
||||||
|
<div class="column is-6">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.name") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input name="name" value="{{ .Chart.Name }}" class="input" placeholder="lower-case letters and underline only" data-v-rule="native;regex" data-v-arg-regex="[a-z_]+" data-v-msg-regex="Only lower-case letters and underline are allowed." required{{if .Chart.Name}} readonly{{ end }}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.title") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input name="title" value="{{ .Chart.Title }}" class="input" placeholder="Title" data-v-rule="native" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.label") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input name="label" value="{{ .Chart.Label }}" class="input" placeholder="Label for dataset, e.g. ${name}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.width") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<span class="select">
|
||||||
|
<select name="width" data-type="integer">
|
||||||
|
{{ yield option(value="1", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="2", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="3", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="4", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="5", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="6", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="7", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="8", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="9", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="10", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="11", selected=.Chart.Width) }}
|
||||||
|
{{ yield option(value="12", selected=.Chart.Width) }}
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.height") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<input name="height" value="{{ .Chart.Height }}" class="input" placeholder="Height" data-type="integer" data-v-rule="native" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.unit") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
<span class="select">
|
||||||
|
<select name="unit">
|
||||||
|
{{ yield option(value="", label="None", selected=.Chart.Unit) }}
|
||||||
|
<optgroup label="Percent">
|
||||||
|
{{ yield option(value="percent:100", label="0-100", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="percent:1", label="0.0-1.0", selected=.Chart.Unit) }}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Time">
|
||||||
|
{{ yield option(value="time:ns", label="Nanoseconds", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="time:µs", label="Microseconds", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="time:ms", label="Milliseconds", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="time:s", label="Seconds", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="time:m", label="Minutes", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="time:h", label="Hours", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="time:d", label="Days", selected=.Chart.Unit) }}
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Size">
|
||||||
|
{{ yield option(value="size:bits", label="Bits", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="size:bytes", label="Bytes", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="size:kilobytes", label="Kilobytes", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="size:megabytes", label="Megabytes", selected=.Chart.Unit) }}
|
||||||
|
{{ yield option(value="size:gigabytes", label="Gigabytes", selected=.Chart.Unit) }}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Description</label>
|
||||||
|
<div class="control">
|
||||||
|
<input name="desc" value="{{ .Chart.Description }}" class="input" placeholder="Chart description">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Query expression</label>
|
||||||
|
<div class="control">
|
||||||
|
<input name="query" value="{{ .Chart.Query }}" class="input" placeholder="Prometheus query expression" data-v-rule="native" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.type") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
{{ yield radio(name="type", value="line", label="Line", checked=.Chart.Type) }}
|
||||||
|
{{ yield radio(name="type", value="bar", label="Bar", checked=.Chart.Type) }}
|
||||||
|
{{ yield radio(name="type", value="pie", label="Pie", checked=.Chart.Type) }}
|
||||||
|
{*{{ yield radio(name="type", value="table", label="Table", checked=.Chart.Type) }}*}
|
||||||
|
{{ yield radio(name="type", value="value", label="Value", checked=.Chart.Type) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">{{ i18n("field.dashboard") }}</label>
|
||||||
|
<div class="control">
|
||||||
|
{{ yield radio(name="dashboard", value="", label="Any", checked=.Chart.Dashboard) }}
|
||||||
|
{{ yield radio(name="dashboard", value="home", label="Home", checked=.Chart.Dashboard) }}
|
||||||
|
{{ yield radio(name="dashboard", value="service", label="Service", checked=.Chart.Dashboard) }}
|
||||||
|
{{ yield radio(name="dashboard", value="task", label="Task", checked=.Chart.Dashboard) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ yield form_submit(url="/system/chart/") }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
82
views/system/chart/list.jet
Normal file
82
views/system/chart/list.jet
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{{ extends "_base" }}
|
||||||
|
|
||||||
|
{{ block script() }}
|
||||||
|
<script>$(() => new Swirl.Metric.ListPage())</script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block body_content() }}
|
||||||
|
<section class="section">
|
||||||
|
<nav class="level">
|
||||||
|
<!-- Left side -->
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input id="txt-query" class="input" type="text" placeholder="Search by name, title and description">
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<p class="subtitle is-5">
|
||||||
|
<strong>{{ len(.Charts) }}</strong>
|
||||||
|
<span class="is-lowercase">{{ i18n("menu.chart") }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Right side -->
|
||||||
|
<div class="level-right">
|
||||||
|
<p class="level-item">
|
||||||
|
<a href="new" class="button is-success"><span class="icon"><i class="fas fa-plus"></i></span><span>{{ i18n("button.new") }}</span></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div id="div-charts" class="columns is-multiline">
|
||||||
|
{{ range .Charts }}
|
||||||
|
<div class="column is-3" data-name="{{ .Name }}" data-title="{{ .Title }}" data-desc="{{ .Description }}">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">{{ .Name }} - {{ .Width }}X{{ .Height }}</p>
|
||||||
|
<p class="card-header-icon">
|
||||||
|
<span class="icon">
|
||||||
|
{{ if .Dashboard == "home" }}
|
||||||
|
<i class="fas fa-home"></i>
|
||||||
|
{{ else if .Dashboard == "service" }}
|
||||||
|
<i class="fas fa-ticket-alt"></i>
|
||||||
|
{{ else if .Dashboard == "task" }}
|
||||||
|
<i class="fas fa-tasks"></i>
|
||||||
|
{{ end }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left">
|
||||||
|
{{ if .Type == "value" }}
|
||||||
|
<i class="fas fa-bullseye fa-4x"></i>
|
||||||
|
{{ else }}
|
||||||
|
<i class="fas fa-chart-{{ .Type }} fa-4x"></i>
|
||||||
|
{{ end }}
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
<strong>{{ .Title }}</strong>
|
||||||
|
<br>
|
||||||
|
{{ .Description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
{*<a class="card-footer-item">View</a>*}
|
||||||
|
<a href="{{ .Name }}/edit" class="card-footer-item">Edit</a>
|
||||||
|
<a data-action="delete-chart" class="card-footer-item">Delete</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
@ -28,7 +28,10 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,6 +24,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,10 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,6 +24,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user