web: support to clear offline proxies data on dashboard (#3963)

This commit is contained in:
fatedier 2024-02-01 10:54:57 +08:00 committed by GitHub
parent 8023d147b0
commit b31c67d7c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1999 additions and 2933 deletions

View File

@ -1,3 +1,7 @@
### Deprecation Notices
* Using an underscore in a flag name is deprecated and has been replaced by a hyphen. The underscore format will remain compatible for some time, until it is completely removed in a future version. For example, `--remote_port` is replaced with `--remote-port`.
### Features
* The `Refresh` and `ClearOfflineProxies` buttons have been added to the Dashboard of frps.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,12 @@
<head>
<meta charset="utf-8">
<title>frp client admin UI</title>
<script type="module" crossorigin src="./index-1c7ed8b0.js"></script>
<link rel="stylesheet" href="./index-1e2a7ce0.css">
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,12 @@
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
<script type="module" crossorigin src="./index-c322b7dd.js"></script>
<link rel="stylesheet" href="./index-1e0c7400.css">
<script type="module" crossorigin src="./index-1gecbKzv.js"></script>
<link rel="stylesheet" crossorigin href="./index-Lf6B06jY.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -61,23 +61,23 @@ func (m *serverMetrics) run() {
for {
time.Sleep(12 * time.Hour)
start := time.Now()
count, total := m.clearUselessInfo()
count, total := m.clearUselessInfo(time.Duration(7*24) * time.Hour)
log.Debug("clear useless proxy statistics data count %d/%d, cost %v", count, total, time.Since(start))
}
}()
}
func (m *serverMetrics) clearUselessInfo() (int, int) {
func (m *serverMetrics) clearUselessInfo(continuousOfflineDuration time.Duration) (int, int) {
count := 0
total := 0
// To check if there are proxies that closed than 7 days and drop them.
// To check if there are any proxies that have been closed for more than continuousOfflineDuration and remove them.
m.mu.Lock()
defer m.mu.Unlock()
total = len(m.info.ProxyStatistics)
for name, data := range m.info.ProxyStatistics {
if !data.LastCloseTime.IsZero() &&
data.LastStartTime.Before(data.LastCloseTime) &&
time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour {
time.Since(data.LastCloseTime) > continuousOfflineDuration {
delete(m.info.ProxyStatistics, name)
count++
log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String())
@ -86,6 +86,10 @@ func (m *serverMetrics) clearUselessInfo() (int, int) {
return count, total
}
func (m *serverMetrics) ClearOfflineProxies() (int, int) {
return m.clearUselessInfo(0)
}
func (m *serverMetrics) NewClient() {
m.info.ClientCounts.Inc(1)
}

View File

@ -79,4 +79,5 @@ type Collector interface {
GetProxiesByType(proxyType string) []*ProxyStats
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
GetProxyTraffic(name string) *ProxyTrafficInfo
ClearOfflineProxies() (int, int)
}

View File

@ -17,6 +17,7 @@ package server
import (
"encoding/json"
"net/http"
"sort"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
@ -53,6 +54,7 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
// view
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
@ -226,6 +228,9 @@ func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
proxyInfoResp := GetProxyInfoResp{}
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
sort.Slice(proxyInfoResp.Proxies, func(i, j int) bool {
return proxyInfoResp.Proxies[i].Name < proxyInfoResp.Proxies[j].Name
})
buf, _ := json.Marshal(&proxyInfoResp)
res.Msg = string(buf)
@ -376,3 +381,26 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
buf, _ := json.Marshal(&trafficResp)
res.Msg = string(buf)
}
// DELETE /api/proxies?status=offline
func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("Http request: [%s]", r.URL.Path)
defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
status := r.URL.Query().Get("status")
if status != "offline" {
res.Code = 400
res.Msg = "status only support offline"
return
}
cleared, total := mem.StatsCollector.ClearOfflineProxies()
log.Info("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
}

View File

@ -1,6 +1,7 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {

View File

@ -3,11 +3,9 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
declare module 'vue' {
export interface GlobalComponents {
ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']

View File

@ -1,6 +1,6 @@
{
"name": "-frpc-dashboard",
"version": "0.0.0",
"name": "frpc-dashboard",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite",
@ -11,25 +11,25 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"element-plus": "^2.3.3",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"element-plus": "^2.5.3",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.4",
"@rushstack/eslint-patch": "^1.7.2",
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.21.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.14.3",
"unplugin-vue-components": "^0.24.0",
"vite": "^4.0.0",
"vue-tsc": "^1.0.12"
"prettier": "^3.2.4",
"typescript": "~5.3.3",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.12",
"vue-tsc": "^1.8.27"
}
}

View File

@ -1,8 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@ -1,16 +1,25 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"references": [
{
"path": "./tsconfig.config.json"
}
]
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,8 @@
// Generated by 'unplugin-auto-import'
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {

View File

@ -1,11 +1,11 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCol: typeof import('element-plus/es')['ElCol']
@ -13,6 +13,8 @@ declare module '@vue/runtime-core' {
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRow: typeof import('element-plus/es')['ElRow']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']

View File

@ -12,27 +12,27 @@
},
"dependencies": {
"@types/humanize-plus": "^1.8.0",
"echarts": "^5.4.1",
"element-plus": "^2.3.3",
"echarts": "^5.4.3",
"element-plus": "^2.5.3",
"humanize-plus": "^1.8.2",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.4",
"@rushstack/eslint-patch": "^1.7.2",
"@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.21.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.13.0",
"unplugin-vue-components": "^0.23.0",
"vite": "^4.0.4",
"vue-tsc": "^1.0.12"
"prettier": "^3.2.4",
"typescript": "~5.3.3",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.12",
"vue-tsc": "^1.8.27"
}
}

View File

@ -1,5 +1,5 @@
<template>
<ProxyView :proxies="proxies" proxyType="http" />
<ProxyView :proxies="proxies" proxyType="http" @refresh="fetchData"/>
</template>
<script setup lang="ts">
@ -27,6 +27,7 @@ const fetchData = () => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(
new HTTPProxy(proxyStats, vhostHTTPPort, subdomainHost)

View File

@ -1,5 +1,5 @@
<template>
<ProxyView :proxies="proxies" proxyType="https" />
<ProxyView :proxies="proxies" proxyType="https" @refresh="fetchData"/>
</template>
<script setup lang="ts">
@ -27,6 +27,7 @@ const fetchData = () => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(
new HTTPSProxy(proxyStats, vhostHTTPSPort, subdomainHost)

View File

@ -1,5 +1,5 @@
<template>
<ProxyView :proxies="proxies" proxyType="stcp" />
<ProxyView :proxies="proxies" proxyType="stcp" @refresh="fetchData"/>
</template>
<script setup lang="ts">
@ -15,6 +15,7 @@ const fetchData = () => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(new STCPProxy(proxyStats))
}

View File

@ -1,5 +1,5 @@
<template>
<ProxyView :proxies="proxies" proxyType="sudp" />
<ProxyView :proxies="proxies" proxyType="sudp" @refresh="fetchData"/>
</template>
<script setup lang="ts">
@ -15,6 +15,7 @@ const fetchData = () => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(new SUDPProxy(proxyStats))
}

View File

@ -1,5 +1,5 @@
<template>
<ProxyView :proxies="proxies" proxyType="tcp" />
<ProxyView :proxies="proxies" proxyType="tcp" @refresh="fetchData" />
</template>
<script setup lang="ts">
@ -15,6 +15,7 @@ const fetchData = () => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(new TCPProxy(proxyStats))
}

View File

@ -1,5 +1,5 @@
<template>
<ProxyView :proxies="proxies" proxyType="udp" />
<ProxyView :proxies="proxies" proxyType="udp" @refresh="fetchData"/>
</template>
<script setup lang="ts">
@ -15,12 +15,12 @@ const fetchData = () => {
return res.json()
})
.then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) {
proxies.value.push(new UDPProxy(proxyStats))
}
})
}
fetchData()
</script>

