Browse Source

new frps dashboard

fatedier 8 years ago
parent
commit
d8683a0079

+ 5 - 0
web/frps/.babelrc

@@ -0,0 +1,5 @@
+{
+    "presets": [
+        ["es2015", { "modules": false }]
+    ]
+}

+ 6 - 0
web/frps/.gitignore

@@ -0,0 +1,6 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log
+.idea
+.vscode/settings.json

+ 9 - 0
web/frps/Makefile

@@ -0,0 +1,9 @@
+.PHONY: dist build
+install:
+	@npm install
+
+dev: install
+	@npm run dev
+
+build:
+	@npm run build

+ 46 - 0
web/frps/package.json

@@ -0,0 +1,46 @@
+{
+  "name": "frps-dashboard",
+  "description": "A dashboard for frp server.",
+  "author": "fatedier",
+  "private": true,
+  "scripts": {
+    "dev": "webpack-dev-server -d --inline --hot --env.dev",
+    "build": "rimraf dist && webpack -p --progress --hide-modules"
+  },
+  "dependencies": {
+    "bootstrap": "^3.3.7",
+    "echarts": "^3.5.0",
+    "element-ui": "^1.2.5",
+    "humanize-plus": "^1.8.2",
+    "vue": "^2.2.4",
+    "vue-resource": "^1.2.1",
+    "vue-router": "^2.3.0"
+  },
+  "engines": {
+    "node": ">=6"
+  },
+  "devDependencies": {
+    "autoprefixer": "^6.6.0",
+    "babel-core": "^6.21.0",
+    "babel-eslint": "^7.1.1",
+    "babel-loader": "^6.4.0",
+    "babel-preset-es2015": "^6.13.2",
+    "css-loader": "^0.27.0",
+    "eslint": "^3.12.2",
+    "eslint-config-enough": "^0.2.2",
+    "eslint-loader": "^1.6.3",
+    "file-loader": "^0.10.1",
+    "html-loader": "^0.4.5",
+    "html-webpack-plugin": "^2.24.1",
+    "less": "^2.7.2",
+    "less-loader": "^3.0.0",
+    "postcss-loader": "^1.3.3",
+    "rimraf": "^2.5.4",
+    "style-loader": "^0.13.2",
+    "url-loader": "^0.5.8",
+    "vue-loader": "^11.1.4",
+    "vue-template-compiler": "^2.1.8",
+    "webpack": "^2.2.0-rc.4",
+    "webpack-dev-server": "beta"
+  }
+}

+ 5 - 0
web/frps/postcss.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+    plugins: [
+        require('autoprefixer')()
+    ]
+}

+ 78 - 0
web/frps/src/App.vue

@@ -0,0 +1,78 @@
+<template>
+    <div id="app">
+        <header class="grid-content header-color">
+            <el-row>
+                <a class="brand" href="#">frp</a>
+            </el-row>
+        </header>
+        <section>
+            <el-row :gutter="20">
+                <el-col id="side-nav" :xs="24" :md="4">
+                    <el-menu default-active="1" mode="vertical" theme="light" router="false" @select="handleSelect">
+                        <el-menu-item index="/">Overview</el-menu-item>
+                        <el-submenu index="/proxies">
+                            <template slot="title">Proxies</template>
+                            <el-menu-item index="/proxies/tcp">TCP</el-menu-item>
+                            <el-menu-item index="/proxies/udp">UDP</el-menu-item>
+                            <el-menu-item index="/proxies/http">HTTP</el-menu-item>
+                            <el-menu-item index="/proxies/https">HTTPS</el-menu-item>
+                        </el-submenu>
+                        <el-menu-item index="">Help</el-menu-item>
+                    </el-menu>
+				</el-col>
+
+				<el-col :xs="24" :md="20">
+                    <div id="content">
+                    <router-view></router-view>
+                    </div>
+				</el-col>
+		</el-row>
+	</section>
+	<footer></footer>
+</div>
+</template>
+
+<script>
+    export default {
+        methods: {
+            handleSelect(key, path) {
+                if (key == '') {
+                    window.open("http://github.com/fatedier/frp")
+                }
+            }
+        }
+    }
+</script>
+
+<style>
+    body {
+        background-color: #fafafa;
+        margin: 0px;
+    }
+    
+    header {
+        width: 100%;
+        height: 60px;
+    }
+    
+    .header-color {
+        background: #58B7FF;
+    }
+    
+    #content {
+        margin-top: 20px;
+        padding-right: 40px;
+    }
+    
+    .brand {
+        color: #fff;
+        background-color: transparent;
+        margin-left: 20px;
+        float: left;
+        line-height: 25px;
+        font-size: 25px;
+        padding: 15px 15px;
+        height: 30px;
+        text-decoration: none;
+    }
+</style>

