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 && (
|
{tablesToRender.length === 0 && (
|
||||||
<Alert
|
<Alert
|
||||||
type="info"
|
type="info"
|
||||||
message="No firewall tables found"
|
title="No firewall tables found"
|
||||||
description="You can create a new table using the button above."
|
description="You can create a new table using the button above."
|
||||||
style={{ marginBottom: 12 }}
|
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 style={{ display: 'flex', width: '100%', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<div>
|
<div>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
Table "{table.name}"
|
Table {table.name}
|
||||||
</Title>
|
</Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
<b>Family:</b> {table.family ?? 'unknown'} <b>Chains:</b> {table.chains.length} {' '}
|
<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>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
|
danger
|
||||||
size="small"
|
size="small"
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => handleDeleteChain(table.family, table.name, chain.name)}
|
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 { Card, Tabs } from 'antd';
|
||||||
import { useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import PythonEditor from '../components/PythonEditor';
|
import PythonEditor from '../components/PythonEditor';
|
||||||
|
import ScriptsManager from '../components/ScriptManager';
|
||||||
|
|
||||||
const TwoTabsPage: React.FC = () => {
|
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");
|
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',
|
key: '1',
|
||||||
label: 'Saved Scripts',
|
label: 'Saved Scripts',
|
||||||
children: <></>,
|
children: (
|
||||||
|
<ScriptsManager
|
||||||
|
onOpenInEditor={(scriptCode) => {
|
||||||
|
setCode(scriptCode);
|
||||||
|
// switch to editor tab programmatically? keep simple — user can switch
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
@@ -17,15 +32,15 @@ const TwoTabsPage: React.FC = () => {
|
|||||||
children: (
|
children: (
|
||||||
<div>
|
<div>
|
||||||
<h3>Python Editor</h3>
|
<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>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
]}
|
||||||
|
/>
|
||||||
return (
|
|
||||||
<Card style={{ width: '100%' }}>
|
|
||||||
<Tabs defaultActiveKey="1" items={tabItems} />
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user