View File

@ -1,5 +1,28 @@
<template>
<div>
<el-page-header
:icon="null"
style="width: 100%; margin-left: 30px; margin-bottom: 20px"
>
<template #title>
<span>{{ proxyType }}</span>
</template>
<template #content> </template>
<template #extra>
<div class="flex items-center" style="margin-right: 30px">
<el-popconfirm
title="Are you sure to clear all data of offline proxies?"
@confirm="clearOfflineProxies"
>
<template #reference>
<el-button>ClearOfflineProxies</el-button>
</template>
</el-popconfirm>
<el-button @click="$emit('refresh')">Refresh</el-button>
</div>
</template>
</el-page-header>
<el-table
:data="proxies"
:default-sort="{ prop: 'name', order: 'ascending' }"
@ -67,6 +90,7 @@
import * as Humanize from 'humanize-plus'
import type { TableColumnCtx } from 'element-plus'
import type { BaseProxy } from '../utils/proxy.js'
import { ElMessage } from 'element-plus'
import ProxyViewExpand from './ProxyViewExpand.vue'
defineProps<{
@ -74,6 +98,8 @@ defineProps<{
proxyType: string
}>()
const emit = defineEmits(['refresh'])
const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
return Humanize.fileSize(row.trafficIn)
}
@ -81,4 +107,37 @@ const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
const formatTrafficOut = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
return Humanize.fileSize(row.trafficOut)
}
const clearOfflineProxies = () => {
fetch('/api/proxies?status=offline', {
method: 'DELETE',
credentials: 'include',
})
.then((res) => {
if (res.ok) {
ElMessage({
message: 'Successfully cleared offline proxies',
type: 'success',
})
emit('refresh')
} else {
ElMessage({
message: 'Failed to clear offline proxies: ' + res.status + ' ' + res.statusText,
type: 'warning',
})
}
})
.catch((err) => {
ElMessage({
message: 'Failed to clear offline proxies: ' + err.message,
type: 'warning',
})
})
}
</script>
<style>
.el-page-header__title {
font-size: 20px;
}
</style>

