1. 修改注册探针的服务端地址为手动填写
2. 优化 Linux 系统服务模板 3. 优化首页流量展示方式
This commit is contained in:
1 parent
31619096b8
commit
d40fc1314f
10 files changed
+336
-238
No files matched your search
@@ -57,21 +57,15 @@ func (h *AgentHandler) DownloadAgent(c echo.Context) error {
|
||||
return c.Stream(http.StatusOK, "application/octet-stream", agentFile)
|
||||
}
|
||||
|
||||
// getServerURL 获取服务器地址(支持反向代理)
|
||||
func getServerURL(c echo.Context) string {
|
||||
// 优先读取 X-Forwarded-Proto 和 X-Forwarded-Host
|
||||
scheme := c.Request().Header.Get("X-Forwarded-Proto")
|
||||
host := c.Request().Header.Get("X-Forwarded-Host")
|
||||
|
||||
// 如果没有反向代理头部,使用默认值
|
||||
if scheme == "" {
|
||||
scheme = c.Scheme()
|
||||
}
|
||||
if host == "" {
|
||||
host = c.Request().Host
|
||||
// getServerURL 获取服务器地址(仅从配置读取)
|
||||
func (h *AgentHandler) getServerURL(c echo.Context) string {
|
||||
// 优先读取配置的服务端地址
|
||||
config, err := h.propertyService.GetAgentInstallConfig(c.Request().Context())
|
||||
if err == nil && config.ServerURL != "" {
|
||||
return config.ServerURL
|
||||
}
|
||||
|
||||
return scheme + "://" + host
|
||||
return ""
|
||||
}
|
||||
|
||||
func bashSingleQuote(value string) string {
|
||||
@@ -82,7 +76,7 @@ func bashSingleQuote(value string) string {
|
||||
}
|
||||
|
||||
func (h *AgentHandler) GetServerUrl(c echo.Context) error {
|
||||
serverUrl := getServerURL(c)
|
||||
serverUrl := h.getServerURL(c)
|
||||
return orz.Ok(c, orz.Map{
|
||||
"serverUrl": serverUrl,
|
||||
})
|
||||
@@ -95,8 +89,11 @@ func (h *AgentHandler) GetInstallScript(c echo.Context) error {
|
||||
return orz.NewError(400, "token不能为空")
|
||||
}
|
||||
|
||||
// 使用统一的 getServerURL 函数获取服务器地址(支持反向代理)
|
||||
serverUrl := getServerURL(c)
|
||||
// 使用统一的 getServerURL 函数获取服务器地址(仅从配置读取)
|
||||
serverUrl := strings.TrimSpace(h.getServerURL(c))
|
||||
if serverUrl == "" {
|
||||
return orz.NewError(400, "请先配置服务端地址")
|
||||
}
|
||||
customName := strings.TrimSpace(c.QueryParam("name"))
|
||||
customNameLiteral := bashSingleQuote(customName)
|
||||
|
||||
|
||||
@@ -156,3 +156,8 @@ type AlertNotifications struct {
|
||||
SSHLoginSuccessEnabled bool `json:"sshLoginSuccessEnabled"` // SSH 登录成功通知
|
||||
TamperEventEnabled bool `json:"tamperEventEnabled"` // 防篡改事件通知
|
||||
}
|
||||
|
||||
// AgentInstallConfig 探针安装配置
|
||||
type AgentInstallConfig struct {
|
||||
ServerURL string `json:"serverUrl"` // 服务端地址
|
||||
}
|
||||
@@ -28,6 +28,8 @@ const (
|
||||
PropertyIDAlertConfig = "alert_config"
|
||||
// PropertyIDDNSProviders DNS 服务商配置的固定 ID
|
||||
PropertyIDDNSProviders = "dns_providers"
|
||||
// PropertyIDAgentInstallConfig 探针安装配置的固定 ID
|
||||
PropertyIDAgentInstallConfig = "agent_install_config"
|
||||
)
|
||||
|
||||
var defaultPublicIPv4APIs = []string{
|
||||
@@ -313,6 +315,21 @@ func (s *PropertyService) DeleteDNSProvider(ctx context.Context, providerType st
|
||||
return s.SetDNSProviderConfigs(ctx, newProviders)
|
||||
}
|
||||
|
||||
// GetAgentInstallConfig 获取探针安装配置
|
||||
func (s *PropertyService) GetAgentInstallConfig(ctx context.Context) (*models.AgentInstallConfig, error) {
|
||||
var config models.AgentInstallConfig
|
||||
err := s.GetValue(ctx, PropertyIDAgentInstallConfig, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取探针安装配置失败: %w", err)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SetAgentInstallConfig 设置探针安装配置
|
||||
func (s *PropertyService) SetAgentInstallConfig(ctx context.Context, config models.AgentInstallConfig) error {
|
||||
return s.Set(ctx, PropertyIDAgentInstallConfig, "探针安装配置", config)
|
||||
}
|
||||
|
||||
// defaultPropertyConfig 默认配置项定义
|
||||
type defaultPropertyConfig struct {
|
||||
ID string
|
||||
@@ -393,6 +410,11 @@ func (s *PropertyService) InitializeDefaultConfigs(ctx context.Context) error {
|
||||
Name: "DNS 服务商配置",
|
||||
Value: []models.DNSProviderConfig{}, // 默认为空数组
|
||||
},
|
||||
{
|
||||
ID: PropertyIDAgentInstallConfig,
|
||||
Name: "探针安装配置",
|
||||
Value: models.AgentInstallConfig{ServerURL: ""}, // 默认空字符串,使用自动检测
|
||||
},
|
||||
}
|
||||
|
||||
// 遍历并初始化每个配置
|
||||
|
||||
@@ -111,7 +111,8 @@ type ServiceManager struct {
|
||||
const systemdScript = `[Unit]
|
||||
Description={{.Description}}
|
||||
ConditionFileIsExecutable={{.Path|cmdEscape}}
|
||||
{{range .Dependencies}} {{.}} {{end}}
|
||||
{{range $i, $dep := .Dependencies}}
|
||||
{{$dep}} {{end}}
|
||||
|
||||
[Service]
|
||||
StartLimitInterval=5
|
||||
@@ -122,14 +123,20 @@ ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
|
||||
{{if .UserName}}User={{.UserName}}{{end}}
|
||||
{{if .ReloadSignal}}ExecReload=/bin/kill -{{.ReloadSignal}} "$MAINPID"{{end}}
|
||||
{{if .PIDFile}}PIDFile={{.PIDFile|cmd}}{{end}}
|
||||
{{if .LogOutput}}StandardOutput={{.LogOutput}}
|
||||
StandardError={{.LogOutput}}{{end}}
|
||||
{{if .LogDirectory}}LogsDirectory={{.LogDirectory}}{{end}}
|
||||
Restart=always
|
||||
{{if and .LogOutput .HasOutputFileSupport -}}
|
||||
StandardOutput=file:{{.LogDirectory}}/{{.Name}}.out
|
||||
StandardError=file:{{.LogDirectory}}/{{.Name}}.err
|
||||
{{- end}}
|
||||
{{if gt .LimitNOFILE -1 }}LimitNOFILE={{.LimitNOFILE}}{{end}}
|
||||
{{if .Restart}}Restart={{.Restart}}{{end}}
|
||||
{{if .SuccessExitStatus}}SuccessExitStatus={{.SuccessExitStatus}}{{end}}
|
||||
RestartSec=5
|
||||
KillMode=process
|
||||
EnvironmentFile=-/etc/sysconfig/{{.Name}}
|
||||
|
||||
{{range $k, $v := .EnvVars -}}
|
||||
Environment={{$k}}={{$v}}
|
||||
{{end -}}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
AgentInstallLayout,
|
||||
ApiChooser,
|
||||
ConfigHelper,
|
||||
ServerUrlChecker,
|
||||
ServiceHelper,
|
||||
AGENT_NAME,
|
||||
AGENT_NAME_EXE,
|
||||
@@ -36,14 +35,11 @@ const DEFAULT_OS: OSType = 'linux-amd64';
|
||||
|
||||
const AgentInstallManual = () => {
|
||||
const { message } = App.useApp();
|
||||
const frontendUrl = useMemo(() => window.location.origin, []);
|
||||
const [selectedOS, setSelectedOS] = useState<OSType>(DEFAULT_OS);
|
||||
const {
|
||||
apiKeys,
|
||||
selectedApiKey,
|
||||
setSelectedApiKey,
|
||||
customAgentName,
|
||||
setCustomAgentName,
|
||||
loading,
|
||||
backendServerUrl,
|
||||
apiKeyOptions,
|
||||
@@ -94,7 +90,6 @@ const AgentInstallManual = () => {
|
||||
|
||||
const getManualInstallSteps = (os: OSType): InstallStep[] => {
|
||||
const config = osConfigs[os];
|
||||
const trimmedName = customAgentName.trim();
|
||||
|
||||
if (os.startsWith('windows')) {
|
||||
return [
|
||||
@@ -108,7 +103,7 @@ Invoke-WebRequest -Uri "${backendServerUrl}${config.downloadUrl}?key=${selectedA
|
||||
},
|
||||
{
|
||||
title: '2. 注册探针',
|
||||
command: `.\\${AGENT_NAME_EXE} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"${trimmedName ? ` --name "${trimmedName}"` : ''}`
|
||||
command: `.\\${AGENT_NAME_EXE} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"`
|
||||
},
|
||||
{
|
||||
title: '3. 验证安装',
|
||||
@@ -136,7 +131,7 @@ curl -L "${backendServerUrl}${config.downloadUrl}?key=${selectedApiKey}" -o ${AG
|
||||
},
|
||||
{
|
||||
title: '4. 注册探针',
|
||||
command: `sudo ${AGENT_NAME} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"${trimmedName ? ` --name "${trimmedName}"` : ''}`
|
||||
command: `sudo ${AGENT_NAME} register --endpoint "${backendServerUrl}" --token "${selectedApiKey}"`
|
||||
},
|
||||
{
|
||||
title: '5. 验证安装',
|
||||
@@ -148,15 +143,12 @@ curl -L "${backendServerUrl}${config.downloadUrl}?key=${selectedApiKey}" -o ${AG
|
||||
return (
|
||||
<AgentInstallLayout activeKey="manual">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<ServerUrlChecker backendServerUrl={backendServerUrl} frontendUrl={frontendUrl} />
|
||||
<ApiChooser
|
||||
apiKeys={apiKeys}
|
||||
selectedApiKey={selectedApiKey}
|
||||
apiKeyOptions={apiKeyOptions}
|
||||
loading={loading}
|
||||
customAgentName={customAgentName}
|
||||
onSelectApiKey={setSelectedApiKey}
|
||||
onCustomNameBlur={setCustomAgentName}
|
||||
/>
|
||||
<Tabs
|
||||
activeKey={selectedOS}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Alert, App, Button, Card, Space, Typography } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, App, Button, Card, Input, Select, Space, Spin, Typography } from 'antd';
|
||||
import { CopyIcon } from 'lucide-react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import {
|
||||
AgentInstallLayout,
|
||||
ApiChooser,
|
||||
ConfigHelper,
|
||||
ServerUrlChecker,
|
||||
ServiceHelper,
|
||||
AGENT_NAME,
|
||||
} from './AgentInstallShared';
|
||||
import { useAgentInstallConfig } from './useAgentInstallConfig';
|
||||
import { getAgentInstallConfig, saveAgentInstallConfig } from '@/api/property';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
const AgentInstallOneClick = () => {
|
||||
const { message } = App.useApp();
|
||||
const frontendUrl = useMemo(() => window.location.origin, []);
|
||||
const {
|
||||
apiKeys,
|
||||
selectedApiKey,
|
||||
@@ -28,14 +26,64 @@ const AgentInstallOneClick = () => {
|
||||
apiKeyOptions,
|
||||
} = useAgentInstallConfig();
|
||||
|
||||
const [serverUrl, setServerUrl] = useState<string>('');
|
||||
const [serverUrlError, setServerUrlError] = useState<string>('');
|
||||
const [serverUrlLoading, setServerUrlLoading] = useState(true);
|
||||
const effectiveServerUrl = serverUrl || backendServerUrl;
|
||||
|
||||
// 加载服务端地址配置
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const config = await getAgentInstallConfig();
|
||||
const configuredUrl = (config.serverUrl || '').trim();
|
||||
setServerUrl(configuredUrl);
|
||||
setServerUrlError(configuredUrl ? '' : '请先配置服务端地址');
|
||||
} catch (error) {
|
||||
console.error('加载服务端地址配置失败:', error);
|
||||
} finally {
|
||||
setServerUrlLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void fetchConfig();
|
||||
}, []);
|
||||
|
||||
// 保存服务端地址配置
|
||||
const handleServerUrlBlur = async (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
setServerUrl(trimmed);
|
||||
if (!trimmed) {
|
||||
setServerUrlError('请先配置服务端地址');
|
||||
message.error('请先配置服务端地址');
|
||||
return;
|
||||
}
|
||||
setServerUrlError('');
|
||||
|
||||
try {
|
||||
await saveAgentInstallConfig({ serverUrl: trimmed });
|
||||
message.success('服务端地址已保存');
|
||||
} catch (error) {
|
||||
console.error('保存服务端地址失败:', error);
|
||||
message.error('保存服务端地址失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFillCurrentUrl = () => {
|
||||
const currentUrl = window.location.origin;
|
||||
setServerUrl(currentUrl);
|
||||
setServerUrlError('');
|
||||
void handleServerUrlBlur(currentUrl);
|
||||
};
|
||||
|
||||
const installCommand = useMemo(() => {
|
||||
if (!backendServerUrl || !selectedApiKey) {
|
||||
if (!effectiveServerUrl || !selectedApiKey) {
|
||||
return '';
|
||||
}
|
||||
const trimmedName = customAgentName.trim();
|
||||
const nameParam = trimmedName ? `&name=${encodeURIComponent(trimmedName)}` : '';
|
||||
return `curl -fsSL "${backendServerUrl}/api/agent/install.sh?token=${selectedApiKey}${nameParam}" | sudo bash`;
|
||||
}, [backendServerUrl, selectedApiKey, customAgentName]);
|
||||
return `curl -fsSL "${effectiveServerUrl}/api/agent/install.sh?token=${selectedApiKey}${nameParam}" | sudo bash`;
|
||||
}, [backendServerUrl, selectedApiKey, customAgentName, serverUrl]);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
copy(text);
|
||||
@@ -45,22 +93,97 @@ const AgentInstallOneClick = () => {
|
||||
return (
|
||||
<AgentInstallLayout activeKey="one-click">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<ServerUrlChecker backendServerUrl={backendServerUrl} frontendUrl={frontendUrl} />
|
||||
<ApiChooser
|
||||
apiKeys={apiKeys}
|
||||
selectedApiKey={selectedApiKey}
|
||||
apiKeyOptions={apiKeyOptions}
|
||||
loading={loading}
|
||||
customAgentName={customAgentName}
|
||||
onSelectApiKey={setSelectedApiKey}
|
||||
onCustomNameBlur={setCustomAgentName}
|
||||
/>
|
||||
<Card type="inner" title="配置选项">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<div>
|
||||
<div className="mb-1 text-gray-600 dark:text-slate-400">
|
||||
服务端地址 <span className="text-xs text-gray-400">(必填)</span>
|
||||
</div>
|
||||
<Spin spinning={serverUrlLoading}>
|
||||
<Input
|
||||
key={serverUrl}
|
||||
placeholder="例如: https://monitor.example.com"
|
||||
defaultValue={serverUrl}
|
||||
onBlur={(e) => {
|
||||
void handleServerUrlBlur(e.currentTarget.value);
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</Spin>
|
||||
<div className="mt-2">
|
||||
<Button size="small" onClick={handleFillCurrentUrl}>
|
||||
使用当前访问地址
|
||||
</Button>
|
||||
</div>
|
||||
{serverUrlError ? (
|
||||
<div className="mt-1 text-xs text-red-500">{serverUrlError}</div>
|
||||
) : (
|
||||
<div className="mt-1 text-xs text-gray-400">
|
||||
请配置可访问的服务端地址,否则无法生成安装命令
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-1 text-gray-600 dark:text-slate-400">选择 API Token</div>
|
||||
{apiKeys.length === 0 ? (
|
||||
<Alert
|
||||
message="暂无可用的 API Token"
|
||||
description={
|
||||
<span>
|
||||
请先前往 <a href="/admin/api-keys">API密钥管理</a> 页面生成一个 API Token
|
||||
</span>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
className="w-full"
|
||||
value={selectedApiKey}
|
||||
onChange={setSelectedApiKey}
|
||||
options={apiKeyOptions}
|
||||
loading={loading}
|
||||
placeholder="请选择 API Token"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-1 text-gray-600 dark:text-slate-400">
|
||||
自定义名称 <span className="text-xs text-gray-400">(可选,留空则使用主机名)</span>
|
||||
</div>
|
||||
<Input
|
||||
key={customAgentName}
|
||||
placeholder="请输入自定义名称,例如: my-server-01"
|
||||
defaultValue={customAgentName}
|
||||
onBlur={(e) => {
|
||||
const trimmed = e.currentTarget.value.trim();
|
||||
setCustomAgentName(trimmed);
|
||||
e.currentTarget.value = trimmed;
|
||||
}}
|
||||
className="w-full"
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Alert
|
||||
description="一键安装脚本仅支持 Linux/macOS 系统。"
|
||||
type="info"
|
||||
showIcon
|
||||
className="mt-2"
|
||||
/>
|
||||
{!effectiveServerUrl && (
|
||||
<Alert
|
||||
description="请先配置服务端地址后再生成安装命令。"
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mt-2"
|
||||
/>
|
||||
)}
|
||||
<Card type="inner" title="一键安装">
|
||||
<Paragraph type="secondary" className="mb-3 text-gray-600 dark:text-slate-400">
|
||||
脚本会自动检测系统架构并下载对应版本的探针,然后完成注册和安装。
|
||||
@@ -74,7 +197,7 @@ const AgentInstallOneClick = () => {
|
||||
onClick={() => void copyToClipboard(installCommand)}
|
||||
icon={<CopyIcon className="h-4 w-4" />}
|
||||
style={{ margin: 0, padding: 0 }}
|
||||
disabled={!selectedApiKey}
|
||||
disabled={!selectedApiKey || !effectiveServerUrl}
|
||||
>
|
||||
复制命令
|
||||
</Button>
|
||||
|
||||
@@ -21,9 +21,7 @@ export type ApiChooserProps = {
|
||||
selectedApiKey: string;
|
||||
apiKeyOptions: ApiKeyOption[];
|
||||
loading: boolean;
|
||||
customAgentName: string;
|
||||
onSelectApiKey: (value: string) => void;
|
||||
onCustomNameBlur: (value: string) => void;
|
||||
};
|
||||
|
||||
export const AgentInstallLayout = ({ activeKey, children }: { activeKey: InstallNavKey; children: ReactNode }) => {
|
||||
@@ -64,124 +62,39 @@ export const AgentInstallLayout = ({ activeKey, children }: { activeKey: Install
|
||||
);
|
||||
};
|
||||
|
||||
export const ServerUrlChecker = ({ backendServerUrl, frontendUrl }: { backendServerUrl: string; frontendUrl: string }) => {
|
||||
const hasAddressMismatch = backendServerUrl && backendServerUrl !== frontendUrl;
|
||||
|
||||
if (!hasAddressMismatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
message="检测到地址不一致"
|
||||
description={
|
||||
<Space direction="vertical" className="w-full">
|
||||
<div>
|
||||
当前访问地址: <Text code>{frontendUrl}</Text>
|
||||
<br />
|
||||
后端检测地址: <Text code>{backendServerUrl}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>这通常是因为您使用了反向代理,但未正确配置转发头部。</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text>请在反向代理配置中添加以下头部:</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Nginx 配置示例:</Text>
|
||||
<pre className="m-0 mt-2 overflow-auto text-xs bg-gray-100 dark:bg-slate-900 p-2 rounded">
|
||||
{`location / {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Caddy 配置示例:</Text>
|
||||
<pre className="m-0 mt-2 overflow-auto text-xs bg-gray-100 dark:bg-slate-900 p-2 rounded">
|
||||
{`reverse_proxy backend:8080 {
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up X-Forwarded-Host {host}
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong>Traefik 配置说明:</Text>
|
||||
<pre className="m-0 mt-2 overflow-auto text-xs bg-gray-100 dark:bg-slate-900 p-2 rounded">
|
||||
{`# Traefik 默认会自动添加 X-Forwarded-* 头部
|
||||
# 无需额外配置`}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Text type="secondary">配置完成后,刷新页面即可生效。</Text>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
closable
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ApiChooser = ({
|
||||
apiKeys,
|
||||
selectedApiKey,
|
||||
apiKeyOptions,
|
||||
loading,
|
||||
customAgentName,
|
||||
onSelectApiKey,
|
||||
onCustomNameBlur,
|
||||
}: ApiChooserProps) => (
|
||||
<Card type="inner" title="配置选项">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<div>
|
||||
<div className="mb-1 text-gray-600 dark:text-slate-400">选择 API Token</div>
|
||||
{apiKeys.length === 0 ? (
|
||||
<Alert
|
||||
message="暂无可用的 API Token"
|
||||
description={
|
||||
<span>
|
||||
请先前往 <a href="/admin/api-keys">API密钥管理</a> 页面生成一个 API Token
|
||||
</span>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
className="w-full"
|
||||
value={selectedApiKey}
|
||||
onChange={onSelectApiKey}
|
||||
options={apiKeyOptions}
|
||||
loading={loading}
|
||||
placeholder="请选择 API Token"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-1 text-gray-600 dark:text-slate-400">
|
||||
自定义名称 <span className="text-xs text-gray-400">(可选,留空则使用主机名)</span>
|
||||
</div>
|
||||
<Input
|
||||
key={customAgentName}
|
||||
placeholder="请输入自定义名称,例如: my-server-01"
|
||||
defaultValue={customAgentName}
|
||||
onBlur={(e) => {
|
||||
const trimmed = e.currentTarget.value.trim();
|
||||
onCustomNameBlur(trimmed);
|
||||
e.currentTarget.value = trimmed;
|
||||
}}
|
||||
className="w-full"
|
||||
allowClear
|
||||
<div>
|
||||
<div className="mb-1 text-gray-600 dark:text-slate-400">选择 API Token</div>
|
||||
{apiKeys.length === 0 ? (
|
||||
<Alert
|
||||
message="暂无可用的 API Token"
|
||||
description={
|
||||
<span>
|
||||
请先前往 <a href="/admin/api-keys">API密钥管理</a> 页面生成一个 API Token
|
||||
</span>
|
||||
}
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
) : (
|
||||
<Select
|
||||
className="w-full"
|
||||
value={selectedApiKey}
|
||||
onChange={onSelectApiKey}
|
||||
options={apiKeyOptions}
|
||||
loading={loading}
|
||||
placeholder="请选择 API Token"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
|
||||
@@ -173,3 +173,22 @@ export const getAlertConfig = async (): Promise<AlertConfig> => {
|
||||
export const saveAlertConfig = async (config: AlertConfig): Promise<void> => {
|
||||
return saveProperty(PROPERTY_ID_ALERT_CONFIG, '告警配置', config);
|
||||
};
|
||||
|
||||
// ==================== 探针安装配置 ====================
|
||||
|
||||
const PROPERTY_ID_AGENT_INSTALL_CONFIG = 'agent_install_config';
|
||||
|
||||
// 探针安装配置
|
||||
export interface AgentInstallConfig {
|
||||
serverUrl: string; // 服务端地址
|
||||
}
|
||||
|
||||
// 获取探针安装配置
|
||||
export const getAgentInstallConfig = async (): Promise<AgentInstallConfig> => {
|
||||
return getProperty<AgentInstallConfig>(PROPERTY_ID_AGENT_INSTALL_CONFIG);
|
||||
};
|
||||
|
||||
// 保存探针安装配置
|
||||
export const saveAgentInstallConfig = async (config: AgentInstallConfig): Promise<void> => {
|
||||
return saveProperty(PROPERTY_ID_AGENT_INSTALL_CONFIG, '探针安装配置', config);
|
||||
};
|
||||
@@ -53,6 +53,13 @@ const getTemperatures = (metrics?: LatestMetrics) => {
|
||||
return metrics.temperature.sort((a, b) => a.type.localeCompare(b.type));
|
||||
};
|
||||
|
||||
const getTrafficProgressColor = (percent: number) => {
|
||||
if (percent >= 100) return 'bg-red-500';
|
||||
if (percent >= 90) return 'bg-orange-500';
|
||||
if (percent >= 80) return 'bg-yellow-500';
|
||||
return 'bg-emerald-500';
|
||||
};
|
||||
|
||||
const ServerCard: FC<ServerCardProps> = ({server}) => {
|
||||
const isOnline = server.status === 1;
|
||||
const cpuUsage = server.metrics?.cpu?.usagePercent ?? 0;
|
||||
@@ -65,6 +72,10 @@ const ServerCard: FC<ServerCardProps> = ({server}) => {
|
||||
const {upload, download} = calculateNetworkSpeed(server.metrics);
|
||||
const temperatures = getTemperatures(server.metrics);
|
||||
const netConn = server.metrics?.networkConnection;
|
||||
const traffic = server.trafficStats;
|
||||
const trafficUsagePercent = traffic?.enabled && traffic.limit > 0
|
||||
? Math.min(100, (traffic.used / traffic.limit) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Link to={`/servers/${server.id}`}>
|
||||
@@ -210,6 +221,40 @@ const ServerCard: FC<ServerCardProps> = ({server}) => {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{traffic?.enabled && (
|
||||
<div className="pt-2 border-t border-slate-200 dark:border-cyan-900/30 space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
<Activity className="w-3 h-3"/>
|
||||
<span>{traffic.type === 'recv' ? '进站' : traffic.type === 'send' ? '出站' : '全部'}流量</span>
|
||||
</div>
|
||||
{traffic.limit > 0 ? (
|
||||
<>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<span className="text-xs text-gray-600 dark:text-cyan-500 font-mono">
|
||||
{formatBytes(traffic.used, 1)} / {formatBytes(traffic.limit, 1)}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-gray-700 dark:text-cyan-400 font-mono">
|
||||
{trafficUsagePercent.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-200 dark:bg-cyan-900/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${getTrafficProgressColor(trafficUsagePercent)}`}
|
||||
style={{width: `${trafficUsagePercent}%`}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
重置日期: 每月 {traffic.resetDay} 号
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
已使用: {formatBytes(traffic.used, 1)}
|
||||
<div className="text-xs text-gray-400 dark:text-cyan-700 mt-1">仅统计模式</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CyberCard>
|
||||
|
||||
@@ -75,6 +75,13 @@ const getTemperatures = (metrics?: LatestMetrics) => {
|
||||
return metrics.temperature.sort((a, b) => a.type.localeCompare(b.type));
|
||||
};
|
||||
|
||||
const getTrafficProgressColor = (percent: number) => {
|
||||
if (percent >= 100) return 'bg-red-500';
|
||||
if (percent >= 90) return 'bg-orange-500';
|
||||
if (percent >= 80) return 'bg-yellow-500';
|
||||
return 'bg-emerald-500';
|
||||
};
|
||||
|
||||
const ServerList = () => {
|
||||
const navigate = useNavigate();
|
||||
const [selectedTag, setSelectedTag] = useState<string>('');
|
||||
@@ -193,82 +200,6 @@ const ServerList = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 流量统计卡片 */}
|
||||
{agents.filter(a => a.trafficStats?.enabled).length > 0 && (
|
||||
<div className="bg-white/80 dark:bg-[#0a0b10]/90 border border-slate-200 dark:border-cyan-900/50 rounded-xl overflow-hidden shadow-sm dark:shadow-2xl backdrop-blur-md p-4">
|
||||
<div className="flex items-center gap-2 mb-4 text-slate-800 dark:text-cyan-100 font-mono font-bold">
|
||||
<Activity className="w-4 h-4"/>
|
||||
<span className="text-sm uppercase tracking-wider">流量统计</span>
|
||||
<span className="text-xs text-gray-500 dark:text-cyan-500">({agents.filter(a => a.trafficStats?.enabled).length} 台设备已启用)</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{agents.filter(a => a.trafficStats?.enabled).map(agent => {
|
||||
const traffic = agent.trafficStats!;
|
||||
const usagePercent = traffic.limit > 0 ? Math.min(100, (traffic.used / traffic.limit) * 100) : 0;
|
||||
const getProgressColor = (percent: number) => {
|
||||
if (percent >= 100) return 'bg-red-500';
|
||||
if (percent >= 90) return 'bg-orange-500';
|
||||
if (percent >= 80) return 'bg-yellow-500';
|
||||
return 'bg-emerald-500';
|
||||
};
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
onClick={() => handleNavigate(agent.id)}
|
||||
className="bg-white dark:bg-black/40 border border-slate-200 dark:border-cyan-900/30 rounded-lg p-3 hover:border-cyan-500 dark:hover:border-cyan-500 transition-all cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-mono font-bold text-sm text-slate-800 dark:text-cyan-100 truncate group-hover:text-cyan-600 dark:group-hover:text-cyan-400 transition-colors">
|
||||
{agent.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono mt-0.5">
|
||||
{traffic.type === 'recv' ? '进站' : traffic.type === 'send' ? '出站' : '全部'}流量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{traffic.limit > 0 ? (
|
||||
<>
|
||||
<div className="flex items-baseline justify-between mb-1">
|
||||
<span className="text-xs text-gray-600 dark:text-cyan-500 font-mono">
|
||||
{formatBytes(traffic.used)} / {formatBytes(traffic.limit)}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-gray-700 dark:text-cyan-400 font-mono">
|
||||
{usagePercent.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-200 dark:bg-cyan-900/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${getProgressColor(usagePercent)}`}
|
||||
style={{width: `${usagePercent}%`}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono mt-1">
|
||||
重置日期: 每月 {traffic.resetDay} 号
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
已使用: {formatBytes(traffic.used)}
|
||||
<div className="text-xs text-gray-400 dark:text-cyan-700 mt-1">仅统计模式</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签过滤器 */}
|
||||
{allTags.length > 1 && (
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2 items-center">
|
||||
@@ -322,6 +253,7 @@ const ServerList = () => {
|
||||
<th className="p-5 font-bold w-[250px]">Identity</th>
|
||||
<th className="p-5 font-bold">Telemetry</th>
|
||||
<th className="p-5 font-bold w-[220px]">I/O Rate</th>
|
||||
<th className="p-5 font-bold w-[240px]">Traffic</th>
|
||||
<th className="p-5 font-bold w-[150px]">Network</th>
|
||||
<th className="p-5 font-bold w-[200px]">Meta / Tags</th>
|
||||
</tr>
|
||||
@@ -339,6 +271,10 @@ const ServerList = () => {
|
||||
const {upload, download} = calculateNetworkSpeed(server.metrics);
|
||||
const temperatures = getTemperatures(server.metrics);
|
||||
const netConn = server.metrics?.networkConnection;
|
||||
const traffic = server.trafficStats;
|
||||
const trafficUsagePercent = traffic?.enabled && traffic.limit > 0
|
||||
? Math.min(100, (traffic.used / traffic.limit) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<tr
|
||||
@@ -455,6 +391,45 @@ const ServerList = () => {
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Traffic */}
|
||||
<td className="p-4 font-mono text-xs align-top">
|
||||
{traffic?.enabled ? (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
{traffic.type === 'recv' ? '进站' : traffic.type === 'send' ? '出站' : '全部'}流量
|
||||
</div>
|
||||
{traffic.limit > 0 ? (
|
||||
<>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<span className="text-xs text-gray-600 dark:text-cyan-500 font-mono">
|
||||
{formatBytes(traffic.used, 1)} / {formatBytes(traffic.limit, 1)}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-gray-700 dark:text-cyan-400 font-mono">
|
||||
{trafficUsagePercent.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-200 dark:bg-cyan-900/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${getTrafficProgressColor(trafficUsagePercent)}`}
|
||||
style={{width: `${trafficUsagePercent}%`}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
重置日期: 每月 {traffic.resetDay} 号
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 dark:text-cyan-600 font-mono">
|
||||
已使用: {formatBytes(traffic.used, 1)}
|
||||
<div className="text-xs text-gray-400 dark:text-cyan-700 mt-1">仅统计模式</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-600 dark:text-cyan-500">-</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Connections */}
|
||||
<td className="p-4 font-mono text-xs align-top">
|
||||
{isOnline && netConn ? (
|
||||
|
||||
Reference in new issue
Block a user