Browse Source

redesign Dashboard

0xJacky 3 years ago
parent
commit
46e86d2a3d

+ 2 - 0
frontend/package.json

@@ -9,11 +9,13 @@
     },
     "dependencies": {
         "ant-design-vue": "^1.7.3",
+        "apexcharts": "^3.33.1",
         "axios": "^0.21.1",
         "chart.js": "^2.9.4",
         "core-js": "^3.9.0",
         "reconnecting-websocket": "^4.4.0",
         "vue": "^2.6.11",
+        "vue-apexcharts": "^1.6.2",
         "vue-chartjs": "^3.5.1",
         "vue-codemirror": "^4.0.6",
         "vue-gettext": "^2.1.12",

+ 2 - 2
frontend/src/api/analytic.js

@@ -1,8 +1,8 @@
 import http from '@/lib/http'
 
 const analytic = {
-    cpu_usage() {
-        return http.get('/analytic/cpu')
+    init() {
+        return http.get('/analytic/init')
     }
 }
 

+ 83 - 0
frontend/src/components/Chart/CPUChart.vue

@@ -0,0 +1,83 @@
+<template>
+    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+export default {
+    name: 'CPUChart',
+    props: {
+        series: Array
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'area',
+                    zoom: {
+                        enabled: false
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                    toolbar: {
+                        show: false
+                    },
+                },
+                colors: ['#ff6385', '#36a3eb'],
+                fill: {
+                    // type: ['solid', 'gradient'],
+                    gradient: {
+                        shade: 'light'
+                    }
+                    //colors:  ['#ff6385', '#36a3eb'],
+                },
+                dataLabels: {
+                    enabled: false
+                },
+                stroke: {
+                    curve: 'smooth',
+                    width: 0,
+                },
+                xaxis: {
+                    type: 'datetime',
+                    labels: {datetimeUTC: false},
+                },
+                tooltip: {
+                    enabled: false
+                },
+                yaxis: {
+                    max: 100,
+                    tickAmount: 4,
+                    min: 0,
+                },
+                legend: {
+                    onItemClick: {
+                        toggleDataSeries: false
+                    },
+                    onItemHover: {
+                        highlightDataSeries: false
+                    },
+                }
+            },
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 82 - 0
frontend/src/components/Chart/DiskChart.vue

@@ -0,0 +1,82 @@
+<template>
+    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+export default {
+    name: 'DiskChart',
+    props: {
+        series: Array
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'area',
+                    zoom: {
+                        enabled: false
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                    toolbar: {
+                        show: false
+                    },
+                },
+                colors: ['#ff6385', '#36a3eb'],
+                fill: {
+                    // type: ['solid', 'gradient'],
+                    gradient: {
+                        shade: 'light'
+                    }
+                    //colors:  ['#ff6385', '#36a3eb'],
+                },
+                dataLabels: {
+                    enabled: false
+                },
+                stroke: {
+                    curve: 'smooth',
+                    width: 0,
+                },
+                xaxis: {
+                    type: 'datetime',
+                    labels: {datetimeUTC: false},
+                },
+                tooltip: {
+                    enabled: false
+                },
+                yaxis: {
+                    tickAmount: 3,
+                    min: 0,
+                },
+                legend: {
+                    onItemClick: {
+                        toggleDataSeries: false
+                    },
+                    onItemHover: {
+                        highlightDataSeries: false
+                    },
+                }
+            },
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 87 - 0
frontend/src/components/Chart/NetChart.vue

@@ -0,0 +1,87 @@
+<template>
+    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+export default {
+    name: 'NetChart',
+    props: {
+        series: Array
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'area',
+                    zoom: {
+                        enabled: false
+                    },
+                    animations: {
+                        enabled: false,
+                    },
+                    toolbar: {
+                        show: false
+                    },
+                },
+                colors: ['#ff6385', '#36a3eb'],
+                fill: {
+                    // type: ['solid', 'gradient'],
+                    gradient: {
+                        shade: 'light'
+                    }
+                    //colors:  ['#ff6385', '#36a3eb'],
+                },
+                dataLabels: {
+                    enabled: false
+                },
+                stroke: {
+                    curve: 'smooth',
+                    width: 0,
+                },
+                xaxis: {
+                    type: 'datetime',
+                    labels: {datetimeUTC: false},
+                },
+                tooltip: {
+                    enabled: false
+                },
+                yaxis: {
+                    tickAmount: 3,
+                    min: 0,
+                    labels: {
+                        formatter: (bytes) => {
+                            return this.bytesToSize(bytes) + '/s'
+                        }
+                    }
+                },
+                legend: {
+                    onItemClick: {
+                        toggleDataSeries: false
+                    },
+                    onItemHover: {
+                        highlightDataSeries: false
+                    },
+                }
+            },
+        }
+    },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 93 - 0
frontend/src/components/Chart/RadialBarChart.vue

@@ -0,0 +1,93 @@
+<template>
+    <div class="container">
+        <p class="text">{{ centerText }}</p>
+        <apexchart type="radialBar" height="205" :options="chartOptions" :series="series" ref="chart"/>
+    </div>
+</template>
+
+<script>
+import VueApexCharts from 'vue-apexcharts'
+import Vue from 'vue'
+
+Vue.use(VueApexCharts)
+Vue.component('apexchart', VueApexCharts)
+export default {
+    name: 'RadialBarChart',
+    props: {
+        series: Array,
+        centerText: String,
+        colors: String,
+        name: String,
+    },
+    watch: {
+        series: {
+            deep: true,
+            handler() {
+                this.$refs.chart.updateSeries(this.series)
+            }
+        }
+    },
+    data() {
+        return {
+            chartOptions: {
+                series: this.series,
+                chart: {
+                    type: 'radialBar',
+                    offsetY: -10
+                },
+                plotOptions: {
+                    radialBar: {
+                        startAngle: -135,
+                        endAngle: 135,
+                        dataLabels: {
+                            name: {
+                                fontSize: '15px',
+                                color: this.colors,
+                                offsetY: 56
+                            },
+                            value: {
+                                offsetY: 60,
+                                fontSize: '14px',
+                                color: undefined,
+                                formatter: function (val) {
+                                    return val + "%";
+                                }
+                            }
+                        }
+                    }
+                },
+                fill: {
+                    colors: this.colors
+                },
+                labels: [this.name],
+                states: {
+                    hover: {
+                        filter: {
+                            type: 'none'
+                        }
+                    },
+                    active: {
+                        filter: {
+                            type: 'none'
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+</script>
+
+<style lang="less" scoped>
+.container {
+    position: relative;
+    margin: 0 auto;
+    .text {
+        position: absolute;
+        top: calc(72px);
+        width: 100%;
+        text-align: center;
+
+    }
+}
+</style>

+ 8 - 9
frontend/src/router/index.js

@@ -62,17 +62,16 @@ export const routes = [
                 meta: {
                     icon: 'file',
                     hideChildren: true
+                }
+            },
+            {
+                path: 'config/:name',
+                name: $gettext('Edit Configuration'),
+                component: () => import('@/views/config/ConfigEdit.vue'),
+                meta: {
+                    hiddenInSidebar: true
                 },
-                children: [{
-                    path: 'config/:name',
-                    name: $gettext('Edit Configuration'),
-                    component: () => import('@/views/config/ConfigEdit.vue'),
-                    meta: {
-                        hiddenInSidebar: true
-                    },
-                },]
             },
-
             {
                 path: 'about',
                 name: $gettext('About'),

+ 2 - 2
frontend/src/views/config/ConfigEdit.vue

@@ -3,8 +3,8 @@
         <vue-itextarea v-model="configText"/>
         <footer-tool-bar>
             <a-space>
-                <a-button @click="$router.go(-1)" v-translate>Cancel</a-button>
-                <a-button type="primary" @click="save" v-translate>Save</a-button>
+                <a-button @click="$router.go(-1)"><translate>Cancel</translate></a-button>
+                <a-button type="primary" @click="save"><translate>Save</translate></a-button>
             </a-space>
         </footer-tool-bar>
     </a-card>

+ 231 - 96
frontend/src/views/dashboard/DashBoard.vue

@@ -1,64 +1,148 @@
 <template>
     <div>
-        <a-row class="row-two">
-            <a-col :lg="24" :sm="24">
-                <a-card style="min-height: 400px" :title="$gettext('Server Status')">
+        <a-row :gutter="[16,16]" class="first-row">
+            <a-col :lg="7" :md="24">
+                <a-card :title="$gettext('Server Info')">
+                    <p>
+                        <translate>Uptime:</translate>
+                        {{ uptime }}
+                    </p>
+                    <p>
+                        <translate>Load Averages:</translate>
+                        1min:{{ loadavg?.load1?.toFixed(2) }} |
+                        5min:{{ loadavg?.load5?.toFixed(2) }} |
+                        15min:{{ loadavg?.load15?.toFixed(2) }}
+                    </p>
+                    <p>
+                        <translate>OS:</translate>
+                        {{ host.platform }} ({{ host.platformVersion }}
+                        {{ host.os }} {{ host.kernelVersion }}
+                        {{ host.kernelArch }})
+                    </p>
+                    <p>
+                        <translate>CPU:</translate>
+                        {{ cpu_info[0]?.modelName }} * {{ cpu_info.length }}
+                    </p>
+<!--                    <p><translate>Memory</translate>: {{-->
+<!--                            $gettextInterpolate(-->
+<!--                                $gettext('Used: %{u}, Cached: %{c}, Free: %{f}, Physical Memory: %{p}'),-->
+<!--                                {u: memory_used, c: memory_cached, f: memory_free, p: memory_total})-->
+<!--                        }}</p>-->
+<!--                    <p><translate>Storage</translate>: {{-->
+<!--                            $gettextInterpolate($gettext('Used: %{used} / Total: %{total}'),-->
+<!--                                {used: disk_used, total: disk_total})-->
+<!--                        }}-->
+<!--                    </p>-->
+                </a-card>
+            </a-col>
+            <a-col :lg="12" :md="24" class="chart_dashboard">
+                <a-card>
                     <a-row>
-                        <a-col :lg="12" :sm="24" class="chart">
-                            <a-statistic :value="cpu" style="margin: 0 50px 10px 0" title="CPU">
+                        <a-col :xs="24" :sm="24" :md="8">
+                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory_pressure]"
+                                              :centerText="memory_used" colors="#36a3eb"/>
+                        </a-col>
+                        <a-col :xs="24" :sm="12" :md="8">
+                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory_swap_percent]"
+                                              :centerText="memory_swap_used" colors="#ff6385"/>
+                        </a-col>
+                        <a-col :xs="24" :sm="12" :md="8">
+                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk_percentage]"
+                                              :centerText="disk_used" colors="#87d068"/>
+                        </a-col>
+                    </a-row>
+                </a-card>
+            </a-col>
+            <a-col :lg="5" :sm="24" class="chart_dashboard">
+                <a-card>
+                    <a-row :gutter="16">
+                        <a-col :span="24">
+                            <a-statistic :value="bytesToSize(net.last_recv)"
+                                         :title="$gettext('Network Total Receive')"/>
+                        </a-col>
+                        <a-col :span="24">
+                            <a-statistic :value="bytesToSize(net.last_sent)"
+                                         :title="$gettext('Network Total Send')" />
+                        </a-col>
+                    </a-row>
+                </a-card>
+            </a-col>
+        </a-row>
+        <a-row class="row-two" :gutter="[16,32]">
+            <a-col :lg="8" :md="24" :sm="24">
+                <a-card :title="$gettext('CPU Status')">
+                    <a-statistic :value="cpu" title="CPU">
+                        <template v-slot:suffix>
+                            <span>%</span>
+                        </template>
+                    </a-statistic>
+                    <c-p-u-chart :series="cpu_analytic_series"/>
+                </a-card>
+            </a-col>
+            <a-col :lg="8" :md="24" :sm="24">
+                <a-card :title="$gettext('Network')">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.recv)"
+                                         :title="$gettext('Receive')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
+                        </a-col>
+                        <a-col :span="12">
+                            <a-statistic :value="bytesToSize(net.sent)" :title="$gettext('Send')">
                                 <template v-slot:suffix>
-                                    <span>%</span>
+                                    <span>/s</span>
                                 </template>
                             </a-statistic>
-                            <p>
-                                <translate>Uptime</translate>
-                                {{ uptime }}
-                            </p>
-                            <p>
-                                <translate>Load Averages:</translate>
-                                1min:{{ loadavg?.load1?.toFixed(2) }} |
-                                5min:{{ loadavg?.load5?.toFixed(2) }} |
-                                15min:{{ loadavg?.load15?.toFixed(2) }}
-                            </p>
-                            <line-chart :chart-data="cpu_analytic" :options="cpu_analytic.options" :height="150"/>
                         </a-col>
-                        <a-col :lg="6" :sm="8" :xs="12" class="chart_dashboard">
-                            <div>
-                                <a-tooltip
-                                    :title="$gettextInterpolate(
-                                        $gettext('Used: %{u}, Cached: %{c}, Free: %{f}, Physical Memory: %{p}'),
-                                         {u: memory_used, c: memory_cached, f:memory_free, p: memory_total})">
-                                    <a-progress :percent="memory_pressure" strokeColor="rgb(135, 208, 104)"
-                                                type="dashboard"/>
-                                    <p class="description" v-translate>Memory</p>
-                                </a-tooltip>
-                            </div>
+                    </a-row>
+                    <net-chart :series="net_analytic"/>
+                </a-card>
+            </a-col>
+            <a-col :lg="8" :md="24" :sm="24">
+                <a-card :title="$gettext('Disk IO')">
+                    <a-row :gutter="16">
+                        <a-col :span="12">
+                            <a-statistic :value="diskIO.writes"
+                                         :title="$gettext('Writes')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
                         </a-col>
-                        <a-col :lg="6" :sm="8" :xs="12" class="chart_dashboard">
-                            <div>
-                                <a-tooltip
-                                    :title="$gettextInterpolate($gettext('Used: %{used} / Total: %{total}'),
-                                    {used: disk_used, total: disk_total})">
-                                    <a-progress :percent="disk_percentage" type="dashboard"/>
-                                    <p class="description" v-translate>Storage</p>
-                                </a-tooltip>
-                            </div>
+                        <a-col :span="12">
+                            <a-statistic :value="diskIO.reads" :title="$gettext('Reads')">
+                                <template v-slot:suffix>
+                                    <span>/s</span>
+                                </template>
+                            </a-statistic>
                         </a-col>
                     </a-row>
+                    <disk-chart :series="diskIO_analytic"/>
                 </a-card>
             </a-col>
         </a-row>
+
     </div>
 </template>
 
 <script>
-import LineChart from '@/components/Chart/LineChart'
 import ReconnectingWebSocket from 'reconnecting-websocket'
+import CPUChart from '@/components/Chart/CPUChart'
+import NetChart from '@/components/Chart/NetChart'
+import $gettext from '@/lib/translate/gettext'
+import RadialBarChart from '@/components/Chart/RadialBarChart'
+import DiskChart from '@/components/Chart/DiskChart'
 
 export default {
     name: 'DashBoard',
     components: {
-        LineChart
+        DiskChart,
+        RadialBarChart,
+        NetChart,
+        CPUChart,
     },
     data() {
         return {
@@ -70,55 +154,47 @@ export default {
             memory_cached: '',
             memory_free: '',
             memory_total: '',
-            cpu_analytic: {
-                datasets: [{
-                    label: 'cpu user',
-                    borderColor: '#36a3eb',
-                    backgroundColor: '#36a3eb',
-                    pointRadius: 0,
-                    data: [],
-                }, {
-                    label: 'cpu total',
-                    borderColor: '#ff6385',
-                    backgroundColor: '#ff6385',
-                    pointRadius: 0,
-                    data: [],
-                }],
-                options: {
-                    responsive: true,
-                    maintainAspectRatio: false,
-                    responsiveAnimationDuration: 0, // Duration of the animation after resizing 调整大小后的动画持续时间
-                    elements: {
-                        line: {
-                            tension: 0 // Disable Bessel curves 禁用贝塞尔曲线
-                        }
-                    },
-                    scales: {
-                        yAxes: [{
-                            ticks: {
-                                max: 100,
-                                min: 0,
-                                stepSize: 20,
-                                display: true
-                            }
-                        }],
-                        xAxes: [
-                            {
-                                type: 'time',
-                                time: {
-                                    unit: 'minute',
-                                }
-                            }
-                        ]
-                    }
-                },
-            },
+            cpu_analytic_series: [{
+                name: 'CPU User',
+                data: []
+            }, {
+                name: 'CPU Total',
+                data: []
+            }],
             cpu: 0,
+            memory_swap_used: '',
+            memory_swap_percent: 0,
             disk_percentage: 0,
             disk_total: '',
             disk_used: '',
+            net: {
+                recv: 0,
+                sent: 0,
+                last_recv: 0,
+                last_sent: 0,
+            },
+            diskIO: {
+                writes: 0,
+                reads: 0,
+            },
+            net_analytic: [{
+                name: $gettext('Receive'),
+                data: []
+            }, {
+                name: $gettext('Send'),
+                data: []
+            }],
+            diskIO_analytic: [{
+                name: $gettext('Writes'),
+                data: []
+            }, {
+                name: $gettext('Reads'),
+                data: []
+            }],
             uptime: '',
-            loadavg: {}
+            loadavg: {},
+            cpu_info: [],
+            host: {}
         }
     },
     created() {
@@ -126,9 +202,25 @@ export default {
             + btoa(this.$store.state.user.token))
         this.websocket.onmessage = this.wsOnMessage
         this.websocket.onopen = this.wsOpen
-        this.$api.analytic.cpu_usage().then(r => {
-            this.cpu_analytic.datasets[0].data = this.cpu_analytic.datasets[0].data.concat(r.user)
-            this.cpu_analytic.datasets[1].data = this.cpu_analytic.datasets[1].data.concat(r.total)
+        this.$api.analytic.init().then(r => {
+            this.cpu_info = r.cpu.info
+            this.net.last_recv = r.network.init.bytesRecv
+            this.net.last_sent = r.network.init.bytesSent
+            this.host = r.host
+            r.cpu.user.forEach(u => {
+                this.cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
+            })
+            r.cpu.total.forEach(u => {
+                this.cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
+            })
+            r.network.bytesRecv.forEach(u => {
+                this.net_analytic[0].data.push([u.x, u.y.toFixed(2)])
+            })
+            r.network.bytesSent.forEach(u => {
+                this.net_analytic[1].data.push([u.x, u.y.toFixed(2)])
+            })
+            this.diskIO_analytic[0].data = this.diskIO_analytic[0].data.concat(r.diskIO.writes)
+            this.diskIO_analytic[1].data = this.diskIO_analytic[1].data.concat(r.diskIO.reads)
         })
     },
     destroyed() {
@@ -142,24 +234,31 @@ export default {
             const r = JSON.parse(m.data)
             // console.log(r)
             this.cpu = r.cpu_system + r.cpu_user
-            const time = new Date()
-            this.cpu_analytic.datasets[0].data
-                .push({x: time, y: r.cpu_user})
-            this.cpu_analytic.datasets[1].data
-                .push({x: time, y: this.cpu})
-            if (this.cpu_analytic.datasets[0].data.length > 200) {
-                this.cpu_analytic.datasets[0].data.shift()
-                this.cpu_analytic.datasets[1].data.shift()
-            }
             this.cpu = this.cpu.toFixed(2)
+            const time = new Date().getTime()
+
+            this.cpu_analytic_series[0].data.push([time, r.cpu_user.toFixed(2)])
+            this.cpu_analytic_series[1].data.push([time, this.cpu])
+
+            if (this.cpu_analytic_series[0].data.length > 100) {
+                this.cpu_analytic_series[0].data.shift()
+                this.cpu_analytic_series[1].data.shift()
+            }
+
+            // mem
             this.memory_pressure = r.memory_pressure
             this.memory_used = r.memory_used
             this.memory_cached = r.memory_cached
             this.memory_free = r.memory_free
             this.memory_total = r.memory_total
+            this.memory_swap_percent = r.memory_swap_percent
+            this.memory_swap_used = r.memory_swap_used
+
+            // disk
             this.disk_percentage = r.disk_percentage
             this.disk_used = r.disk_used
             this.disk_total = r.disk_total
+
             let uptime = Math.floor(r.uptime)
             let uptime_days = Math.floor(uptime / 86400)
             uptime -= uptime_days * 86400
@@ -167,16 +266,52 @@ export default {
             uptime -= uptime_hours * 3600
             this.uptime = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(uptime / 60) + 'm'
             this.loadavg = r.loadavg
+
+            // net
+            this.net.recv = r.network.bytesRecv - this.net.last_recv
+            this.net.sent = r.network.bytesSent - this.net.last_sent
+            this.net.last_recv = r.network.bytesRecv
+            this.net.last_sent = r.network.bytesSent
+
+            this.net_analytic[0].data.push([time, this.net.recv])
+            this.net_analytic[1].data.push([time, this.net.sent])
+
+            if (this.net_analytic[0].data.length > 100) {
+                this.net_analytic[1].data.shift()
+                this.net_analytic[0].data.shift()
+            }
+
+            // diskIO
+            this.diskIO.writes = r.diskIO.writes.y
+            this.diskIO.reads = r.diskIO.reads.y
+
+            this.diskIO_analytic[0].data.push(r.diskIO.writes)
+            this.diskIO_analytic[1].data.push(r.diskIO.reads)
+
+            if (this.diskIO_analytic[0].data.length > 100) {
+                this.diskIO_analytic[0].data.shift()
+                this.diskIO_analytic[1].data.shift()
+            }
         }
     }
 }
 </script>
 
 <style lang="less" scoped>
+.first-row {
+    .ant-card {
+        min-height: 227px;
+    }
+}
+
 .ant-card {
+    .ant-statistic {
+        margin: 0 50px 10px 10px
+    }
 
     .chart {
-        max-height: 300px;
+        max-width: 800px;
+        max-height: 350px;
     }
 
     .chart_dashboard {

+ 2 - 2
frontend/src/views/domain/DomainEdit.vue

@@ -21,8 +21,8 @@
 
         <footer-tool-bar>
             <a-space>
-                <a-button @click="$router.push('/domain/list')" v-translate>Cancel</a-button>
-                <a-button type="primary" @click="save" v-translate>Save</a-button>
+                <a-button @click="$router.go(-1)"><translate>Cancel</translate></a-button>
+                <a-button type="primary" @click="save"><translate>Save</translate></a-button>
             </a-space>
         </footer-tool-bar>
     </div>

+ 72 - 0
frontend/yarn.lock

@@ -2536,6 +2536,18 @@ anymatch@~3.1.1:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
+apexcharts@^3.33.1:
+  version "3.33.1"
+  resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.33.1.tgz#7159f45e7d726a548e5135a327c03e7894d0bf13"
+  integrity sha512-5aVzrgJefd8EH4w7oRmuOhA3+cxJxQg27cYg3ANVGvPCOB4AY3mVVNtFHRFaIq7bv8ws4GRaA9MWfzoWQw3MPQ==
+  dependencies:
+    svg.draggable.js "^2.2.2"
+    svg.easing.js "^2.0.0"
+    svg.filter.js "^2.0.2"
+    svg.pathmorphing.js "^0.1.3"
+    svg.resize.js "^1.4.3"
+    svg.select.js "^3.0.1"
+
 aproba@^1.0.3, aproba@^1.1.1:
   version "1.2.0"
   resolved "https://registry.npm.taobao.org/aproba/download/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -9739,6 +9751,61 @@ svg-tags@^1.0.0:
   resolved "https://registry.npm.taobao.org/svg-tags/download/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
   integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
 
+svg.draggable.js@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
+  integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
+  dependencies:
+    svg.js "^2.0.1"
+
+svg.easing.js@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12"
+  integrity sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=
+  dependencies:
+    svg.js ">=2.3.x"
+
+svg.filter.js@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203"
+  integrity sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=
+  dependencies:
+    svg.js "^2.2.5"
+
+svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d"
+  integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
+
+svg.pathmorphing.js@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65"
+  integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
+  dependencies:
+    svg.js "^2.4.0"
+
+svg.resize.js@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332"
+  integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
+  dependencies:
+    svg.js "^2.6.5"
+    svg.select.js "^2.1.2"
+
+svg.select.js@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73"
+  integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
+  dependencies:
+    svg.js "^2.2.5"
+
+svg.select.js@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917"
+  integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
+  dependencies:
+    svg.js "^2.6.5"
+
 svgo@^1.0.0:
   version "1.3.2"
   resolved "https://registry.npm.taobao.org/svgo/download/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
@@ -10304,6 +10371,11 @@ void-elements@^3.1.0:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
   integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
 
+vue-apexcharts@^1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/vue-apexcharts/-/vue-apexcharts-1.6.2.tgz#0547826067f97e8ea67ca9423e524eb6669746ad"
+  integrity sha512-9HS3scJwWgKjmkcWIf+ndNDR0WytUJD8Ju0V2ZYcjYtlTLwJAf2SKUlBZaQTkDmwje/zMgulvZRi+MXmi+WkKw==
+
 vue-chartjs@^3.5.1:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-3.5.1.tgz#d25e845708f7744ae51bed9d23a975f5f8fc6529"

+ 1 - 1
main.go

@@ -36,7 +36,7 @@ func main() {
 	if "" != settings.ServerSettings.JwtSecret {
 		model.Init()
 		go tool.AutoCert()
-		go tool.RecordCpuUsage()
+		go tool.RecordServerAnalytic()
 	}
 
 	srv := &http.Server{

+ 38 - 4
server/api/analytic.go

@@ -10,6 +10,7 @@ import (
 	"github.com/shirou/gopsutil/v3/load"
 	"github.com/shirou/gopsutil/v3/mem"
 	"github.com/shirou/gopsutil/v3/net"
+	"math"
 	"net/http"
 	"runtime"
 	"strconv"
@@ -54,6 +55,10 @@ func Analytic(c *gin.Context) {
 			response["memory_used"] = humanize.Bytes(memoryStat.Used)
 			response["memory_cached"] = humanize.Bytes(memoryStat.Cached)
 			response["memory_free"] = humanize.Bytes(memoryStat.Free)
+			response["memory_swap_used"] = humanize.Bytes(memoryStat.SwapTotal - memoryStat.SwapFree)
+			response["memory_swap_total"] = humanize.Bytes(memoryStat.SwapTotal)
+			response["memory_swap_cached"] = humanize.Bytes(memoryStat.SwapCached)
+			response["memory_swap_percent"] = float64(memoryStat.SwapFree) / math.Max(float64(memoryStat.SwapTotal), 1)
 
 			response["memory_pressure"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f", memoryStat.UsedPercent), 64)
 
@@ -83,8 +88,16 @@ func Analytic(c *gin.Context) {
 			response["disk_total"] = humanize.Bytes(diskUsage.Total)
 			response["disk_percentage"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f", diskUsage.UsedPercent), 64)
 
+			response["diskIO"] = gin.H{
+				"writes": tool.DiskWriteBuffer[len(tool.DiskWriteBuffer)-1],
+				"reads":  tool.DiskReadBuffer[len(tool.DiskReadBuffer)-1],
+			}
+
 			network, _ := net.IOCounters(false)
-			response["network"] = network
+
+			if len(network) > 0 {
+				response["network"] = network[0]
+			}
 
 			m, _ := json.Marshal(response)
 			message = m
@@ -99,9 +112,30 @@ func Analytic(c *gin.Context) {
 	}
 }
 
-func GetCpuUsageRecord(c *gin.Context) {
+func GetAnalyticInit(c *gin.Context) {
+	cpuInfo, _ := cpu.Info()
+	network, _ := net.IOCounters(false)
+	var _net net.IOCountersStat
+	if len(network) > 0 {
+		_net = network[0]
+	}
+	hostInfo, _ := host.Info()
+
 	c.JSON(http.StatusOK, gin.H{
-		"user":  tool.CpuUserBuffer,
-		"total": tool.CpuTotalBuffer,
+		"host": hostInfo,
+		"cpu": gin.H{
+			"info":  cpuInfo,
+			"user":  tool.CpuUserBuffer,
+			"total": tool.CpuTotalBuffer,
+		},
+		"network": gin.H{
+			"init":      _net,
+			"bytesRecv": tool.NetRecvBuffer,
+			"bytesSent": tool.NetSentBuffer,
+		},
+		"diskIO": gin.H{
+			"writes": tool.DiskWriteBuffer,
+			"reads":  tool.DiskReadBuffer,
+		},
 	})
 }

+ 1 - 1
server/router/routers.go

@@ -38,7 +38,7 @@ func InitRouter() *gin.Engine {
 		g := g.Group("/", authRequired())
 		{
 			g.GET("/analytic", api.Analytic)
-			g.GET("/analytic/cpu", api.GetCpuUsageRecord)
+			g.GET("/analytic/init", api.GetAnalyticInit)
 
 			g.GET("/users", api.GetUsers)
 			g.GET("/user/:id", api.GetUser)

+ 2 - 0
server/settings/settings.go

@@ -15,6 +15,7 @@ type Server struct {
 	HTTPChallengePort string
 	Email             string
 	Database          string
+	DiskName          string
 }
 
 var ServerSettings = &Server{
@@ -22,6 +23,7 @@ var ServerSettings = &Server{
 	RunMode:           "debug",
 	HTTPChallengePort: "9180",
 	Database:          "database",
+	DiskName:          "vda",
 }
 
 var ConfPath string

+ 124 - 0
server/tool/analytic.go

@@ -0,0 +1,124 @@
+package tool
+
+import (
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/shirou/gopsutil/v3/cpu"
+	"github.com/shirou/gopsutil/v3/disk"
+	"github.com/shirou/gopsutil/v3/net"
+	"runtime"
+	"time"
+)
+
+type usage struct {
+	Time  time.Time `json:"x"`
+	Usage float64   `json:"y"`
+}
+
+var CpuUserBuffer []usage
+var CpuTotalBuffer []usage
+var NetRecvBuffer []usage
+var NetSentBuffer []usage
+var DiskWriteBuffer []usage
+var DiskReadBuffer []usage
+
+var LastDiskWrites uint64
+var LastDiskReads uint64
+
+var LastNetRecv uint64
+var LastNetSent uint64
+
+func RecordServerAnalytic() {
+	network, _ := net.IOCounters(false)
+	diskIOCounters, _ := disk.IOCounters(settings.ServerSettings.DiskName)
+	diskIO, ok := diskIOCounters[settings.ServerSettings.DiskName]
+
+	if ok {
+		LastDiskWrites = diskIO.WriteCount
+		LastDiskReads = diskIO.ReadCount
+	}
+
+	if len(network) > 0 {
+		LastNetRecv = network[0].BytesRecv
+		LastNetSent = network[0].BytesSent
+	}
+
+	now := time.Now()
+	// 初始化记录数组
+	for i := 100; i > 0; i-- {
+		u := usage{Time: now.Add(time.Duration(-i) * time.Second)}
+		CpuUserBuffer = append(CpuUserBuffer, u)
+		CpuTotalBuffer = append(CpuTotalBuffer, u)
+		NetRecvBuffer = append(NetRecvBuffer, u)
+		NetSentBuffer = append(NetSentBuffer, u)
+		DiskWriteBuffer = append(DiskWriteBuffer, u)
+		DiskReadBuffer = append(DiskReadBuffer, u)
+	}
+	for {
+		cpuTimesBefore, _ := cpu.Times(false)
+		time.Sleep(1000 * time.Millisecond)
+		cpuTimesAfter, _ := cpu.Times(false)
+		threadNum := runtime.GOMAXPROCS(0)
+
+		cpuUserUsage := (cpuTimesAfter[0].User - cpuTimesBefore[0].User) / (float64(1000*threadNum) / 1000)
+		cpuUserUsage *= 100
+		cpuSystemUsage := (cpuTimesAfter[0].System - cpuTimesBefore[0].System) / (float64(1000*threadNum) / 1000)
+		cpuSystemUsage *= 100
+		now := time.Now()
+		u := usage{
+			Time:  now,
+			Usage: cpuUserUsage,
+		}
+		CpuUserBuffer = append(CpuUserBuffer, u)
+		s := usage{
+			Time:  now,
+			Usage: cpuUserUsage + cpuSystemUsage,
+		}
+		CpuTotalBuffer = append(CpuTotalBuffer, s)
+		if len(CpuUserBuffer) > 100 {
+			CpuUserBuffer = CpuUserBuffer[1:]
+		}
+		if len(CpuTotalBuffer) > 100 {
+			CpuTotalBuffer = CpuTotalBuffer[1:]
+		}
+		network, _ = net.IOCounters(false)
+		if len(network) == 0 {
+			continue
+		}
+		NetRecvBuffer = append(NetRecvBuffer, usage{
+			Time:  now,
+			Usage: float64(network[0].BytesRecv - LastNetRecv),
+		})
+		NetSentBuffer = append(NetRecvBuffer, usage{
+			Time:  now,
+			Usage: float64(network[0].BytesSent - LastNetSent),
+		})
+		LastNetRecv = network[0].BytesRecv
+		LastNetSent = network[0].BytesSent
+		if len(NetRecvBuffer) > 100 {
+			NetRecvBuffer = NetRecvBuffer[1:]
+		}
+		if len(NetSentBuffer) > 100 {
+			NetSentBuffer = NetSentBuffer[1:]
+		}
+		diskIOCounters, _ = disk.IOCounters(settings.ServerSettings.DiskName)
+		diskIO, ok = diskIOCounters[settings.ServerSettings.DiskName]
+		if ok {
+			DiskReadBuffer = append(DiskReadBuffer, usage{
+				Time:  now,
+				Usage: float64(diskIO.ReadCount - LastDiskReads),
+			})
+			DiskWriteBuffer = append(DiskWriteBuffer, usage{
+				Time:  now,
+				Usage: float64(diskIO.WriteCount - LastDiskWrites),
+			})
+			if len(DiskReadBuffer) > 100 {
+				DiskReadBuffer = DiskReadBuffer[1:]
+			}
+			if len(DiskWriteBuffer) > 100 {
+				DiskWriteBuffer = DiskWriteBuffer[1:]
+			}
+			LastDiskWrites = diskIO.WriteCount
+			LastDiskReads = diskIO.ReadCount
+		}
+	}
+}

+ 0 - 46
server/tool/cpu_usage.go

@@ -1,46 +0,0 @@
-package tool
-
-import (
-	"github.com/shirou/gopsutil/v3/cpu"
-	"runtime"
-	"time"
-)
-
-type cpuUsage struct {
-	Time  time.Time `json:"x"`
-	Usage float64   `json:"y"`
-}
-
-var CpuUserBuffer []cpuUsage
-var CpuTotalBuffer []cpuUsage
-
-func RecordCpuUsage() {
-	for {
-		cpuTimesBefore, _ := cpu.Times(false)
-		time.Sleep(1000 * time.Millisecond)
-		cpuTimesAfter, _ := cpu.Times(false)
-		threadNum := runtime.GOMAXPROCS(0)
-
-		cpuUserUsage := (cpuTimesAfter[0].User - cpuTimesBefore[0].User) / (float64(1000*threadNum) / 1000)
-		cpuUserUsage *= 100
-		cpuSystemUsage := (cpuTimesAfter[0].System - cpuTimesBefore[0].System) / (float64(1000*threadNum) / 1000)
-		cpuSystemUsage *= 100
-		now := time.Now()
-		u := cpuUsage{
-			Time:  now,
-			Usage: cpuUserUsage,
-		}
-		CpuUserBuffer = append(CpuUserBuffer, u)
-		s := cpuUsage{
-			Time:  now,
-			Usage: cpuUserUsage + cpuSystemUsage,
-		}
-		CpuTotalBuffer = append(CpuTotalBuffer, s)
-		if len(CpuUserBuffer) > 200 {
-			CpuUserBuffer = CpuUserBuffer[1:]
-		}
-		if len(CpuTotalBuffer) > 200 {
-			CpuTotalBuffer = CpuTotalBuffer[1:]
-		}
-	}
-}