start scripting frontend
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 8s
This commit is contained in:
@@ -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'} <b>Chains:</b> {table.chains.length} {' '}
|
||||
@@ -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)}
|
||||
|
||||
458
frontend/src/components/ScriptManager.tsx
Normal file
458
frontend/src/components/ScriptManager.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
import { Card, Tabs } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import PythonEditor from '../components/PythonEditor';
|
||||
import ScriptsManager from '../components/ScriptManager';
|
||||
|
||||
const TwoTabsPage: React.FC = () => {
|
||||
// Editor state: when a saved script is "opened" from the list, we set this
|
||||
const [code, setCode] = useState<string>("print('hello world')\n");
|
||||
// A way for ScriptsManager to open a script into the editor:
|
||||
const editorSetRef = useRef<(code: string) => void>(() => {});
|
||||
|
||||
const tabItems = [
|
||||
return (
|
||||
<Card style={{ width: '100%' }}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: 'Saved Scripts',
|
||||
children: <></>,
|
||||
children: (
|
||||
<ScriptsManager
|
||||
onOpenInEditor={(scriptCode) => {
|
||||
setCode(scriptCode);
|
||||
// switch to editor tab programmatically? keep simple — user can switch
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
@@ -17,15 +32,15 @@ const TwoTabsPage: React.FC = () => {
|
||||
children: (
|
||||
<div>
|
||||
<h3>Python Editor</h3>
|
||||
<PythonEditor value={code} onChange={setCode} />
|
||||
<div style={{ marginBottom: 12, display: 'flex', gap: 8 }}>
|
||||
{/* Buttons could be added here for "Run", "Save", "Upload" etc. */}
|
||||
</div>
|
||||
<PythonEditor value={code} onChange={setCode} height={520} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card style={{ width: '100%' }}>
|
||||
<Tabs defaultActiveKey="1" items={tabItems} />
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user