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