mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 14:51:57 +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/debug",
|
||||
"util/i18n",
|
||||
"util/lazy"
|
||||
"util/lazy",
|
||||
"util/run"
|
||||
]
|
||||
revision = "772f5a654db9ee95a5dc851e9562253bc0b2f11d"
|
||||
|
||||
@ -224,6 +225,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "c829e2bd1f0dc356225caad79652e6732d0572e0e59580a5e6c5b6baab134755"
|
||||
inputs-digest = "dea823c7646b8b71101e0443b1c0c36a723989261f733ad4cb44c6e9b0d3942d"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
@ -171,6 +171,14 @@ dd {
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
.card-header-title, .card-header-icon, .card-footer-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@ -246,7 +254,7 @@ dd {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
textarea.code{
|
||||
textarea.code {
|
||||
padding: 0.75em;
|
||||
}
|
||||
|
||||
|
@ -451,7 +451,7 @@ var Swirl;
|
||||
}
|
||||
class RegexRule {
|
||||
validate($form, $input, arg) {
|
||||
let regex = new RegExp(arg, 'i');
|
||||
let regex = new RegExp(arg);
|
||||
let value = $.trim($input.val());
|
||||
return { ok: !value || regex.test(value) };
|
||||
}
|
||||
@ -1858,149 +1858,298 @@ var Swirl;
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Service;
|
||||
(function (Service) {
|
||||
class MetricChartOptions {
|
||||
var Core;
|
||||
(function (Core) {
|
||||
class GraphOptions {
|
||||
constructor() {
|
||||
this.type = "line";
|
||||
this.width = 12;
|
||||
this.height = 50;
|
||||
}
|
||||
}
|
||||
class MetricChart {
|
||||
Core.GraphOptions = GraphOptions;
|
||||
class Graph {
|
||||
constructor(elem, opts) {
|
||||
this.colors = [
|
||||
'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)',
|
||||
];
|
||||
opts = $.extend(new MetricChartOptions(), opts);
|
||||
this.$elem = $(elem);
|
||||
this.opts = $.extend(new GraphOptions(), opts);
|
||||
this.name = this.$elem.data("chart-name");
|
||||
}
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
Core.Graph = Graph;
|
||||
class ValueGraph extends Graph {
|
||||
constructor(elem, opts) {
|
||||
super(elem, opts);
|
||||
}
|
||||
setData(d) {
|
||||
}
|
||||
setSize(w, h) {
|
||||
}
|
||||
}
|
||||
Core.ValueGraph = ValueGraph;
|
||||
class ComplexGraph extends Graph {
|
||||
constructor(elem, opts) {
|
||||
super(elem, opts);
|
||||
if (!this.opts.colors) {
|
||||
this.opts.colors = ComplexGraph.defaultColors;
|
||||
}
|
||||
this.config = {
|
||||
type: opts.type,
|
||||
data: {},
|
||||
options: {
|
||||
title: {
|
||||
text: opts.title || 'NONE'
|
||||
},
|
||||
animation: {
|
||||
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.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 = $(elem).get(0).getContext('2d');
|
||||
this.fillConfig();
|
||||
this.ctx = $(elem).find("canvas").get(0).getContext('2d');
|
||||
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) => {
|
||||
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.borderColor = color;
|
||||
ds.borderWidth = 2;
|
||||
ds.pointRadius = 2;
|
||||
});
|
||||
this.config.data.datasets = datasets;
|
||||
this.config.data.datasets = d;
|
||||
this.chart.update();
|
||||
}
|
||||
}
|
||||
class StatsPage {
|
||||
constructor() {
|
||||
this.chartOptions = {
|
||||
"cpu": { tickY: (value) => value + '%' },
|
||||
"memory": { tickY: StatsPage.formatSize },
|
||||
"network_in": { tickY: StatsPage.formatSize },
|
||||
"network_out": { tickY: StatsPage.formatSize },
|
||||
"threads": {},
|
||||
"goroutines": {},
|
||||
"gc_duration": { tickY: (value) => value * 1000 + 'ms' },
|
||||
Core.MatrixGraph = MatrixGraph;
|
||||
class GraphFactory {
|
||||
static create(elem) {
|
||||
let $elem = $(elem);
|
||||
let opts = {
|
||||
type: $elem.data("chart-type"),
|
||||
unit: $elem.data("chart-unit"),
|
||||
height: $elem.data("chart-height"),
|
||||
colors: $elem.data("chart-colors"),
|
||||
};
|
||||
this.charts = {};
|
||||
let $cb_time = $("#cb-time");
|
||||
if ($cb_time.length == 0) {
|
||||
return;
|
||||
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);
|
||||
}
|
||||
$cb_time.change(this.loadData.bind(this));
|
||||
$("#cb-refresh").change(() => {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
else {
|
||||
this.refreshData();
|
||||
}
|
||||
});
|
||||
$.each(this.chartOptions, (name, opt) => {
|
||||
let $el = $("#canvas_" + name);
|
||||
if ($el.length > 0) {
|
||||
this.charts[name] = new MetricChart($el, opt);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Core.GraphFactory = GraphFactory;
|
||||
class GraphPanelOptions {
|
||||
constructor() {
|
||||
this.time = "30m";
|
||||
this.refreshInterval = 15000;
|
||||
}
|
||||
}
|
||||
Core.GraphPanelOptions = GraphPanelOptions;
|
||||
class GraphPanel {
|
||||
constructor(elems, opts) {
|
||||
this.charts = [];
|
||||
this.opts = $.extend(new GraphPanelOptions(), opts);
|
||||
$(elems).each((i, e) => {
|
||||
let g = GraphFactory.create(e);
|
||||
if (g != null) {
|
||||
this.charts.push(g);
|
||||
}
|
||||
});
|
||||
this.refreshData();
|
||||
}
|
||||
refreshData() {
|
||||
this.loadData();
|
||||
if ($("#cb-refresh").prop("checked")) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), 15000);
|
||||
if (this.opts.refreshInterval > 0) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval);
|
||||
}
|
||||
}
|
||||
refresh() {
|
||||
if (!this.timer) {
|
||||
this.loadData();
|
||||
if (this.opts.refreshInterval > 0) {
|
||||
this.timer = setTimeout(this.refreshData.bind(this), this.opts.refreshInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = 0;
|
||||
}
|
||||
setTime(time) {
|
||||
this.opts.time = time;
|
||||
this.loadData();
|
||||
}
|
||||
loadData() {
|
||||
let time = $("#cb-time").val();
|
||||
$ajax.get(`metrics`, { time: time }).json((d) => {
|
||||
$.each(this.charts, (name, chart) => {
|
||||
if (d[name]) {
|
||||
chart.setData(d[name]);
|
||||
let args = {
|
||||
dashboard: this.opts.name,
|
||||
time: this.opts.time,
|
||||
};
|
||||
if (this.opts.id) {
|
||||
args.id = this.opts.id;
|
||||
}
|
||||
$ajax.get(`/system/chart/data`, args).json((d) => {
|
||||
$.each(this.charts, (i, g) => {
|
||||
if (d[g.getName()]) {
|
||||
g.setData(d[g.getName()]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
static formatSize(size) {
|
||||
if (size < 1024) {
|
||||
return size.toString() + 'B';
|
||||
}
|
||||
else if (size < 1048576) {
|
||||
return (size / 1024).toFixed(2) + 'K';
|
||||
}
|
||||
else if (size < 1073741824) {
|
||||
return (size / 1048576).toFixed(2) + 'M';
|
||||
}
|
||||
else {
|
||||
return (size / 1073741824).toFixed(2) + 'G';
|
||||
}
|
||||
Core.GraphPanel = GraphPanel;
|
||||
})(Core = Swirl.Core || (Swirl.Core = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Service;
|
||||
(function (Service) {
|
||||
var Modal = Swirl.Core.Modal;
|
||||
var GraphPanel = Swirl.Core.GraphPanel;
|
||||
class StatsPage {
|
||||
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;
|
||||
@ -2305,4 +2454,77 @@ var Swirl;
|
||||
Volume.NewPage = NewPage;
|
||||
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Modal = Swirl.Core.Modal;
|
||||
var GraphPanel = Swirl.Core.GraphPanel;
|
||||
class IndexPage {
|
||||
constructor() {
|
||||
this.panel = new GraphPanel($("#div-charts").children("div"), { name: "home" });
|
||||
$("#btn-add").click(() => {
|
||||
Modal.alert("Coming soon...");
|
||||
});
|
||||
}
|
||||
}
|
||||
Swirl.IndexPage = IndexPage;
|
||||
})(Swirl || (Swirl = {}));
|
||||
var Swirl;
|
||||
(function (Swirl) {
|
||||
var Metric;
|
||||
(function (Metric) {
|
||||
var Modal = Swirl.Core.Modal;
|
||||
var Dispatcher = Swirl.Core.Dispatcher;
|
||||
class FilterBox {
|
||||
constructor(elem, callback, timeout) {
|
||||
this.$elem = $(elem);
|
||||
this.$elem.keyup(() => {
|
||||
if (this.timer > 0) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
let text = this.$elem.val().toLowerCase();
|
||||
this.timer = setTimeout(() => callback(text), timeout || 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
class ListPage {
|
||||
constructor() {
|
||||
this.$charts = $("#div-charts").children();
|
||||
this.fb = new FilterBox("#txt-query", this.filterCharts.bind(this));
|
||||
Dispatcher.bind("#div-charts").on("delete-chart", this.deleteChart.bind(this));
|
||||
}
|
||||
deleteChart(e) {
|
||||
let $container = $(e.target).closest("div.column");
|
||||
let name = $container.data("name");
|
||||
Modal.confirm(`Are you sure to delete chart: <strong>${name}</strong>?`, "Delete chart", (dlg, e) => {
|
||||
$ajax.post(name + "/delete").trigger(e.target).json(r => {
|
||||
$container.remove();
|
||||
dlg.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
filterCharts(text) {
|
||||
if (!text) {
|
||||
this.$charts.show();
|
||||
return;
|
||||
}
|
||||
this.$charts.each((i, elem) => {
|
||||
let $elem = $(elem), texts = [
|
||||
$elem.data("name").toLowerCase(),
|
||||
$elem.data("title").toLowerCase(),
|
||||
$elem.data("desc").toLowerCase(),
|
||||
];
|
||||
for (let i = 0; i < texts.length; i++) {
|
||||
let index = texts[i].indexOf(text);
|
||||
if (index >= 0) {
|
||||
$(elem).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$(elem).hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
Metric.ListPage = ListPage;
|
||||
})(Metric = Swirl.Metric || (Swirl.Metric = {}));
|
||||
})(Swirl || (Swirl = {}));
|
||||
//# sourceMappingURL=swirl.js.map
|
File diff suppressed because one or more lines are too long
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 {
|
||||
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());
|
||||
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/graph.ts" />
|
||||
namespace Swirl.Service {
|
||||
class MetricChartOptions {
|
||||
type?: string = "line";
|
||||
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();
|
||||
}
|
||||
}
|
||||
import Modal = Swirl.Core.Modal;
|
||||
import GraphPanel = Swirl.Core.GraphPanel;
|
||||
|
||||
export class StatsPage {
|
||||
private chartOptions: { [index: string]: MetricChartOptions } = {
|
||||
"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;
|
||||
private panel: GraphPanel;
|
||||
|
||||
constructor() {
|
||||
let $cb_time = $("#cb-time");
|
||||
@ -120,53 +13,24 @@ namespace Swirl.Service {
|
||||
return;
|
||||
}
|
||||
|
||||
$cb_time.change(this.loadData.bind(this));
|
||||
$("#cb-refresh").change(() => {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
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.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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/ext/times"
|
||||
@ -37,18 +38,18 @@ type metricBiz struct {
|
||||
}
|
||||
|
||||
func (b *metricBiz) GetServiceCharts(service string, categories []string) (charts []model.ChartInfo) {
|
||||
charts = append(charts, model.NewChartInfo("cpu", "CPU", "name", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`))
|
||||
charts = append(charts, model.NewChartInfo("memory", "Memory", "name", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("network_in", "Network Receive", "name", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
|
||||
charts = append(charts, model.NewChartInfo("network_out", "Network Send", "name", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
|
||||
charts = append(charts, model.NewChartInfo("cpu", "CPU", "${name}", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`))
|
||||
charts = append(charts, model.NewChartInfo("memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
|
||||
charts = append(charts, model.NewChartInfo("network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
|
||||
for _, c := range categories {
|
||||
if c == "java" {
|
||||
charts = append(charts, model.NewChartInfo("threads", "Threads", "instance", `jvm_threads_current{service="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "instance", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`))
|
||||
charts = append(charts, model.NewChartInfo("threads", "Threads", "${instance}", `jvm_threads_current{service="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`))
|
||||
} else if c == "go" {
|
||||
charts = append(charts, model.NewChartInfo("threads", "Threads", "instance", `go_threads{service="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("goroutines", "Goroutines", "instance", `go_goroutines{service="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "instance", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`))
|
||||
charts = append(charts, model.NewChartInfo("threads", "Threads", "${instance}", `go_threads{service="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("goroutines", "Goroutines", "${instance}", `go_goroutines{service="%s"}`))
|
||||
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`))
|
||||
}
|
||||
}
|
||||
for i, c := range charts {
|
||||
@ -75,7 +76,7 @@ func (b *metricBiz) GetMatrix(query, label string, start, end time.Time) (lines
|
||||
|
||||
matrix := value.(pmodel.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 {
|
||||
p := model.ChartPoint{
|
||||
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
|
||||
}
|
||||
|
||||
func (b *metricBiz) GetVector(query string, t time.Time) (values []float64, err error) {
|
||||
api, err := b.getAPI()
|
||||
func (b *metricBiz) GetVector(query, label string, t time.Time) (cv model.ChartVector, err error) {
|
||||
var api papi.API
|
||||
api, err = b.getAPI()
|
||||
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 {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
vector := value.(pmodel.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
|
||||
}
|
||||
@ -143,3 +149,12 @@ func (b *metricBiz) getAPI() (api papi.API, err error) {
|
||||
}
|
||||
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.role: Roles
|
||||
field.user: Users
|
||||
field.width: Width
|
||||
field.height: Height
|
||||
field.unit: Unit
|
||||
field.title: Title
|
||||
field.label: Label
|
||||
field.dashboard: Dashboard
|
||||
|
||||
# menu
|
||||
menu.home: Home
|
||||
@ -69,6 +75,7 @@ menu.role: Roles
|
||||
menu.user: Users
|
||||
menu.setting: Settings
|
||||
menu.event: Events
|
||||
menu.chart: Charts
|
||||
menu.version: Version
|
||||
menu.profile: Profile
|
||||
menu.password: Password
|
||||
@ -90,7 +97,7 @@ home.node: Nodes
|
||||
home.network: Networks
|
||||
home.service: Services
|
||||
home.stack: Stacks
|
||||
home.feature: Features
|
||||
home.monitor: Monitors
|
||||
|
||||
# image pages
|
||||
image.title: Image
|
||||
@ -169,6 +176,10 @@ setting.description: Manage Swirl system settings.
|
||||
event.title: Event
|
||||
event.description: Manage all user events.
|
||||
|
||||
# chart pages
|
||||
chart.title: Chart
|
||||
chart.description: Manage all metric charts.
|
||||
|
||||
# profile pages
|
||||
profile.title: Profile
|
||||
profile.description: User profiles.
|
||||
|
@ -45,6 +45,12 @@ field.driver: 驱动
|
||||
field.scope: 范围
|
||||
field.role: 角色
|
||||
field.user: 用户
|
||||
field.width: 宽度
|
||||
field.height: 高度
|
||||
field.unit: 单位
|
||||
field.title: 标题
|
||||
field.label: 标签
|
||||
field.dashboard: 仪表盘
|
||||
|
||||
# menu
|
||||
menu.home: 首页
|
||||
@ -69,6 +75,7 @@ menu.role: 角色
|
||||
menu.user: 用户
|
||||
menu.setting: 设置
|
||||
menu.event: 事件
|
||||
menu.chart: 图表
|
||||
menu.version: 版本
|
||||
menu.profile: 资料
|
||||
menu.password: 密码
|
||||
@ -90,7 +97,7 @@ home.node: 节点
|
||||
home.network: 网络
|
||||
home.service: 服务
|
||||
home.stack: 编排
|
||||
home.feature: 功能
|
||||
home.monitor: 监控
|
||||
|
||||
# image pages
|
||||
image.title: 镜像
|
||||
@ -169,6 +176,10 @@ setting.description: 管理 Swirl 系统设置。
|
||||
event.title: 事件
|
||||
event.description: 管理用户操作日志。
|
||||
|
||||
# chart pages
|
||||
chart.title: 图表
|
||||
chart.description: 管理监控图表。
|
||||
|
||||
# profile pages
|
||||
profile.title: 资料
|
||||
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) {
|
||||
var (
|
||||
count int
|
||||
m = newModel(ctx)
|
||||
count int
|
||||
setting *model.Setting
|
||||
charts []*model.Chart
|
||||
m = newModel(ctx)
|
||||
)
|
||||
|
||||
if count, err = docker.NodeCount(); err != nil {
|
||||
@ -56,6 +58,15 @@ func homeIndex(ctx web.Context) (err error) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cuigh/auxo/data"
|
||||
"github.com/cuigh/auxo/data/set"
|
||||
"github.com/cuigh/auxo/errors"
|
||||
"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"`
|
||||
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"`
|
||||
Metrics web.HandlerFunc `path:"/:name/metrics" name:"service.metrics" authorize:"?"`
|
||||
}
|
||||
|
||||
// Service creates an instance of ServiceController
|
||||
@ -52,7 +50,6 @@ func Service() (c *ServiceController) {
|
||||
PermEdit: servicePermEdit,
|
||||
PermUpdate: permUpdate("service", "name"),
|
||||
Stats: serviceStats,
|
||||
Metrics: serviceMetrics,
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,13 +294,9 @@ func serviceStats(ctx web.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var charts []model.ChartInfo
|
||||
var charts []*model.Chart
|
||||
if setting.Metrics.Prometheus != "" {
|
||||
var categories []string
|
||||
if label := service.Spec.Labels["swirl.metrics"]; label != "" {
|
||||
categories = strings.Split(label, ",")
|
||||
}
|
||||
charts = biz.Metric.GetServiceCharts(name, categories)
|
||||
charts, err = biz.Chart.GetServiceCharts(name)
|
||||
}
|
||||
|
||||
period := cast.ToDuration(ctx.Q("time"), time.Hour)
|
||||
@ -312,31 +305,3 @@ func serviceStats(ctx web.Context) error {
|
||||
Set("Refresh", refresh).Set("Charts", charts)
|
||||
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)
|
||||
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.
|
||||
|
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/setting", controller.Setting())
|
||||
g.Handle("/system/event", controller.Event())
|
||||
g.Handle("/system/chart", controller.Chart())
|
||||
|
||||
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"`
|
||||
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 {
|
||||
Prometheus string `bson:"prometheus" json:"prometheus"`
|
||||
} `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"`
|
||||
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) {
|
||||
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 {
|
||||
log.Get("scaler").Error("scaler > Failed to query metrics: ", err)
|
||||
return scaleNone, 0
|
||||
}
|
||||
if len(values) == 0 {
|
||||
if len(vector.Data) == 0 {
|
||||
return scaleNone, 0
|
||||
}
|
||||
|
||||
value := values[0]
|
||||
value := vector.Data[0]
|
||||
if value <= low {
|
||||
return scaleDown, value
|
||||
} else if value >= high {
|
||||
|
@ -221,4 +221,15 @@ var Perms = []PermGroup{
|
||||
{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/setting/">{{ i18n("menu.setting") }}</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">
|
||||
<div class="navbar-item">
|
||||
<div class="navbar-content">
|
||||
|
@ -1,5 +1,10 @@
|
||||
{{ 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) }}
|
||||
<div class="column">
|
||||
<div class="notification is-{{style}}">
|
||||
@ -28,7 +33,7 @@
|
||||
{{end}}
|
||||
|
||||
{{ block body() }}
|
||||
<section class="hero is-primary is-medium is-bold">
|
||||
<section class="hero is-primary is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<h1 class="title is-2">
|
||||
@ -36,18 +41,12 @@
|
||||
</h1>
|
||||
<h2 class="subtitle is-5">{{ i18n("description") }}</h2>
|
||||
<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">
|
||||
<i class="fab fa-github"></i>
|
||||
</span>
|
||||
<span>GitHub</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -64,51 +63,48 @@
|
||||
{{yield infobox(style="primary", icon="server", text=i18n("home.stack"), url="/stack/task/", count=.StackCount)}}
|
||||
</div>
|
||||
<hr>
|
||||
<h3 class="title is-5">{{ i18n("home.feature") }}</h3>
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="card is-fullwidth">
|
||||
<nav class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<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">
|
||||
<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>
|
||||
<div class="card-content">
|
||||
{{yield progress(title=i18n("menu.image"), percent=100)}}
|
||||
{{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)}}
|
||||
<canvas id="canvas_{{ .Name }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
@ -21,9 +21,7 @@
|
||||
<section class="hero is-small is-light">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title is-2">
|
||||
{{ .Service.Spec.Name }}
|
||||
</h2>
|
||||
<h2 id="h2-service-name" class="title is-2">{{ .Service.Spec.Name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -74,21 +72,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<div class="columns is-multiline">
|
||||
<div id="div-charts" class="columns is-multiline">
|
||||
{{ range .Charts }}
|
||||
<div class="column is-12">
|
||||
<div class="block">
|
||||
<div class="block-header">
|
||||
<p>{{ .Title }}</p>
|
||||
</div>
|
||||
<div class="block-body is-bordered">
|
||||
<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">
|
||||
<p class="card-header-title">{{ .Title }}</p>
|
||||
<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>
|
||||
<div class="card-content">
|
||||
<canvas id="canvas_{{ .Name }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<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.
|
||||
|
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 class="is-active">
|
||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||
</li>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -24,6 +24,9 @@
|
||||
<li>
|
||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -29,7 +29,10 @@
|
||||
</li>
|
||||
<li>
|
||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||
</li>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -24,6 +24,9 @@
|
||||
<li>
|
||||
<a href="/system/event/">{{ i18n("menu.event") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/system/chart/">{{ i18n("menu.chart") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user