mirror of
https://github.com/fatedier/frp.git
synced 2026-03-19 00:09:14 +08:00
server: add client registry with dashboard support (#5115)
This commit is contained in:
@@ -1,293 +0,0 @@
|
||||
import * as Humanize from 'humanize-plus'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { PieChart, BarChart } from 'echarts/charts'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LabelLayout } from 'echarts/features'
|
||||
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
} from 'echarts/components'
|
||||
|
||||
echarts.use([
|
||||
PieChart,
|
||||
BarChart,
|
||||
CanvasRenderer,
|
||||
LabelLayout,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
])
|
||||
|
||||
function DrawTrafficChart(
|
||||
elementId: string,
|
||||
trafficIn: number,
|
||||
trafficOut: number
|
||||
) {
|
||||
const myChart = echarts.init(
|
||||
document.getElementById(elementId) as HTMLElement,
|
||||
'macarons'
|
||||
)
|
||||
myChart.showLoading()
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Network Traffic',
|
||||
subtext: 'today',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (v: any) {
|
||||
return Humanize.fileSize(v.data.value) + ' (' + v.percent + '%)'
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: ['Traffic In', 'Traffic Out'],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
center: ['50%', '60%'],
|
||||
data: [
|
||||
{
|
||||
value: trafficIn,
|
||||
name: 'Traffic In',
|
||||
},
|
||||
{
|
||||
value: trafficOut,
|
||||
name: 'Traffic Out',
|
||||
},
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
myChart.setOption(option)
|
||||
myChart.hideLoading()
|
||||
}
|
||||
|
||||
function DrawProxyChart(elementId: string, serverInfo: any) {
|
||||
const myChart = echarts.init(
|
||||
document.getElementById(elementId) as HTMLElement,
|
||||
'macarons'
|
||||
)
|
||||
myChart.showLoading()
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Proxies',
|
||||
subtext: 'now',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function (v: any) {
|
||||
return String(v.data.value)
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
data: <string[]>[],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '55%',
|
||||
center: ['50%', '60%'],
|
||||
data: <any[]>[],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
if (
|
||||
serverInfo.proxyTypeCount.tcp != null &&
|
||||
serverInfo.proxyTypeCount.tcp != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.tcp,
|
||||
name: 'TCP',
|
||||
})
|
||||
option.legend.data.push('TCP')
|
||||
}
|
||||
if (
|
||||
serverInfo.proxyTypeCount.udp != null &&
|
||||
serverInfo.proxyTypeCount.udp != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.udp,
|
||||
name: 'UDP',
|
||||
})
|
||||
option.legend.data.push('UDP')
|
||||
}
|
||||
if (
|
||||
serverInfo.proxyTypeCount.http != null &&
|
||||
serverInfo.proxyTypeCount.http != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.http,
|
||||
name: 'HTTP',
|
||||
})
|
||||
option.legend.data.push('HTTP')
|
||||
}
|
||||
if (
|
||||
serverInfo.proxyTypeCount.https != null &&
|
||||
serverInfo.proxyTypeCount.https != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.https,
|
||||
name: 'HTTPS',
|
||||
})
|
||||
option.legend.data.push('HTTPS')
|
||||
}
|
||||
if (
|
||||
serverInfo.proxyTypeCount.stcp != null &&
|
||||
serverInfo.proxyTypeCount.stcp != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.stcp,
|
||||
name: 'STCP',
|
||||
})
|
||||
option.legend.data.push('STCP')
|
||||
}
|
||||
if (
|
||||
serverInfo.proxyTypeCount.sudp != null &&
|
||||
serverInfo.proxyTypeCount.sudp != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.sudp,
|
||||
name: 'SUDP',
|
||||
})
|
||||
option.legend.data.push('SUDP')
|
||||
}
|
||||
if (
|
||||
serverInfo.proxyTypeCount.xtcp != null &&
|
||||
serverInfo.proxyTypeCount.xtcp != 0
|
||||
) {
|
||||
option.series[0].data.push({
|
||||
value: serverInfo.proxyTypeCount.xtcp,
|
||||
name: 'XTCP',
|
||||
})
|
||||
option.legend.data.push('XTCP')
|
||||
}
|
||||
|
||||
myChart.setOption(option)
|
||||
myChart.hideLoading()
|
||||
}
|
||||
|
||||
// 7 days
|
||||
function DrawProxyTrafficChart(
|
||||
elementId: string,
|
||||
trafficInArr: number[],
|
||||
trafficOutArr: number[]
|
||||
) {
|
||||
const params = {
|
||||
width: '600px',
|
||||
height: '400px',
|
||||
}
|
||||
|
||||
const myChart = echarts.init(
|
||||
document.getElementById(elementId) as HTMLElement,
|
||||
'macarons',
|
||||
params
|
||||
)
|
||||
myChart.showLoading()
|
||||
|
||||
trafficInArr = trafficInArr.reverse()
|
||||
trafficOutArr = trafficOutArr.reverse()
|
||||
let now = new Date()
|
||||
now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6)
|
||||
const dates: Array<string> = []
|
||||
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)
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
formatter: function (data: any) {
|
||||
let html = ''
|
||||
if (data.length > 0) {
|
||||
html += data[0].name + '<br/>'
|
||||
}
|
||||
for (const v of data) {
|
||||
const 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: number) {
|
||||
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 }
|
||||
82
web/frps/src/utils/client.ts
Normal file
82
web/frps/src/utils/client.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { formatDistanceToNow } from './format'
|
||||
import type { ClientInfoData } from '../types/client'
|
||||
|
||||
export class Client {
|
||||
key: string
|
||||
user: string
|
||||
clientId: string
|
||||
runId: string
|
||||
hostname: string
|
||||
metas: Map<string, string>
|
||||
firstConnectedAt: Date
|
||||
lastConnectedAt: Date
|
||||
disconnectedAt?: Date
|
||||
online: boolean
|
||||
|
||||
constructor(data: ClientInfoData) {
|
||||
this.key = data.key
|
||||
this.user = data.user
|
||||
this.clientId = data.clientId
|
||||
this.runId = data.runId
|
||||
this.hostname = data.hostname
|
||||
this.metas = new Map<string, string>()
|
||||
if (data.metas) {
|
||||
for (const [key, value] of Object.entries(data.metas)) {
|
||||
this.metas.set(key, value)
|
||||
}
|
||||
}
|
||||
this.firstConnectedAt = new Date(data.firstConnectedAt * 1000)
|
||||
this.lastConnectedAt = new Date(data.lastConnectedAt * 1000)
|
||||
if (data.disconnectedAt && data.disconnectedAt > 0) {
|
||||
this.disconnectedAt = new Date(data.disconnectedAt * 1000)
|
||||
}
|
||||
this.online = data.online
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
if (this.clientId) {
|
||||
return this.user ? `${this.user}.${this.clientId}` : this.clientId
|
||||
}
|
||||
return this.runId
|
||||
}
|
||||
|
||||
get shortRunId(): string {
|
||||
return this.runId.substring(0, 8)
|
||||
}
|
||||
|
||||
get firstConnectedAgo(): string {
|
||||
return formatDistanceToNow(this.firstConnectedAt)
|
||||
}
|
||||
|
||||
get lastConnectedAgo(): string {
|
||||
return formatDistanceToNow(this.lastConnectedAt)
|
||||
}
|
||||
|
||||
get disconnectedAgo(): string {
|
||||
if (!this.disconnectedAt) return ''
|
||||
return formatDistanceToNow(this.disconnectedAt)
|
||||
}
|
||||
|
||||
get statusColor(): string {
|
||||
return this.online ? 'success' : 'danger'
|
||||
}
|
||||
|
||||
get metasArray(): Array<{ key: string; value: string }> {
|
||||
const arr: Array<{ key: string; value: string }> = []
|
||||
this.metas.forEach((value, key) => {
|
||||
arr.push({ key, value })
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
matchesFilter(searchText: string): boolean {
|
||||
const search = searchText.toLowerCase()
|
||||
return (
|
||||
this.key.toLowerCase().includes(search) ||
|
||||
this.user.toLowerCase().includes(search) ||
|
||||
this.clientId.toLowerCase().includes(search) ||
|
||||
this.runId.toLowerCase().includes(search) ||
|
||||
this.hostname.toLowerCase().includes(search)
|
||||
)
|
||||
}
|
||||
}
|
||||
33
web/frps/src/utils/format.ts
Normal file
33
web/frps/src/utils/format.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export function formatDistanceToNow(date: Date): string {
|
||||
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000)
|
||||
|
||||
let interval = seconds / 31536000
|
||||
if (interval > 1) return Math.floor(interval) + ' years ago'
|
||||
|
||||
interval = seconds / 2592000
|
||||
if (interval > 1) return Math.floor(interval) + ' months ago'
|
||||
|
||||
interval = seconds / 86400
|
||||
if (interval > 1) return Math.floor(interval) + ' days ago'
|
||||
|
||||
interval = seconds / 3600
|
||||
if (interval > 1) return Math.floor(interval) + ' hours ago'
|
||||
|
||||
interval = seconds / 60
|
||||
if (interval > 1) return Math.floor(interval) + ' minutes ago'
|
||||
|
||||
return Math.floor(seconds) + ' seconds ago'
|
||||
}
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '0 B'
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
// Prevent index out of bounds for extremely large numbers
|
||||
const unit = sizes[i] || sizes[sizes.length - 1]
|
||||
const val = bytes / Math.pow(k, i)
|
||||
|
||||
return parseFloat(val.toFixed(2)) + ' ' + unit
|
||||
}
|
||||
@@ -128,7 +128,7 @@ class TCPMuxProxy extends BaseProxy {
|
||||
if (proxyStats.conf.subdomain) {
|
||||
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user