Add chart management page

This commit is contained in:
cuigh 2018-03-22 16:13:54 +08:00
parent 93be76045c
commit b2893f5a99
35 changed files with 1670 additions and 423 deletions

5
Gopkg.lock generated
View File

@ -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

View File

@ -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;
}

View File

@ -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

View 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();
})
}
}
}

View 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()]);
}
});
});
}
}
}

View File

@ -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);
// }
// }
// }

View File

@ -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
View 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...");
});
}
}
}

View File

@ -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
View 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
}

View File

@ -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 + "]"
})
}

View File

@ -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.

View File

@ -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
View 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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
View 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
}

View File

@ -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
View 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,
}
}

View File

@ -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,
}
}

View File

@ -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"`
}

View File

@ -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 {

View File

@ -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"},
},
},
}

View File

@ -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">

View File

@ -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 }}

View File

@ -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.

View 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
View 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 }}

View 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 }}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>