BIN
web/frps/src/assets/favicon.ico


+ 140 - 0
web/frps/src/components/Overview.vue

@@ -0,0 +1,140 @@
+<template>
+    <div>
+        <el-row>
+            <el-col :md="12">
+                <div class="source">
+                    <el-form label-position="left" class="server_info">
+                        <el-form-item label="Http Port">
+                          <span>{{ vhost_http_port }}</span>
+                        </el-form-item>
+                        <el-form-item label="Https Port">
+                          <span>{{ vhost_https_port }}</span>
+                        </el-form-item>
+                        <el-form-item label="Auth Timeout">
+                          <span>{{ auth_timeout }}</span>
+                        </el-form-item>
+                        <el-form-item label="Subdomain Host">
+                          <span>{{ subdomain_host }}</span>
+                        </el-form-item>
+                        <el-form-item label="Max PoolCount">
+                          <span>{{ max_pool_count }}</span>
+                        </el-form-item>
+                        <el-form-item label="HeartBeat Timeout">
+                          <span>{{ heart_beat_timeout }}</span>
+                        </el-form-item>
+                        <el-form-item label="Client Counts">
+                          <span>{{ client_counts }}</span>
+                        </el-form-item>
+                        <el-form-item label="Current Conns">
+                          <span>{{ cur_conns }}</span>
+                        </el-form-item>
+                        <el-form-item label="Proxy Counts">
+                          <span>{{ proxy_counts }}</span>
+                        </el-form-item>
+                    </el-form>
+                </div>
+            </el-col>
+            <el-col :md="12">
+                <div id="traffic" style="width: 400px;height:250px;margin-bottom: 30px;"></div>
+                <div id="proxies" style="width: 400px;height:250px;"></div>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script>
+    import {DrawTrafficChart, DrawProxyChart} from "../utils/chart.js"
+    
+    export default {
+        data() {
+            return {
+                vhost_http_port: "",
+                vhost_https_port: "",
+                auth_timeout: "",
+                subdomain_host: "",
+                max_pool_count: "",
+                heart_beat_timeout: "",
+                client_counts: "",
+                cur_conns: "",
+                proxy_counts: ""
+            }
+        },
+        created() {
+            this.fetchData()
+        },
+        watch: {
+            '$route': 'fetchData'
+        },
+        methods: {
+            fetchData() {
+                fetch('/api/serverinfo')
+              .then(res => {
+                return res.json()
+              }).then(json => {
+                this.vhost_http_port = json.vhost_http_port
+                if (this.vhost_http_port == 0) {
+                    this.vhost_http_port = "disable"
+                }
+                this.vhost_https_port = json.vhost_https_port
+                if (this.vhost_https_port == 0) {
+                    this.vhost_https_port = "disable"
+                }
+                this.auth_timeout = json.auth_timeout
+                this.subdomain_host = json.subdomain_host
+                this.max_pool_count = json.max_pool_count
+                this.heart_beat_timeout = json.heart_beat_timeout
+                this.client_counts = json.client_counts
+                this.cur_conns = json.cur_conns
+                this.proxy_counts = 0
+                if (json.proxy_type_count != null) {
+                    if (json.proxy_type_count.tcp != null) {
+                        this.proxy_counts += json.proxy_type_count.tcp
+                    }
+                    if (json.proxy_type_count.udp != null) {
+                        this.proxy_counts += json.proxy_type_count.udp
+                    }
+                    if (json.proxy_type_count.http != null) {
+                        this.proxy_counts += json.proxy_type_count.http
+                    }
+                    if (json.proxy_type_count.https != null) {
+                        this.proxy_counts += json.proxy_type_count.https
+                    }
+                }
+                DrawTrafficChart('traffic', json.total_traffic_in, json.total_traffic_out)
+                DrawProxyChart('proxies', json)
+              }).catch( err => {
+                  this.$message({
+                      showClose: true,
+                      message: 'Get server info from frps failed!',
+                      type: 'warning'
+                    })
+              })
+            }
+        }
+    }
+</script>
+
+<style>
+.source {
+    border: 1px solid #eaeefb;
+    border-radius: 4px;
+    transition: .2s;
+    padding: 24px;
+}
+
+.server_info {
+    margin-left: 40px;
+    font-size: 0px;
+}
+
+.server_info label {
+    width: 150px;
+    color: #99a9bf;
+}
+
+.server_info .el-form-item {
+    margin-right: 0;
+    margin-bottom: 0;
+    width: 100%;
+}
+</style>

