mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +08:00
feat(settings): add GeoIP provider selection to settings page
- Introduced a new ResourceSelect component for selecting the GeoIP provider. - Added options for disabling GeoIP, using MaxMind mmdb, and various external GeoIP services. - Implemented saving functionality for the selected GeoIP provider with a corresponding button. - Updated default operation fields to include GeoIPProvider with a default value of 'disabled'.
This commit is contained in:
@@ -9,6 +9,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { EmptyState } from '@/components/feedback/empty-state';
|
import { EmptyState } from '@/components/feedback/empty-state';
|
||||||
import { useTheme } from '@/components/providers/theme-provider';
|
import { useTheme } from '@/components/providers/theme-provider';
|
||||||
|
import worldGeoJson from '@/features/dashboard/data/world-geo.json';
|
||||||
import { StatusBadge } from '@/components/ui/status-badge';
|
import { StatusBadge } from '@/components/ui/status-badge';
|
||||||
import type {
|
import type {
|
||||||
DashboardConfig,
|
DashboardConfig,
|
||||||
@@ -71,6 +72,56 @@ type MapNodeDatum = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WorldGeoJson = {
|
||||||
|
type: string;
|
||||||
|
features?: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let worldMapRegistrationAttempted = false;
|
||||||
|
let worldMapRegistrationSucceeded = false;
|
||||||
|
let worldMapRegistrationError: Error | null = null;
|
||||||
|
|
||||||
|
function ensureWorldMapRegistered() {
|
||||||
|
if (worldMapRegistrationSucceeded || echarts.getMap('world')) {
|
||||||
|
worldMapRegistrationSucceeded = true;
|
||||||
|
worldMapRegistrationError = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worldMapRegistrationAttempted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldMapRegistrationAttempted = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const geoJson = worldGeoJson as WorldGeoJson;
|
||||||
|
|
||||||
|
if (geoJson.type !== 'FeatureCollection' || !Array.isArray(geoJson.features)) {
|
||||||
|
throw new Error('invalid world geojson payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
echarts.registerMap(
|
||||||
|
'world',
|
||||||
|
geoJson as unknown as Parameters<typeof echarts.registerMap>[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!echarts.getMap('world')) {
|
||||||
|
throw new Error('world map registration failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
worldMapRegistrationSucceeded = true;
|
||||||
|
worldMapRegistrationError = null;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
worldMapRegistrationSucceeded = false;
|
||||||
|
worldMapRegistrationError =
|
||||||
|
error instanceof Error ? error : new Error('unknown world map registration error');
|
||||||
|
console.error('Failed to register ECharts world map', worldMapRegistrationError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatPercent(value: number) {
|
function formatPercent(value: number) {
|
||||||
if (!Number.isFinite(value)) {
|
if (!Number.isFinite(value)) {
|
||||||
return '0%';
|
return '0%';
|
||||||
@@ -271,32 +322,9 @@ export function WorldStage({
|
|||||||
const [mapFailed, setMapFailed] = useState(false);
|
const [mapFailed, setMapFailed] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
const ready = ensureWorldMapRegistered();
|
||||||
|
setMapReady(ready);
|
||||||
import('@/features/dashboard/data/world-geo.json')
|
setMapFailed(!ready);
|
||||||
.then(({ default: worldGeoJson }) => {
|
|
||||||
if (!echarts.getMap('world')) {
|
|
||||||
echarts.registerMap('world', worldGeoJson);
|
|
||||||
}
|
|
||||||
if (!echarts.getMap('world')) {
|
|
||||||
throw new Error('world map registration failed');
|
|
||||||
}
|
|
||||||
if (!disposed) {
|
|
||||||
setMapReady(true);
|
|
||||||
setMapFailed(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to register ECharts world map', error);
|
|
||||||
if (!disposed) {
|
|
||||||
setMapReady(false);
|
|
||||||
setMapFailed(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
disposed = true;
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onlineRate =
|
const onlineRate =
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -35,6 +35,7 @@ import {
|
|||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
ResourceField,
|
ResourceField,
|
||||||
ResourceInput,
|
ResourceInput,
|
||||||
|
ResourceSelect,
|
||||||
ResourceTextarea,
|
ResourceTextarea,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
ToggleField,
|
ToggleField,
|
||||||
@@ -71,6 +72,7 @@ const defaultOperationFields = {
|
|||||||
AgentHeartbeatInterval: '10000',
|
AgentHeartbeatInterval: '10000',
|
||||||
NodeOfflineThreshold: '120000',
|
NodeOfflineThreshold: '120000',
|
||||||
AgentUpdateRepo: 'Rain-kl/ATSFlare',
|
AgentUpdateRepo: 'Rain-kl/ATSFlare',
|
||||||
|
GeoIPProvider: 'disabled',
|
||||||
OpenRestyWorkerProcesses: 'auto',
|
OpenRestyWorkerProcesses: 'auto',
|
||||||
OpenRestyWorkerConnections: '4096',
|
OpenRestyWorkerConnections: '4096',
|
||||||
OpenRestyWorkerRlimitNofile: '65535',
|
OpenRestyWorkerRlimitNofile: '65535',
|
||||||
@@ -337,6 +339,7 @@ export function SettingsPage() {
|
|||||||
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '10000',
|
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '10000',
|
||||||
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
|
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
|
||||||
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
|
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
|
||||||
|
GeoIPProvider: optionMap.GeoIPProvider ?? 'disabled',
|
||||||
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
|
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
|
||||||
OpenRestyWorkerConnections:
|
OpenRestyWorkerConnections:
|
||||||
optionMap.OpenRestyWorkerConnections ?? '4096',
|
optionMap.OpenRestyWorkerConnections ?? '4096',
|
||||||
@@ -1040,6 +1043,47 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
<AppCard
|
||||||
|
title="IP 归属方式"
|
||||||
|
description="控制世界地图等场景使用的 IP 归属解析来源。选择 MaxMind 时会按需下载本地 mmdb 数据库。"
|
||||||
|
action={
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
void runBusyAction('operation-geoip', async () => {
|
||||||
|
await saveOptionEntries(
|
||||||
|
[['GeoIPProvider', operationFields.GeoIPProvider]],
|
||||||
|
'IP 归属方式已保存。',
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={busyKey === 'operation-geoip'}
|
||||||
|
>
|
||||||
|
{busyKey === 'operation-geoip' ? '保存中...' : '保存归属方式'}
|
||||||
|
</PrimaryButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ResourceField
|
||||||
|
label="归属方式"
|
||||||
|
hint="disabled 关闭解析;mmdb 使用本地数据库;其余选项调用外部 GeoIP 服务。"
|
||||||
|
>
|
||||||
|
<ResourceSelect
|
||||||
|
value={operationFields.GeoIPProvider}
|
||||||
|
onChange={(event) =>
|
||||||
|
setOperationFields((previous) => ({
|
||||||
|
...previous,
|
||||||
|
GeoIPProvider: event.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="disabled">关闭</option>
|
||||||
|
<option value="mmdb">MaxMind mmdb</option>
|
||||||
|
<option value="ip-api">ip-api.com</option>
|
||||||
|
<option value="geojs">geojs.io</option>
|
||||||
|
<option value="ipinfo">ipinfo.io</option>
|
||||||
|
</ResourceSelect>
|
||||||
|
</ResourceField>
|
||||||
|
</AppCard>
|
||||||
<AppCard
|
<AppCard
|
||||||
title="Discovery Token 与部署命令"
|
title="Discovery Token 与部署命令"
|
||||||
description="适用于新节点首次接入。可直接复制一键安装命令。"
|
description="适用于新节点首次接入。可直接复制一键安装命令。"
|
||||||
|
|||||||
Reference in New Issue
Block a user