View File

@ -17,10 +17,7 @@
<el-form-item label="KCP Bind Port" v-if="data.kcpBindPort != 0">
<span>{{ data.kcpBindPort }}</span>
</el-form-item>
<el-form-item
label="QUIC Bind Port"
v-if="data.quicBindPort != 0"
>
<el-form-item label="QUIC Bind Port" v-if="data.quicBindPort != 0">
<span>{{ data.quicBindPort }}</span>
</el-form-item>
<el-form-item label="Http Port" v-if="data.vhostHTTPPort != 0">

View File

@ -23,8 +23,10 @@ class BaseProxy {
this.type = ''
this.encryption = false
this.compression = false
this.encryption = (proxyStats.conf?.transport?.useEncryption) || this.encryption;
this.compression = (proxyStats.conf?.transport?.useCompression) || this.compression;
this.encryption =
proxyStats.conf?.transport?.useEncryption || this.encryption
this.compression =
proxyStats.conf?.transport?.useCompression || this.compression
this.conns = proxyStats.curConns
this.trafficIn = proxyStats.todayTrafficIn
this.trafficOut = proxyStats.todayTrafficOut
@ -76,12 +78,12 @@ class HTTPProxy extends BaseProxy {
this.type = 'http'
this.port = port
if (proxyStats.conf) {
this.customDomains = proxyStats.conf.customDomains || this.customDomains;
this.customDomains = proxyStats.conf.customDomains || this.customDomains
this.hostHeaderRewrite = proxyStats.conf.hostHeaderRewrite
this.locations = proxyStats.conf.locations
if (proxyStats.conf.subdomain) {
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
}
}
}
}
}
@ -92,7 +94,7 @@ class HTTPSProxy extends BaseProxy {
this.type = 'https'
this.port = port
if (proxyStats.conf != null) {
this.customDomains = proxyStats.conf.customDomains || this.customDomains;
this.customDomains = proxyStats.conf.customDomains || this.customDomains
if (proxyStats.conf.subdomain) {
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
}

View File

@ -1,8 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@ -1,16 +1,25 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"references": [
{
"path": "./tsconfig.config.json"
}
]
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because it is too large Load Diff