+ 142 - 0
web/frps/src/components/ProxiesHttp.vue

@@ -0,0 +1,142 @@
+<template>
+  <div>
+    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
+      <el-table-column type="expand">
+        <template scope="props">
+          <el-popover
+            ref="popover4"
+            placement="right"
+            width="600"
+  		  style="margin-left:0px"
+            trigger="click">
+            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
+          </el-popover>
+  
+          <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button>
+  
+          <el-form label-position="left" inline class="demo-table-expand">
+            <el-form-item label="Name">
+              <span>{{ props.row.name }}</span>
+            </el-form-item>
+            <el-form-item label="Type">
+              <span>{{ props.row.type }}</span>
+            </el-form-item>
+            <el-form-item label="Domains">
+              <span>{{ props.row.custom_domains }}</span>
+            </el-form-item>
+            <el-form-item label="SubDomain">
+              <span>{{ props.row.subdomain }}</span>
+            </el-form-item>
+            <el-form-item label="locations">
+              <span>{{ props.row.locations }}</span>
+            </el-form-item>
+            <el-form-item label="HostRewrite">
+              <span>{{ props.row.host_header_rewrite }}</span>
+            </el-form-item>
+            <el-form-item label="Encryption">
+              <span>{{ props.row.encryption }}</span>
+            </el-form-item>
+            <el-form-item label="Compression">
+              <span>{{ props.row.compression }}</span>
+            </el-form-item>
+        </el-form>
+    </template>
+    </el-table-column>
+    <el-table-column
+      label="Name"
+      prop="name"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Port"
+      prop="port"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Connections"
+      prop="conns"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic In"
+      prop="traffic_in"
+      :formatter="formatTrafficIn"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic Out"
+      prop="traffic_out"
+      :formatter="formatTrafficOut"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="status"
+      prop="status"
+      sortable>
+      <template scope="scope">
+        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
+        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
+      </template>
+    </el-table-column>
+</el-table>
+</div>
+</template>
+
+<script>
+  import Humanize from "humanize-plus";
+  import Traffic from './Traffic.vue'
+  import {
+    HttpProxy
+  } from "../utils/proxy.js"
+  export default {
+    data() {
+      return {
+        proxies: null,
+        vhost_http_port: "",
+        subdomain_host: ""
+      }
+    },
+    created() {
+      this.fetchData()
+    },
+    watch: {
+      '$route': 'fetchData'
+    },
+    methods: {
+      formatTrafficIn(row, column) {
+        return Humanize.fileSize(row.traffic_in)
+      },
+      formatTrafficOut(row, column) {
+        return Humanize.fileSize(row.traffic_out)
+      },
+      fetchData() {
+        fetch('/api/serverinfo')
+          .then(res => {
+            return res.json()
+          }).then(json => {
+            this.vhost_http_port = json.vhost_http_port
+            this.subdomain_host = json.subdomain_host
+            if (this.vhost_http_port == null || this.vhost_http_port == 0) {
+              return
+            } else {
+              fetch('/api/proxy/http')
+                .then(res => {
+                  return res.json()
+                }).then(json => {
+                  this.proxies = new Array()
+                  for (let proxyStats of json.proxies) {
+                    this.proxies.push(new HttpProxy(proxyStats, this.vhost_http_port, this.subdomain_host))
+                  }
+                })
+            }
+          })
+      }
+    },
+    components: {
+        'my-traffic-chart': Traffic
+    }
+  }
+</script>
+
+<style>
+</style>

+ 137 - 0
web/frps/src/components/ProxiesHttps.vue

