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([]); const [loading, setLoading] = useState(false); const [uploadModalVisible, setUploadModalVisible] = useState(false); const [editorModalVisible, setEditorModalVisible] = useState(false); const [editorValue, setEditorValue] = useState(''); const [currentEditingName, setCurrentEditingName] = useState(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(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 = [ { title: 'Name', dataIndex: 'name', key: 'name', render: (text) => {text}, }, { title: 'Path', dataIndex: 'path', key: 'path', render: (p) => p || '-', }, { title: 'Actions', key: 'actions', render: (_, record) => ( {/* Upload Modal */} { setUploadModalVisible(false); form.resetFields(); }} footer={null} >
false} maxCount={1}> false} maxCount={1}>
{/* Editor Modal (open existing script) */} setEditorModalVisible(false)} width={900} footer={ ), icon: null, okButtonProps: { style: { display: 'none' } }, cancelButtonProps: { style: { display: 'none' } }, }); }} > Save as... } > {/* Enable Modal */} { setEnableModalVisible(false); setEnableTarget(null); }} footer={null} >
); }