start scripting frontend
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s

This commit is contained in:
2026-03-01 00:29:05 +01:00
parent 969edeab86
commit 8fe9a06bfa
3 changed files with 496 additions and 22 deletions

View File

@@ -292,7 +292,7 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
{tablesToRender.length === 0 && (
<Alert
type="info"
message="No firewall tables found"
title="No firewall tables found"
description="You can create a new table using the button above."
style={{ marginBottom: 12 }}
/>
@@ -310,7 +310,7 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
<div style={{ display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Title level={5} style={{ margin: 0 }}>
Table "{table.name}"
Table {table.name}
</Title>
<Text type="secondary">
<b>Family:</b> {table.family ?? 'unknown'} &nbsp; <b>Chains:</b> {table.chains.length} &nbsp;{' '}
@@ -405,6 +405,7 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
<Space>
<Button
danger
size="small"
icon={<DeleteOutlined />}
onClick={() => handleDeleteChain(table.family, table.name, chain.name)}

View File

@@ -0,0 +1,458 @@
import {
DeleteOutlined,
DownloadOutlined,
EditOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
PlusOutlined,
UploadOutlined,
} from '@ant-design/icons';
import {
Button,
Col,
Form,
Input,
Modal,
notification,
Popconfirm,
Row,
Space,
Spin,
Table,
Tooltip,
Upload,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useEffect, useState } from 'react';
import {
deleteScript,
disableScript,
downloadScript,
enableScript,
fetchScriptsStatusAll,
listScripts,
uploadScript,
} from '../api/apiClient';
import PythonEditor from '../components/PythonEditor';
import { EnableRequest, ScriptInfo } from '../types/scripting';
type Props = {
onOpenInEditor?: (code: string) => void;
};
export default function ScriptsManager({ onOpenInEditor }: Props) {
const [scripts, setScripts] = useState<ScriptInfo[]>([]);
const [loading, setLoading] = useState(false);
const [uploadModalVisible, setUploadModalVisible] = useState(false);
const [editorModalVisible, setEditorModalVisible] = useState(false);
const [editorValue, setEditorValue] = useState<string>('');
const [currentEditingName, setCurrentEditingName] = useState<string | null>(null);
const [fetchingStatuses, setFetchingStatuses] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
refreshList();
}, []);
async function refreshList() {
setLoading(true);
try {
const data = await listScripts();
setScripts(data || []);
} catch (err: any) {
notification.error({ message: 'Failed to list scripts', description: err?.message || String(err) });
} finally {
setLoading(false);
}
}
async function refreshStatuses() {
setFetchingStatuses(true);
try {
const all = await fetchScriptsStatusAll();
// assume the API returns keyed info matching script names. adapt as needed
// For simplicity, re-fetch list (which may include status), but if you have separate structure, merge.
await refreshList();
} catch (err: any) {
notification.error({ message: 'Failed to fetch statuses', description: err?.message || String(err) });
} finally {
setFetchingStatuses(false);
}
}
async function handleDownload(name: string) {
try {
const res = await downloadScript(name);
// Expecting server returns { filename?: string, content?: string/base64 } or actual text
// If server returns raw script text, just download it as blob
let blob: Blob;
if (typeof res === 'string') {
blob = new Blob([res], { type: 'text/x-python' });
} else if (res && res.content && res.filename) {
// maybe base64
try {
const b = atob(res.content);
const arr = new Uint8Array(b.length);
for (let i = 0; i < b.length; i++) arr[i] = b.charCodeAt(i);
blob = new Blob([arr], { type: 'text/x-python' });
} catch {
blob = new Blob([res.content], { type: 'text/x-python' });
}
} else {
// fallback: JSON
blob = new Blob([JSON.stringify(res, null, 2)], { type: 'application/json' });
}
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name.endsWith('.py') ? name : `${name}.py`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
notification.success({ message: 'Download started' });
} catch (err: any) {
notification.error({ message: 'Download failed', description: err?.message || String(err) });
}
}
async function handleDelete(name: string) {
try {
await deleteScript(name);
notification.success({ message: 'Deleted', description: `${name} removed` });
refreshList();
} catch (err: any) {
notification.error({ message: 'Delete failed', description: err?.message || String(err) });
}
}
function fileToBlob(file: File | Blob) {
return file;
}
async function handleUpload(formValues: any) {
const { name, scriptFile, requirementsFile } = formValues;
if (!name || (!scriptFile && !editorValue && !currentEditingName)) {
notification.warning({ message: 'Please provide a name and script' });
return;
}
const script =
scriptFile?.file ||
(currentEditingName
? new Blob([editorValue], { type: 'text/x-python' })
: new Blob([editorValue], { type: 'text/x-python' }));
const req = requirementsFile?.file || null;
try {
await uploadScript({
name,
script,
requirements: req,
});
notification.success({ message: 'Uploaded', description: `${name} uploaded` });
setUploadModalVisible(false);
form.resetFields();
setCurrentEditingName(null);
refreshList();
} catch (err: any) {
notification.error({ message: 'Upload failed', description: err?.message || String(err) });
}
}
async function openInEditorFromServer(name: string) {
try {
const res = await downloadScript(name);
let text = '';
if (typeof res === 'string') text = res;
else if (res && res.content) {
// maybe base64
try {
text = atob(res.content);
} catch {
text = typeof res.content === 'string' ? res.content : JSON.stringify(res.content);
}
} else {
text = JSON.stringify(res);
}
setEditorValue(text);
setEditorModalVisible(true);
setCurrentEditingName(name);
if (onOpenInEditor) onOpenInEditor(text);
} catch (err: any) {
notification.error({ message: 'Failed to open', description: err?.message || String(err) });
}
}
async function handleSaveFromEditorAs(name: string) {
try {
// create blob from editor and upload
await uploadScript({
name,
script: new Blob([editorValue], { type: 'text/x-python' }),
});
notification.success({ message: 'Saved', description: `${name} uploaded` });
setEditorModalVisible(false);
refreshList();
} catch (err: any) {
notification.error({ message: 'Save failed', description: err?.message || String(err) });
}
}
// enable modal/form
const [enableModalVisible, setEnableModalVisible] = useState(false);
const [enableTarget, setEnableTarget] = useState<string | null>(null);
const [enableForm] = Form.useForm();
async function handleEnableSubmit(values: any) {
if (!enableTarget) return;
const req: EnableRequest = {
qnum: Number(values.qnum),
service_name: values.service_name || null,
extra_args: values.extra_args || null,
enable_at_boot: !!values.enable_at_boot,
};
try {
await enableScript(enableTarget, req);
notification.success({ message: 'Enabled', description: `${enableTarget} enabled` });
setEnableModalVisible(false);
setEnableTarget(null);
enableForm.resetFields();
refreshList();
} catch (err: any) {
notification.error({ message: 'Enable failed', description: err?.message || String(err) });
}
}
async function handleDisable(name: string) {
try {
// ask for qnum? assume qnum required; if you know qnum mapping show UI to input; here we default to 0 if not known
const qnum = 0;
await disableScript(name, qnum);
notification.success({ message: 'Disabled', description: `${name} disabled` });
refreshList();
} catch (err: any) {
notification.error({ message: 'Disable failed', description: err?.message || String(err) });
}
}
const columns: ColumnsType<ScriptInfo> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
render: (text) => <code>{text}</code>,
},
{
title: 'Path',
dataIndex: 'path',
key: 'path',
render: (p) => p || '-',
},
{
title: 'Actions',
key: 'actions',
render: (_, record) => (
<Space>
<Tooltip title="Download">
<Button icon={<DownloadOutlined />} onClick={() => handleDownload(record.name)} />
</Tooltip>
<Tooltip title="Open in editor">
<Button icon={<EditOutlined />} onClick={() => openInEditorFromServer(record.name)} />
</Tooltip>
<Tooltip title="Enable">
<Button
icon={<PlayCircleOutlined />}
onClick={() => {
setEnableTarget(record.name);
setEnableModalVisible(true);
}}
/>
</Tooltip>
<Tooltip title="Disable">
<Button icon={<PauseCircleOutlined />} onClick={() => handleDisable(record.name)} />
</Tooltip>
<Popconfirm title={`Delete ${record.name}?`} onConfirm={() => handleDelete(record.name)}>
<Button danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div>
<Row justify="space-between" style={{ marginBottom: 12 }}>
<Col>
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setUploadModalVisible(true)}>
Upload / New
</Button>
<Button onClick={refreshList}>Refresh</Button>
<Button onClick={refreshStatuses}>{fetchingStatuses ? <Spin size="small" /> : 'Statuses'}</Button>
</Space>
</Col>
</Row>
<Table rowKey="name" columns={columns} dataSource={scripts} loading={loading} pagination={{ pageSize: 10 }} />
{/* Upload Modal */}
<Modal
open={uploadModalVisible}
title="Upload or create script"
onCancel={() => {
setUploadModalVisible(false);
form.resetFields();
}}
footer={null}
>
<Form form={form} layout="vertical" onFinish={handleUpload}>
<Form.Item name="name" label="Script name (include .py if you want)" rules={[{ required: true }]}>
<Input placeholder="my_script.py" />
</Form.Item>
<Form.Item label="Script file (optional if using editor below)">
<Form.Item name="scriptFile" noStyle>
<Upload beforeUpload={() => false} maxCount={1}>
<Button icon={<UploadOutlined />}>Choose script file</Button>
</Upload>
</Form.Item>
</Form.Item>
<Form.Item label="requirements.txt (optional)">
<Form.Item name="requirementsFile" noStyle>
<Upload beforeUpload={() => false} maxCount={1}>
<Button icon={<UploadOutlined />}>Choose requirements</Button>
</Upload>
</Form.Item>
</Form.Item>
<Form.Item label="Or paste/edit script (editor)">
<PythonEditor value={editorValue} onChange={setEditorValue} height={220} />
</Form.Item>
<Form.Item>
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
onClick={() => {
setUploadModalVisible(false);
form.resetFields();
}}
>
Cancel
</Button>
<Button type="primary" htmlType="submit">
Upload
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{/* Editor Modal (open existing script) */}
<Modal
open={editorModalVisible}
title={currentEditingName ? `Editing — ${currentEditingName}` : 'Editor'}
onCancel={() => setEditorModalVisible(false)}
width={900}
footer={
<Space>
<Button onClick={() => setEditorModalVisible(false)}>Close</Button>
<Button
type="default"
onClick={() => {
// Save as new name
Modal.confirm({
title: 'Save as',
content: (
<Form
layout="vertical"
onFinish={(values) => {
handleSaveFromEditorAs(values.name);
Modal.destroyAll();
}}
>
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input placeholder="new_name.py" />
</Form.Item>
<Form.Item>
<Button htmlType="submit" type="primary">
Save
</Button>
</Form.Item>
</Form>
),
icon: null,
okButtonProps: { style: { display: 'none' } },
cancelButtonProps: { style: { display: 'none' } },
});
}}
>
Save as...
</Button>
<Button
type="primary"
onClick={() => {
// overwrite current
if (!currentEditingName) {
notification.warning({ message: 'No filename to overwrite' });
return;
}
handleSaveFromEditorAs(currentEditingName);
}}
>
Save
</Button>
</Space>
}
>
<PythonEditor value={editorValue} onChange={setEditorValue} height={520} />
</Modal>
{/* Enable Modal */}
<Modal
open={enableModalVisible}
title={`Enable ${enableTarget ?? ''}`}
onCancel={() => {
setEnableModalVisible(false);
setEnableTarget(null);
}}
footer={null}
>
<Form form={enableForm} layout="vertical" onFinish={handleEnableSubmit}>
<Form.Item name="qnum" label="Queue number (qnum)" rules={[{ required: true }]}>
<Input placeholder="e.g. 0" type="number" />
</Form.Item>
<Form.Item name="service_name" label="Service name (optional)">
<Input />
</Form.Item>
<Form.Item name="extra_args" label="Extra args (optional)">
<Input />
</Form.Item>
<Form.Item name="enable_at_boot" valuePropName="checked">
<Input type="checkbox" />
</Form.Item>
<Form.Item>
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
onClick={() => {
setEnableModalVisible(false);
enableForm.resetFields();
}}
>
Cancel
</Button>
<Button type="primary" htmlType="submit">
Enable
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
}