@@ -0,0 +1,137 @@
+<template>
+  <div>
+    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
+      <el-table-column type="expand">
+        <template scope="props">
+          <el-popover
+            ref="popover4"
+            placement="right"
+            width="600"
+  		  style="margin-left:0px"
+            trigger="click">
+            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
+          </el-popover>
+  
+          <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button>
+  
+          <el-form label-position="left" inline class="demo-table-expand">
+            <el-form-item label="Name">
+              <span>{{ props.row.name }}</span>
+            </el-form-item>
+            <el-form-item label="Type">
+              <span>{{ props.row.type }}</span>
+            </el-form-item>
+            <el-form-item label="Domains">
+              <span>{{ props.row.custom_domains }}</span>
+            </el-form-item>
+            <el-form-item label="SubDomain">
+              <span>{{ props.row.subdomain }}</span>
+            </el-form-item>
+            <el-form-item label="Encryption">
+              <span>{{ props.row.encryption }}</span>
+            </el-form-item>
+            <el-form-item label="Compression">
+              <span>{{ props.row.compression }}</span>
+            </el-form-item>
+        </el-form>
+    </template>
+    </el-table-column>
+    <el-table-column
+      label="Name"
+      prop="name"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Port"
+      prop="port"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Connections"
+      prop="conns"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic In"
+      prop="traffic_in"
+      :formatter="formatTrafficIn"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic Out"
+      prop="traffic_out"
+      :formatter="formatTrafficOut"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="status"
+      prop="status"
+      sortable>
+      <template scope="scope">
+        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
+        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
+      </template>
+    </el-table-column>
+</el-table>
+
+</div>
+</template>
+
+<script>
+  import Humanize from "humanize-plus";
+  import Traffic from './Traffic.vue'
+  import {
+    HttpsProxy
+  } from "../utils/proxy.js"
+  export default {
+    data() {
+      return {
+        proxies: null,
+        vhost_https_port: "",
+        subdomain_host: ""
+      }
+    },
+    created() {
+      this.fetchData()
+    },
+    watch: {
+      '$route': 'fetchData'
+    },
+    methods: {
+      formatTrafficIn(row, column) {
+        return Humanize.fileSize(row.traffic_in)
+      },
+      formatTrafficOut(row, column) {
+        return Humanize.fileSize(row.traffic_out)
+      },
+      fetchData() {
+        fetch('/api/serverinfo')
+          .then(res => {
+            return res.json()
+          }).then(json => {
+            this.vhost_https_port = json.vhost_https_port
+            this.subdomain_host = json.subdomain_host
+            if (this.vhost_https_port == null || this.vhost_https_port == 0) {
+              return
+            } else {
+              fetch('/api/proxy/https')
+                .then(res => {
+                  return res.json()
+                }).then(json => {
+                  this.proxies = new Array()
+                  for (let proxyStats of json.proxies) {
+                    this.proxies.push(new HttpsProxy(proxyStats, this.vhost_https_port, this.subdomain_host))
+                  }
+                })
+            }
+          })
+      }
+    },
+    components: {
+        'my-traffic-chart': Traffic
+    }
+  }
+</script>
+
+<style>
+</style>

+ 118 - 0
web/frps/src/components/ProxiesTcp.vue

@@ -0,0 +1,118 @@
+<template>
+  <div>
+    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
+      <el-table-column type="expand">
+        <template scope="props">
+          <el-popover
+            ref="popover4"
+            placement="right"
+            width="600"
+  		  style="margin-left:0px"
+            trigger="click">
+            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
+          </el-popover>
+  
+          <el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button>
+  
+          <el-form label-position="left" inline class="demo-table-expand">
+            <el-form-item label="Name">
+              <span>{{ props.row.name }}</span>
+            </el-form-item>
+            <el-form-item label="Type">
+              <span>{{ props.row.type }}</span>
+            </el-form-item>
+            <el-form-item label="Addr">
+              <span>{{ props.row.addr }}</span>
+            </el-form-item>
+            <el-form-item label="Encryption">
+              <span>{{ props.row.encryption }}</span>
+            </el-form-item>
+            <el-form-item label="Compression">
+              <span>{{ props.row.compression }}</span>
+            </el-form-item>
+        </el-form>
+        </template>
+    </el-table-column>
+    <el-table-column
+      label="Name"
+      prop="name"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Port"
+      prop="port"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Connections"
+      prop="conns"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic In"
+      prop="traffic_in"
+      :formatter="formatTrafficIn"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic Out"
+      prop="traffic_out"
+      :formatter="formatTrafficOut"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="status"
+      prop="status"
+      sortable>
+      <template scope="scope">
+        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
+        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
+      </template>
+    </el-table-column>
+  </el-table>
+</div>
+</template>
+
+<script>
+  import Humanize from 'humanize-plus'
+  import Traffic from './Traffic.vue'
+  import { TcpProxy } from '../utils/proxy.js'
+  export default {
+    data() {
+      return {
+        proxies: null
+      }
+    },
+    created() {
+      this.fetchData()
+    },
+    watch: {
+      '$route': 'fetchData'
+    },
+    methods: {
+      formatTrafficIn(row, column) {
+        return Humanize.fileSize(row.traffic_in)
+      },
+      formatTrafficOut(row, column) {
+        return Humanize.fileSize(row.traffic_out)
+      },
+      fetchData() {
+        fetch('/api/proxy/tcp')
+          .then(res => {
+            return res.json()
+          }).then(json => {
+            this.proxies = new Array()
+            for (let proxyStats of json.proxies) {
+              this.proxies.push(new TcpProxy(proxyStats))
+            }
+          })
+      }
+    },
+    components: {
+        'my-traffic-chart': Traffic
+    }
+  }
+</script>
+
+<style>
+</style>

