mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 14:26:36 +08:00
feat: add OperationSetting component and integrate into settings page
- Introduced OperationSetting component for managing agent configurations. - Updated settings page to include a new tab for operation settings. - Implemented functionality to fetch and update agent parameters such as heartbeat interval, sync interval, and auto-update settings. docs: enhance deployment documentation for agent installation - Added detailed instructions for agent installation using a script. - Included examples for using discovery and agent tokens. - Updated sections on global discovery tokens and agent auto-update features. docs: revise design and development guidelines for V3 - Updated design document to reflect the current state and goals for V3. - Clarified development guidelines to focus on operational experience improvements. ci: add GitHub Actions workflow for agent releases - Created a new workflow to automate the release of agent binaries on GitHub. - Configured the workflow to build binaries for multiple platforms and publish them as releases. feat: implement self-update mechanism for agent - Added updater module to handle checking for and applying updates from GitHub releases. - Implemented logic to restart the agent after a successful update. chore: create install script for agent deployment - Developed a bash script to facilitate the installation of the ATSFlare agent. - The script supports automatic configuration and systemd service creation.
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Header,
|
||||
Icon,
|
||||
Label,
|
||||
Message,
|
||||
Segment,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, copy, showError, showSuccess } from '../helpers';
|
||||
|
||||
const OperationSetting = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
AgentHeartbeatInterval: '30000',
|
||||
AgentSyncInterval: '30000',
|
||||
NodeOfflineThreshold: '120000',
|
||||
AgentAutoUpdate: 'false',
|
||||
AgentUpdateRepo: 'Rain-kl/ATSFlare',
|
||||
ServerAddress: '',
|
||||
AgentDiscoveryToken: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key in inputs) {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
});
|
||||
setInputs((prev) => ({ ...prev, ...newInputs }));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getOptions();
|
||||
getDiscoveryToken();
|
||||
}, []);
|
||||
|
||||
const getDiscoveryToken = async () => {
|
||||
try {
|
||||
const res = await API.get('/api/nodes/bootstrap-token');
|
||||
const { success, data } = res.data;
|
||||
if (success && data) {
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
AgentDiscoveryToken: data.discovery_token,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
setLoading(true);
|
||||
const res = await API.put('/api/option', { key, value });
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('设置已保存');
|
||||
setInputs((prev) => ({ ...prev, [key]: value }));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (e, { name, checked }) => {
|
||||
setInputs((prev) => ({ ...prev, [name]: checked ? 'true' : 'false' }));
|
||||
};
|
||||
|
||||
const submitAgentIntervals = async () => {
|
||||
const hb = parseInt(inputs.AgentHeartbeatInterval, 10);
|
||||
const sync = parseInt(inputs.AgentSyncInterval, 10);
|
||||
const offline = parseInt(inputs.NodeOfflineThreshold, 10);
|
||||
if (isNaN(hb) || hb < 5000) {
|
||||
showError('心跳间隔不能小于 5000 毫秒');
|
||||
return;
|
||||
}
|
||||
if (isNaN(sync) || sync < 5000) {
|
||||
showError('同步间隔不能小于 5000 毫秒');
|
||||
return;
|
||||
}
|
||||
if (isNaN(offline) || offline < 10000) {
|
||||
showError('离线阈值不能小于 10000 毫秒');
|
||||
return;
|
||||
}
|
||||
await updateOption('AgentHeartbeatInterval', String(hb));
|
||||
await updateOption('AgentSyncInterval', String(sync));
|
||||
await updateOption('NodeOfflineThreshold', String(offline));
|
||||
};
|
||||
|
||||
const submitAutoUpdate = async () => {
|
||||
await updateOption('AgentAutoUpdate', inputs.AgentAutoUpdate);
|
||||
await updateOption('AgentUpdateRepo', inputs.AgentUpdateRepo);
|
||||
};
|
||||
|
||||
const rotateDiscoveryToken = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/nodes/bootstrap-token/rotate');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
showSuccess('Discovery Token 已重新生成');
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
AgentDiscoveryToken: data.discovery_token,
|
||||
}));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (e) {
|
||||
showError('操作失败');
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const formatMs = (ms) => {
|
||||
const val = parseInt(ms, 10);
|
||||
if (isNaN(val)) return ms;
|
||||
if (val >= 60000) return `${val / 60000} 分钟`;
|
||||
return `${val / 1000} 秒`;
|
||||
};
|
||||
|
||||
const serverAddr = inputs.ServerAddress || window.location.origin;
|
||||
const curlCommand = inputs.AgentDiscoveryToken
|
||||
? `curl -fsSL https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/install-agent.sh | bash -s -- --server-url ${serverAddr} --discovery-token ${inputs.AgentDiscoveryToken}`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>Agent 运行参数</Header>
|
||||
<Message info size='small'>
|
||||
<p>这些参数通过心跳响应下发到所有 Agent,修改后下次心跳即生效。</p>
|
||||
</Message>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label={`心跳间隔 (${formatMs(inputs.AgentHeartbeatInterval)})`}
|
||||
placeholder='30000'
|
||||
value={inputs.AgentHeartbeatInterval}
|
||||
name='AgentHeartbeatInterval'
|
||||
onChange={handleInputChange}
|
||||
type='number'
|
||||
/>
|
||||
<Form.Input
|
||||
label={`同步间隔 (${formatMs(inputs.AgentSyncInterval)})`}
|
||||
placeholder='30000'
|
||||
value={inputs.AgentSyncInterval}
|
||||
name='AgentSyncInterval'
|
||||
onChange={handleInputChange}
|
||||
type='number'
|
||||
/>
|
||||
<Form.Input
|
||||
label={`离线阈值 (${formatMs(inputs.NodeOfflineThreshold)})`}
|
||||
placeholder='120000'
|
||||
value={inputs.NodeOfflineThreshold}
|
||||
name='NodeOfflineThreshold'
|
||||
onChange={handleInputChange}
|
||||
type='number'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitAgentIntervals} primary>
|
||||
保存运行参数
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>Agent 自动更新</Header>
|
||||
<Form.Group>
|
||||
<Form.Checkbox
|
||||
label='启用 Agent 自动更新'
|
||||
checked={inputs.AgentAutoUpdate === 'true'}
|
||||
name='AgentAutoUpdate'
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='更新仓库'
|
||||
placeholder='Rain-kl/ATSFlare'
|
||||
value={inputs.AgentUpdateRepo}
|
||||
name='AgentUpdateRepo'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={submitAutoUpdate} primary>
|
||||
保存更新设置
|
||||
</Form.Button>
|
||||
|
||||
<Divider />
|
||||
<Header as='h3'>节点接入</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Field>
|
||||
<label>全局 Discovery Token</label>
|
||||
<Segment>
|
||||
<Label basic>{inputs.AgentDiscoveryToken || '(未设置)'}</Label>
|
||||
<Button
|
||||
size='mini'
|
||||
icon
|
||||
style={{ marginLeft: '8px' }}
|
||||
onClick={() =>
|
||||
copy(inputs.AgentDiscoveryToken, 'Discovery Token')
|
||||
}
|
||||
title='复制'
|
||||
>
|
||||
<Icon name='copy' />
|
||||
</Button>
|
||||
<Button
|
||||
size='mini'
|
||||
color='orange'
|
||||
style={{ marginLeft: '4px' }}
|
||||
onClick={rotateDiscoveryToken}
|
||||
>
|
||||
重新生成
|
||||
</Button>
|
||||
</Segment>
|
||||
</Form.Field>
|
||||
</Form.Group>
|
||||
|
||||
{curlCommand && (
|
||||
<>
|
||||
<Header as='h4'>Agent 一键部署命令</Header>
|
||||
<Message>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: 'JetBrains Mono, Consolas, monospace',
|
||||
fontSize: '12px',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{curlCommand}
|
||||
</pre>
|
||||
</Message>
|
||||
<Button
|
||||
size='small'
|
||||
icon
|
||||
labelPosition='left'
|
||||
onClick={() => copy(curlCommand, '部署命令')}
|
||||
>
|
||||
<Icon name='copy' />
|
||||
复制部署命令
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default OperationSetting;
|
||||
Reference in New Issue
Block a user