mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 22:46:38 +08:00
feat(nodes): 调整节点详情 Tab 并新增边缘部署命令
将数据看板并入概览,运行状态与配置合并为状态与部署; 边缘节点支持自动填充 Server URL 与 Agent Token 的 Docker 部署卡片。
This commit is contained in:
@@ -11,11 +11,12 @@ sidebar: false
|
||||
## 重大变更
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
>
|
||||
> 3.1.2 版本更新了 CLickHouse 部署配置。
|
||||
>
|
||||
>
|
||||
> 3.0.0 版本为 Wavelet 平台迁移与架构重构版本,涉及数据库表结构、环境变量以及前后端底层架构的重大变更。请务必在升级前备份数据库,并且更新到 V2.3.4。
|
||||
> 目前已知的兼容性问题:
|
||||
>
|
||||
> - Pages 无法迁移, 升级前请先手动下载并备份 Pages 静态站点的 ZIP 包,升级后重新创建。
|
||||
> - 性能调优参数重置, 升级后请重新配置
|
||||
|
||||
@@ -28,6 +29,7 @@ sidebar: false
|
||||
|
||||
### 改进
|
||||
|
||||
- 节点详情页 Tab 调整为「概览」与「状态与部署」:原数据看板并入概览;运行状态与配置信息并入状态与部署;边缘节点新增可自动填充 Server URL 与 Agent Token 的 Docker 部署命令卡片。
|
||||
- WAF 规则编辑器支持为节点自定义显示名称,并从节点库拖放到画布指定位置添加节点。
|
||||
- WAF 规则画布支持右键删除节点或连线,并屏蔽浏览器默认右键菜单。
|
||||
- WAF 规则编辑器支持一键格式化布局,按流程层次自动整理节点位置。
|
||||
@@ -90,7 +92,6 @@ sidebar: false
|
||||
|
||||
- 移除了 WAF 旧版固定名单与人机验证配置;升级后请在发布前使用新的可视化规则重新编排防护策略。
|
||||
|
||||
|
||||
## [v3.2.0] - 2026-07-12
|
||||
|
||||
### 新增
|
||||
@@ -177,7 +178,6 @@ sidebar: false
|
||||
- 网站管理拆分为域名、路由、静态托管、WAF 和缓存等独立能力,更适合维护复杂站点配置。
|
||||
- Tunnel 节点统一纳入节点管理,配置发布和运行状态查看更加一致。
|
||||
|
||||
|
||||
## [v2.3.4] - 2026-06-17
|
||||
|
||||
### 变更
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
import { NodeService } from '@/lib/services/openflare';
|
||||
|
||||
import { AgentUpdateDialog } from './agent-update-dialog';
|
||||
import { InstallCommand } from './install-command';
|
||||
import { NodeDetailShell } from './node-detail-shell';
|
||||
import {
|
||||
NodeErrorBanner,
|
||||
@@ -199,7 +200,7 @@ export function EdgeNodeDetail({ node }: { node: NodeItem }) {
|
||||
</>
|
||||
);
|
||||
|
||||
const overviewTab = (
|
||||
const manageTab = (
|
||||
<div className='space-y-6'>
|
||||
{node.last_error ? <NodeErrorBanner message={node.last_error} /> : null}
|
||||
|
||||
@@ -277,11 +278,9 @@ export function EdgeNodeDetail({ node }: { node: NodeItem }) {
|
||||
</div>
|
||||
</NodeSectionCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const manageTab = (
|
||||
<div className='space-y-6'>
|
||||
<InstallCommand node={node} variant='edge' />
|
||||
|
||||
<NodeSectionCard
|
||||
title='节点标识'
|
||||
description='用于 Agent 接入与鉴别的核心字段'
|
||||
@@ -361,8 +360,7 @@ export function EdgeNodeDetail({ node }: { node: NodeItem }) {
|
||||
icon: Activity,
|
||||
},
|
||||
]}
|
||||
overview={overviewTab}
|
||||
dashboard={
|
||||
overview={
|
||||
<NodeObservability
|
||||
nodeId={node.id}
|
||||
node={node}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { type NodeItem, StatusService } from '@/lib/services/openflare';
|
||||
|
||||
import {
|
||||
buildEdgeDockerInstallCommand,
|
||||
buildRelayDockerInstallCommand,
|
||||
buildRelayInstallCommand,
|
||||
buildTunnelDockerInstallCommand,
|
||||
@@ -25,7 +26,7 @@ import {
|
||||
getServerUrl,
|
||||
} from './node-utils';
|
||||
|
||||
type InstallVariant = 'relay' | 'tunnel';
|
||||
type InstallVariant = 'edge' | 'relay' | 'tunnel';
|
||||
|
||||
const variantMeta: Record<
|
||||
InstallVariant,
|
||||
@@ -33,10 +34,16 @@ const variantMeta: Record<
|
||||
title: string;
|
||||
description: string;
|
||||
tokenLabel: string;
|
||||
scriptLabel: string;
|
||||
scriptLabel?: string;
|
||||
dockerLabel: string;
|
||||
}
|
||||
> = {
|
||||
edge: {
|
||||
title: '边缘节点部署',
|
||||
description: '使用 Agent Token 将边缘节点接入控制端。',
|
||||
tokenLabel: 'Agent Token',
|
||||
dockerLabel: 'Docker 容器部署',
|
||||
},
|
||||
relay: {
|
||||
title: '中继部署命令',
|
||||
description: '使用 Discovery Token 将 frps 中继节点接入控制端。',
|
||||
@@ -79,7 +86,7 @@ export function InstallCommand({
|
||||
|
||||
const normalizedServerUrl = getServerUrl(serverUrl);
|
||||
const scriptCommand = useMemo(() => {
|
||||
if (!normalizedServerUrl || !node.access_token) {
|
||||
if (!normalizedServerUrl || !node.access_token || variant === 'edge') {
|
||||
return '';
|
||||
}
|
||||
return variant === 'relay'
|
||||
@@ -91,6 +98,13 @@ export function InstallCommand({
|
||||
if (!normalizedServerUrl || !node.access_token) {
|
||||
return '';
|
||||
}
|
||||
if (variant === 'edge') {
|
||||
return buildEdgeDockerInstallCommand(
|
||||
normalizedServerUrl,
|
||||
node.access_token,
|
||||
serverVersion,
|
||||
);
|
||||
}
|
||||
return variant === 'relay'
|
||||
? buildRelayDockerInstallCommand(
|
||||
normalizedServerUrl,
|
||||
@@ -190,12 +204,14 @@ export function InstallCommand({
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className='space-y-2'>
|
||||
<p className='text-sm font-medium'>{meta.scriptLabel}</p>
|
||||
<pre className='overflow-x-auto rounded-lg border bg-muted/40 p-3 text-xs whitespace-pre-wrap'>
|
||||
{scriptCommand}
|
||||
</pre>
|
||||
</div>
|
||||
{scriptCommand && meta.scriptLabel ? (
|
||||
<div className='space-y-2'>
|
||||
<p className='text-sm font-medium'>{meta.scriptLabel}</p>
|
||||
<pre className='overflow-x-auto rounded-lg border bg-muted/40 p-3 text-xs whitespace-pre-wrap'>
|
||||
{scriptCommand}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
<div className='space-y-2'>
|
||||
<p className='text-sm font-medium'>{meta.dockerLabel}</p>
|
||||
<pre className='overflow-x-auto rounded-lg border bg-muted/40 p-3 text-xs whitespace-pre-wrap'>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { NodeKpiCard } from './node-detail-primitives';
|
||||
import { NodeStatusBadge } from './node-status-badge';
|
||||
import type { StatusTone } from './node-utils';
|
||||
|
||||
export type NodeDetailTabId = 'overview' | 'dashboard' | 'manage';
|
||||
export type NodeDetailTabId = 'overview' | 'manage';
|
||||
|
||||
export type NodeDetailTabConfig = {
|
||||
id: NodeDetailTabId;
|
||||
@@ -33,18 +33,20 @@ const TAB_CONFIGS: NodeDetailTabConfig[] = [
|
||||
id: 'overview',
|
||||
label: '概览',
|
||||
},
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: '数据看板',
|
||||
},
|
||||
{
|
||||
id: 'manage',
|
||||
label: '配置与部署',
|
||||
label: '状态与部署',
|
||||
},
|
||||
];
|
||||
|
||||
function isValidTab(value: string | null): value is NodeDetailTabId {
|
||||
return value === 'overview' || value === 'dashboard' || value === 'manage';
|
||||
function resolveTab(value: string | null): NodeDetailTabId | null {
|
||||
if (value === 'overview' || value === 'dashboard') {
|
||||
return 'overview';
|
||||
}
|
||||
if (value === 'manage') {
|
||||
return 'manage';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function NodeDetailShell({
|
||||
@@ -55,7 +57,6 @@ export function NodeDetailShell({
|
||||
actions,
|
||||
kpis,
|
||||
overview,
|
||||
dashboard,
|
||||
manage,
|
||||
defaultTab = 'overview',
|
||||
}: {
|
||||
@@ -66,7 +67,6 @@ export function NodeDetailShell({
|
||||
actions: ReactNode;
|
||||
kpis: NodeDetailKpi[];
|
||||
overview: ReactNode;
|
||||
dashboard: ReactNode;
|
||||
manage: ReactNode;
|
||||
defaultTab?: NodeDetailTabId;
|
||||
}) {
|
||||
@@ -76,7 +76,7 @@ export function NodeDetailShell({
|
||||
|
||||
const activeTab = useMemo(() => {
|
||||
const tab = searchParams.get('tab');
|
||||
return isValidTab(tab) ? tab : defaultTab;
|
||||
return resolveTab(tab) ?? defaultTab;
|
||||
}, [defaultTab, searchParams]);
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
@@ -88,6 +88,16 @@ export function NodeDetailShell({
|
||||
[pathname, router, searchParams],
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(value: string) => {
|
||||
const tab = resolveTab(value);
|
||||
if (tab) {
|
||||
setActiveTab(tab);
|
||||
}
|
||||
},
|
||||
[setActiveTab],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='py-6 px-1 space-y-6'>
|
||||
<section className='overflow-hidden rounded-2xl border bg-gradient-to-br from-card via-card to-muted/30'>
|
||||
@@ -144,11 +154,7 @@ export function NodeDetailShell({
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => {
|
||||
if (isValidTab(value)) {
|
||||
setActiveTab(value);
|
||||
}
|
||||
}}
|
||||
onValueChange={handleTabChange}
|
||||
className='w-full gap-0'
|
||||
>
|
||||
<div className='space-y-3 pb-1'>
|
||||
@@ -174,9 +180,6 @@ export function NodeDetailShell({
|
||||
<TabsContent value='overview' className='mt-6 outline-none'>
|
||||
{activeTab === 'overview' ? overview : null}
|
||||
</TabsContent>
|
||||
<TabsContent value='dashboard' className='mt-6 outline-none'>
|
||||
{activeTab === 'dashboard' ? dashboard : null}
|
||||
</TabsContent>
|
||||
<TabsContent value='manage' className='mt-6 outline-none'>
|
||||
{activeTab === 'manage' ? manage : null}
|
||||
</TabsContent>
|
||||
|
||||
@@ -138,6 +138,25 @@ export function getImageTag(version?: string): string {
|
||||
return 'latest';
|
||||
}
|
||||
|
||||
export function buildEdgeDockerInstallCommand(
|
||||
serverUrl: string,
|
||||
agentToken: string,
|
||||
version?: string,
|
||||
) {
|
||||
const tag = getImageTag(version);
|
||||
const image = `ghcr.io/rain-kl/openflare-agent:${tag}`;
|
||||
|
||||
return [
|
||||
`docker pull ${image}`,
|
||||
`docker rm -f openflare-agent 2>/dev/null || true`,
|
||||
`docker run -d --name openflare-agent --restart unless-stopped \\`,
|
||||
` -p 80:80 -p 443:443/tcp -p 443:443/udp \\`,
|
||||
` -e OPENFLARE_SERVER_URL=${serverUrl} \\`,
|
||||
` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`,
|
||||
` ${image}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildRelayInstallCommand(
|
||||
serverUrl: string,
|
||||
discoveryToken: string,
|
||||
|
||||
@@ -278,7 +278,7 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) {
|
||||
</>
|
||||
);
|
||||
|
||||
const overviewTab = (
|
||||
const manageTab = (
|
||||
<div className='space-y-6'>
|
||||
{node.last_error ? <NodeErrorBanner message={node.last_error} /> : null}
|
||||
|
||||
@@ -359,11 +359,7 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) {
|
||||
</div>
|
||||
</NodeSectionCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const manageTab = (
|
||||
<div className='space-y-6'>
|
||||
<NodeSectionCard
|
||||
title='FRPS WebUI'
|
||||
description='控制 frps 内置 Web 管理界面是否启用及其监听端口'
|
||||
@@ -514,8 +510,7 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) {
|
||||
icon: Activity,
|
||||
},
|
||||
]}
|
||||
overview={overviewTab}
|
||||
dashboard={
|
||||
overview={
|
||||
<NodeObservability
|
||||
nodeId={node.id}
|
||||
variant='compact'
|
||||
|
||||
@@ -183,7 +183,7 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) {
|
||||
</>
|
||||
);
|
||||
|
||||
const overviewTab = (
|
||||
const manageTab = (
|
||||
<div className='space-y-6'>
|
||||
{node.last_error ? <NodeErrorBanner message={node.last_error} /> : null}
|
||||
|
||||
@@ -271,11 +271,9 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) {
|
||||
</div>
|
||||
</NodeSectionCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const manageTab = (
|
||||
<div className='space-y-6'>
|
||||
<InstallCommand node={node} variant='tunnel' />
|
||||
|
||||
<NodeSectionCard title='接入凭证' description='隧道客户端接入所需 Token'>
|
||||
<div className='divide-y'>
|
||||
<NodeInfoRow label='Tunnel Token'>
|
||||
@@ -294,8 +292,6 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) {
|
||||
</NodeInfoRow>
|
||||
</div>
|
||||
</NodeSectionCard>
|
||||
|
||||
<InstallCommand node={node} variant='tunnel' />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -338,8 +334,7 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) {
|
||||
icon: Activity,
|
||||
},
|
||||
]}
|
||||
overview={overviewTab}
|
||||
dashboard={
|
||||
overview={
|
||||
<NodeObservability
|
||||
nodeId={node.id}
|
||||
variant='compact'
|
||||
|
||||
Reference in New Issue
Block a user