+ 120 - 0
web/frps/src/components/ProxiesUdp.vue

@@ -0,0 +1,120 @@
+<template>
+  <div>
+    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
+      <el-table-column type="expand">
+        <template scope="props">
+          <el-popover
+            ref="popover4"
+            placement="right"
+            width="600"
+  		  style="margin-left:0px"
+            trigger="click">
+            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
+          </el-popover>
+  
+          <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button>
+  
+          <el-form label-position="left" inline class="demo-table-expand">
+            <el-form-item label="Name">
+              <span>{{ props.row.name }}</span>
+            </el-form-item>
+            <el-form-item label="Type">
+              <span>{{ props.row.type }}</span>
+            </el-form-item>
+            <el-form-item label="Addr">
+              <span>{{ props.row.addr }}</span>
+            </el-form-item>
+            <el-form-item label="Encryption">
+              <span>{{ props.row.encryption }}</span>
+            </el-form-item>
+            <el-form-item label="Compression">
+              <span>{{ props.row.compression }}</span>
+            </el-form-item>
+        </el-form>
+    </template>
+    </el-table-column>
+    <el-table-column
+      label="Name"
+      prop="name"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Port"
+      prop="port"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Connections"
+      prop="conns"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic In"
+      prop="traffic_in"
+      :formatter="formatTrafficIn"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="Traffic Out"
+      prop="traffic_out"
+      :formatter="formatTrafficOut"
+      sortable>
+    </el-table-column>
+    <el-table-column
+      label="status"
+      prop="status"
+      sortable>
+      <template scope="scope">
+        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
+        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
+      </template>
+    </el-table-column>
+  </el-table>
+</div>
+</template>
+
+<script>
+  import Humanize from "humanize-plus";
+  import Traffic from './Traffic.vue'
+  import {
+    UdpProxy
+  } from "../utils/proxy.js"
+  export default {
+    data() {
+      return {
+        proxies: null
+      }
+    },
+    created() {
+      this.fetchData()
+    },
+    watch: {
+      '$route': 'fetchData'
+    },
+    methods: {
+      formatTrafficIn(row, column) {
+        return Humanize.fileSize(row.traffic_in)
+      },
+      formatTrafficOut(row, column) {
+        return Humanize.fileSize(row.traffic_out)
+      },
+      fetchData() {
+        fetch('/api/proxy/udp')
+          .then(res => {
+            return res.json()
+          }).then(json => {
+            this.proxies = new Array()
+            for (let proxyStats of json.proxies) {
+              this.proxies.push(new UdpProxy(proxyStats))
+            }
+          })
+      }
+    },
+    components: {
+        'my-traffic-chart': Traffic
+    }
+  }
+</script>
+
+<style>
+</style>

+ 36 - 0
web/frps/src/components/Traffic.vue

@@ -0,0 +1,36 @@
+<template>
+    <div :id="proxy_name" style="width: 600px;height:400px;"></div>
+</template>
+
+<script>
+import {DrawProxyTrafficChart} from '../utils/chart.js'
+export default {
+    props: ['proxy_name'],
+    created() {
+        this.fetchData()
+    },
+    //watch: {
+        //'$route': 'fetchData'
+    //},
+    methods: {
+        fetchData() {
+            let url = '/api/proxy/traffic/' + this.proxy_name
+            fetch(url)
+              .then(res => {
+                return res.json()
+              }).then(json => {
+                DrawProxyTrafficChart(this.proxy_name, json.traffic_in, json.traffic_out)
+              }).catch( err => {
+                  this.$message({
+                      showClose: true,
+                      message: 'Get server info from frps failed!' + err,
+                      type: 'warning'
+                    })
+              })
+        }
+    }
+}
+</script>
+
+<style>
+</style>

+ 15 - 0
web/frps/src/index.html

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <title>frps dashboard</title>
+</head>
+
+<body>
+    <div id="app"></div>
+    <!--<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>-->
+    <!--<script src="//cdn.bootcss.com/echarts/3.4.0/echarts.min.js"></script>-->
+</body>
+
+</html>

+ 17 - 0
web/frps/src/main.js

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-default/index.css'
+import './utils/less/custom.less'
+
+import App from './App.vue'
+import router from './router'
+
+Vue.use(ElementUI)
+Vue.config.productionTip = false
+
+new Vue({
+    el: '#app',
+    router,
+    template: '<App/>',
+    components: { App }
+})

+ 33 - 0
web/frps/src/router/index.js

@@ -0,0 +1,33 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+import Overview from '../components/Overview.vue'
+import ProxiesTcp from '../components/ProxiesTcp.vue'
+import ProxiesUdp from '../components/ProxiesUdp.vue'
+import ProxiesHttp from '../components/ProxiesHttp.vue'
+import ProxiesHttps from '../components/ProxiesHttps.vue'
+
+Vue.use(Router)
+
+export default new Router({
+    routes: [{
+        path: '/',
+        name: 'Overview',
+        component: Overview
+    }, {
+        path: '/proxies/tcp',
+        name: 'ProxiesTcp',
+        component: ProxiesTcp
+    }, {
+        path: '/proxies/udp',
+        name: 'ProxiesUdp',
+        component: ProxiesUdp
+    }, {
+        path: '/proxies/http',
+        name: 'ProxiesHttp',
+        component: ProxiesHttp
+    }, {
+        path: '/proxies/https',
+        name: 'ProxiesHttps',
+        component: ProxiesHttps
+    }]
+})

+ 187 - 0
web/frps/src/utils/chart.js

@@ -0,0 +1,187 @@
+import Humanize from "humanize-plus"
+import echarts from "echarts/lib/echarts"
+
+import "echarts/theme/macarons"
+import "echarts/lib/chart/bar"
+import "echarts/lib/chart/pie"
+import "echarts/lib/component/tooltip"
+import "echarts/lib/component/title"
+
+function DrawTrafficChart(elementId, trafficIn, trafficOut) {
+    let myChart = echarts.init(document.getElementById(elementId), 'macarons');
+    myChart.showLoading()
+
+    let option = {
+        title: {
+            text: 'Network Traffic',
+            subtext: 'today',
+            x: 'center'
+        },
+        tooltip: {
+            trigger: 'item',
+            formatter: function(v) {
+                return Humanize.fileSize(v.data.value) + " (" + v.percent + "%)"
+            }
+        },
+        series: [{
+            type: 'pie',
+            radius: '55%',
+            center: ['50%', '60%'],
+            data: [{
+                value: trafficIn,
+                name: 'Traffic In'
+            }, {
+                value: trafficOut,
+                name: 'Traffic Out'
+            }, ],
+            itemStyle: {
+                emphasis: {
+                    shadowBlur: 10,
+                    shadowOffsetX: 0,
+                    shadowColor: 'rgba(0, 0, 0, 0.5)'
+                }
+            }
+        }]
+    };
+    myChart.setOption(option);
+    myChart.hideLoading()
+}
+
+function DrawProxyChart(elementId, serverInfo) {
+    if (serverInfo.proxy_type_count.tcp == null) {
+        serverInfo.proxy_type_count.tcp = 0
+    }
+    if (serverInfo.proxy_type_count.udp == null) {
+        serverInfo.proxy_type_count.udp = 0
+    }
+    if (serverInfo.proxy_type_count.http == null) {
+        serverInfo.proxy_type_count.http = 0
+    }
+    if (serverInfo.proxy_type_count.https == null) {
+        serverInfo.proxy_type_count.https = 0
+    }
+    let myChart = echarts.init(document.getElementById(elementId), 'macarons')
+    myChart.showLoading()
+
+    let option = {
+        title: {
+            text: 'Proxies',
+            subtext: 'now',
+            x: 'center'
+        },
+        tooltip: {
+            trigger: 'item',
+            formatter: function(v) {
+                return v.data.value
+            }
+        },
+        series: [{
+            type: 'pie',
+            radius: '55%',
+            center: ['50%', '60%'],
+            data: [{
+                value: serverInfo.proxy_type_count.tcp,
+                name: 'TCP'
+            }, {
+                value: serverInfo.proxy_type_count.udp,
+                name: 'UDP'
+            }, {
+                value: serverInfo.proxy_type_count.http,
+                name: 'HTTP'
+            }, {
+                value: serverInfo.proxy_type_count.https,
+                name: 'HTTPS'
+            }],
+            itemStyle: {
+                emphasis: {
+                    shadowBlur: 10,
+                    shadowOffsetX: 0,
+                    shadowColor: 'rgba(0, 0, 0, 0.5)'
+                }
+            }
+        }]
+    };
+    myChart.setOption(option);
+    myChart.hideLoading()
+}
+
+// 7 days
+function DrawProxyTrafficChart(elementId, trafficInArr, trafficOutArr) {
+    let params = {
+        width: '600px',
+        height: '400px'
+    }
+
+    let myChart = echarts.init(document.getElementById(elementId), 'macarons', params);
+    myChart.showLoading()
+
+    trafficInArr = trafficInArr.reverse()
+    trafficOutArr = trafficOutArr.reverse()
+    let now = new Date()
+    now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6)
+    let dates = new Array()
+    for (let i = 0; i < 7; i++) {
+        dates.push(now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate())
+        now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
+    }
+
+    let option = {
+        tooltip: {
+            trigger: 'axis',
+            axisPointer: {
+                type: 'shadow'
+            },
+            formatter: function(data) {
+                let html = ''
+                if (data.length > 0) {
+                    html += data[0].name + '<br/>'
+                }
+                for (let v of data) {
+                    let colorEl = '<span style="display:inline-block;margin-right:5px;' +
+                        'border-radius:10px;width:9px;height:9px;background-color:' + v.color + '"></span>';
+                    html += colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '<br/>'
+                }
+                return html
+            }
+        },
+        legend: {
+            data: ['Traffic In', 'Traffic Out']
+        },
+        grid: {
+            left: '3%',
+            right: '4%',
+            bottom: '3%',
+            containLabel: true
+        },
+        xAxis: [{
+            type: 'category',
+            data: dates
+        }],
+        yAxis: [{
+            type: 'value',
+            axisLabel: {
+                formatter: function(value) {
+                    return Humanize.fileSize(value)
+                }
+            }
+        }],
+        series: [{
+            name: 'Traffic In',
+            type: 'bar',
+            data: trafficInArr
+        }, {
+
+            name: 'Traffic Out',
+            type: 'bar',
+            data: trafficOutArr
+        }]
+    };
+    myChart.setOption(option);
+    myChart.hideLoading()
+}
+
+export {
+    DrawTrafficChart,
+    DrawProxyChart,
+    DrawProxyTrafficChart
+}

+ 22 - 0
web/frps/src/utils/less/custom.less

@@ -0,0 +1,22 @@
+@color: red;
+
+.el-form-item {
+    span {
+        margin-left: 15px;
+    }
+}
+
+.demo-table-expand {
+    font-size: 0;
+
+    label {
+        width: 90px;
+        color: #99a9bf;
+    }
+
+    .el-form-item {
+        margin-right: 0;
+        margin-bottom: 0;
+        width: 50%;
+    }
+}

+ 88 - 0
web/frps/src/utils/proxy.js

@@ -0,0 +1,88 @@
+class BaseProxy {
+    constructor(proxyStats) {
+        this.name = proxyStats.name
+        if (proxyStats.conf != null) {
+            this.encryption = proxyStats.conf.use_encryption
+            this.compression = proxyStats.conf.use_compression
+        } else {
+            this.encryption = ""
+            this.compression = ""
+        }
+        this.conns = proxyStats.cur_conns
+        this.traffic_in = proxyStats.today_traffic_in
+        this.traffic_out = proxyStats.today_traffic_out
+        this.status = proxyStats.status
+    }
+}
+
+class TcpProxy extends BaseProxy {
+    constructor(proxyStats) {
+        super(proxyStats)
+        this.type = "tcp"
+        if (proxyStats.conf != null) {
+            this.addr = proxyStats.conf.bind_addr + ":" + proxyStats.conf.remote_port
+            this.port = proxyStats.conf.remote_port
+        } else {
+            this.addr = ""
+            this.port = ""
+        }
+    }
+}
+
+class UdpProxy extends BaseProxy {
+    constructor(proxyStats) {
+        super(proxyStats)
+        this.type = "udp"
+        if (proxyStats.conf != null) {
+            this.addr = proxyStats.conf.bind_addr + ":" + proxyStats.conf.remote_port
+            this.port = proxyStats.conf.remote_port
+        } else {
+            this.addr = ""
+            this.port = ""
+        }
+    }
+}
+
+class HttpProxy extends BaseProxy {
+    constructor(proxyStats, port, subdomain_host) {
+        super(proxyStats)
+        this.type = "http"
+        this.port = port
+        if (proxyStats.conf != null) {
+            this.custom_domains = proxyStats.conf.custom_domains
+            this.host_header_rewrite = proxyStats.conf.host_header_rewrite
+            this.locations = proxyStats.conf.locations
+            if (proxyStats.conf.sub_domain != "") {
+                this.subdomain = proxyStats.conf.sub_domain + "." + subdomain_host
+            } else {
+                this.subdomain = ""
+            }
+        } else {
+            this.custom_domains = ""
+            this.host_header_rewrite = ""
+            this.subdomain = ""
+            this.locations = ""
+        }
+    }
+}
+
+class HttpsProxy extends BaseProxy {
+    constructor(proxyStats, port, subdomain_host) {
+        super(proxyStats)
+        this.type = "https"
+        this.port = port
+        if (proxyStats.conf != null) {
+            this.custom_domains = proxyStats.conf.custom_domains
+            if (proxyStats.conf.sub_domain != "") {
+                this.subdomain = proxyStats.conf.sub_domain + "." + subdomain_host
+            } else {
+                this.subdomain = ""
+            }
+        } else {
+            this.custom_domains = ""
+            this.subdomain = ""
+        }
+    }
+}
+
+export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy}

+ 2 - 0
web/frps/src/vendor.js

@@ -0,0 +1,2 @@
+import Vue from 'vue'
+import ElementUI from 'element-ui'

+ 93 - 0
web/frps/webpack.config.js

@@ -0,0 +1,93 @@
+const path = require('path')
+var webpack = require('webpack')
+var HtmlWebpackPlugin = require('html-webpack-plugin')
+var url = require('url')
+var publicPath = ''
+
+module.exports = (options = {}) => ({
+    entry: {
+        vendor: './src/vendor',
+        index: './src/main.js'
+    },
+    output: {
+        path: path.resolve(__dirname, 'dist'),
+        filename: options.dev ? '[name].js' : '[name].js?[chunkhash]',
+        chunkFilename: '[id].js?[chunkhash]',
+        publicPath: options.dev ? '/assets/' : publicPath
+    },
+    resolve: {
+        extensions: ['.js', '.vue', '.json'],
+        alias: {
+            'vue$': 'vue/dist/vue.esm.js',
+            '@': path.resolve(__dirname, 'src'),
+        }
+    },
+    module: {
+        rules: [{
+            test: /\.vue$/,
+            use: ['vue-loader']
+        }, {
+            test: /\.js$/,
+            use: ['babel-loader'],
+            exclude: /node_modules/
+        }, {
+            test: /\.html$/,
+            use: [{
+                loader: 'html-loader',
+                options: {
+                    root: path.resolve(__dirname, 'src'),
+                    attrs: ['img:src', 'link:href']
+                }
+            }]
+        }, {
+            test: /\.less$/,
+            loader: 'style-loader!css-loader!postcss-loader!less-loader'
+        }, {
+            test: /\.css$/,
+            use: ['style-loader', 'css-loader', 'postcss-loader']
+        }, {
+            test: /favicon\.png$/,
+            use: [{
+                loader: 'file-loader',
+                options: {
+                    name: '[name].[ext]?[hash]'
+                }
+            }]
+        }, {
+            test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
+            exclude: /favicon\.png$/,
+            use: [{
+                loader: 'url-loader',
+                options: {
+                    limit: 10000
+                }
+            }]
+        }]
+    },
+    plugins: [
+        new webpack.optimize.CommonsChunkPlugin({
+            names: ['vendor', 'manifest']
+        }),
+        new HtmlWebpackPlugin({
+            favicon: 'src/assets/favicon.ico',
+            template: 'src/index.html'
+        })
+    ],
+    devServer: {
+        host: '127.0.0.1',
+        port: 8010,
+        proxy: {
+            '/api/': {
+                target: 'http://127.0.0.1:8080',
+                changeOrigin: true,
+                pathRewrite: {
+                    '^/api': ''
+                }
+            }
+        },
+        historyApiFallback: {
+            index: url.parse(options.dev ? '/assets/' : publicPath).pathname
+        }
+    }//,
+    //devtool: options.dev ? '#eval-source-map' : '#source-map'
+})