firewall + scripts working
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s
This commit is contained in:
@@ -4,3 +4,19 @@
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice {
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification {
|
||||||
|
left: 70% !important;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-topRight,
|
||||||
|
.ant-notification-topLeft {
|
||||||
|
left: 70% !important;
|
||||||
|
right: auto !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Layout } from 'antd';
|
import { Layout, notification } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@@ -8,6 +8,14 @@ import Sidebar from './Layout/Sidebar';
|
|||||||
|
|
||||||
const { Content, Footer } = Layout;
|
const { Content, Footer } = Layout;
|
||||||
|
|
||||||
|
notification.config({
|
||||||
|
placement: 'topRight', // topLeft | topRight | bottomLeft | bottomRight
|
||||||
|
top: 20, // distance from top
|
||||||
|
bottom: 24, // distance from bottom
|
||||||
|
duration: 3, // auto close time (seconds)
|
||||||
|
maxCount: 3, // max notifications shown
|
||||||
|
});
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Typography,
|
Typography,
|
||||||
message,
|
notification,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ReactElement, useEffect, useState } from 'react';
|
import { ReactElement, useEffect, useState } from 'react';
|
||||||
import { execFirewallRaw, fetchRuleset } from '../api/apiClient';
|
import { execFirewallRaw, fetchRuleset } from '../api/apiClient';
|
||||||
import { CmdResult, ExecResult } from '../types/firewall';
|
import type { CmdResult, ExecResult } from '../types/firewall';
|
||||||
|
|
||||||
const { Paragraph, Text } = Typography;
|
const { Paragraph, Text } = Typography;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -30,7 +30,12 @@ type TableProp = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
/**
|
||||||
|
* onClose may be called with (created?: boolean).
|
||||||
|
* If created === true the parent can decide to refresh and optionally show notifications.
|
||||||
|
* If undefined or false, it's a plain close.
|
||||||
|
*/
|
||||||
|
onClose?: (created?: boolean) => void;
|
||||||
table?: TableProp; // if provided, family & tableName are prefilled & readonly
|
table?: TableProp; // if provided, family & tableName are prefilled & readonly
|
||||||
startOnPreview?: boolean;
|
startOnPreview?: boolean;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
@@ -60,13 +65,14 @@ export default function FirewallAddChainModal({
|
|||||||
const [results, setResults] = useState<CmdResult[]>([]);
|
const [results, setResults] = useState<CmdResult[]>([]);
|
||||||
const [selectedFamily, setSelectedFamily] = useState<string>(table?.family ?? 'bridge');
|
const [selectedFamily, setSelectedFamily] = useState<string>(table?.family ?? 'bridge');
|
||||||
|
|
||||||
// set defaults and prefill when modal opens or table prop changes
|
const isPrefilled = Boolean(table?.family && table?.name);
|
||||||
|
|
||||||
|
// init form values when modal opens or table prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// only prefill family/tableName if provided by prop
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
family: table?.family ?? 'bridge',
|
family: table?.family ?? 'bridge',
|
||||||
tableName: table?.name ?? 'filter',
|
tableName: table?.name ?? 'filter',
|
||||||
// intentionally do NOT set chainName here so user input is preserved normally
|
// Do not override chainName if the user has typed it previously
|
||||||
type: 'filter',
|
type: 'filter',
|
||||||
hook: 'forward',
|
hook: 'forward',
|
||||||
priority: 0,
|
priority: 0,
|
||||||
@@ -94,36 +100,36 @@ export default function FirewallAddChainModal({
|
|||||||
setRulesetEmpty(false);
|
setRulesetEmpty(false);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// don't block UI — warn user
|
notification.warning({
|
||||||
message.warning('Could not load ruleset: ' + (err?.message ?? String(err)));
|
message: 'Could not load ruleset',
|
||||||
|
description: err?.message ?? String(err),
|
||||||
|
duration: 6,
|
||||||
|
});
|
||||||
|
setRulesetEmpty(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingRuleset(false);
|
setLoadingRuleset(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build add chain command — now reads chainName from values OR live form value
|
// Build add chain command — respects provided values or live form values
|
||||||
function buildCommands(values: any): string[] {
|
function buildCommands(values?: any): string[] {
|
||||||
const family = values.family ?? form.getFieldValue('family') ?? table?.family ?? 'bridge';
|
const vals = values ?? form.getFieldsValue();
|
||||||
const tableName = values.tableName ?? form.getFieldValue('tableName') ?? table?.name ?? 'filter';
|
|
||||||
|
|
||||||
// prefer explicitly provided values.chainName, otherwise read from form live state
|
const family = (vals.family ?? table?.family ?? 'bridge').trim();
|
||||||
const chainFromValues =
|
const tableName = (vals.tableName ?? table?.name ?? 'filter').trim();
|
||||||
values && typeof values.chainName === 'string' && values.chainName.trim().length > 0
|
|
||||||
? values.chainName.trim()
|
|
||||||
: undefined;
|
|
||||||
const chainFromForm = form.getFieldValue('hook');
|
|
||||||
const chain =
|
|
||||||
chainFromValues ||
|
|
||||||
(typeof chainFromForm === 'string' && chainFromForm.trim().length > 0 ? chainFromForm.trim() : undefined) ||
|
|
||||||
'mychain';
|
|
||||||
|
|
||||||
const type = values.type ?? form.getFieldValue('type') ?? 'filter';
|
const type = (vals.type ?? 'filter').trim();
|
||||||
const hook = values.hook ?? form.getFieldValue('hook') ?? 'forward';
|
const hook = (vals.hook ?? 'forward').trim();
|
||||||
const priority =
|
const priority =
|
||||||
typeof (values.priority ?? form.getFieldValue('priority')) === 'number'
|
typeof vals.priority === 'number'
|
||||||
? (values.priority ?? form.getFieldValue('priority'))
|
? vals.priority
|
||||||
: 0;
|
: Number.isFinite(Number(vals.priority))
|
||||||
const policy = values.policy ?? form.getFieldValue('policy') ?? '';
|
? Number(vals.priority)
|
||||||
|
: 0;
|
||||||
|
const policy = vals.policy ?? '';
|
||||||
|
|
||||||
|
// chain name: prefer explicit chainName else fallback to hook (less ideal) else 'mychain'
|
||||||
|
const chain = hook;
|
||||||
|
|
||||||
const policyPart = policy ? ` policy ${policy} ;` : '';
|
const policyPart = policy ? ` policy ${policy} ;` : '';
|
||||||
const cmd = `add chain ${family} ${tableName} ${chain} { type ${type} hook ${hook} priority ${priority} ;${policyPart} }`;
|
const cmd = `add chain ${family} ${tableName} ${chain} { type ${type} hook ${hook} priority ${priority} ;${policyPart} }`;
|
||||||
@@ -150,20 +156,32 @@ export default function FirewallAddChainModal({
|
|||||||
setResults(acc);
|
setResults(acc);
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
|
|
||||||
// refresh ruleset after running
|
// refresh ruleset shown in modal
|
||||||
try {
|
try {
|
||||||
await refreshRuleset();
|
await refreshRuleset();
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore - refreshRuleset handles messaging */
|
/* ignored, refreshRuleset already notified on failure */
|
||||||
}
|
}
|
||||||
|
|
||||||
const hadError = acc.some((r) => r.err);
|
const hadError = acc.some((r) => r.err);
|
||||||
if (!hadError) {
|
if (!hadError) {
|
||||||
message.success('Chain created and ruleset refreshed');
|
// success notification inside the modal (not global refresh notification)
|
||||||
onClose();
|
notification.success({
|
||||||
|
message: 'Chain created',
|
||||||
|
description: 'Chain created and ruleset refreshed locally in the modal.',
|
||||||
|
duration: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inform parent that a resource was created.
|
||||||
|
// Parent can decide whether to refresh and whether to show a notification.
|
||||||
|
onClose?.(true);
|
||||||
if (onSuccess) onSuccess();
|
if (onSuccess) onSuccess();
|
||||||
} else {
|
} else {
|
||||||
message.error('Some commands returned errors — see results in the modal');
|
notification.error({
|
||||||
|
message: 'Some commands returned errors',
|
||||||
|
description: 'See execution results below for details.',
|
||||||
|
duration: 6,
|
||||||
|
});
|
||||||
setStep(1);
|
setStep(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +191,7 @@ export default function FirewallAddChainModal({
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (step === 0) onClose();
|
if (step === 0) onClose?.(false);
|
||||||
else setStep(0);
|
else setStep(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -184,22 +202,30 @@ export default function FirewallAddChainModal({
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
const txt = buildCommands(form.getFieldsValue()).join('\n');
|
const txt = buildCommands(form.getFieldsValue()).join('\n');
|
||||||
navigator.clipboard.writeText(txt).then(() => message.success('Command copied'));
|
try {
|
||||||
|
await navigator.clipboard.writeText(txt);
|
||||||
|
notification.success({ message: 'Command copied to clipboard' });
|
||||||
|
} catch {
|
||||||
|
notification.warning({ message: 'Unable to copy to clipboard' });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy Command
|
Copy Command
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
// validate required fields before going to preview
|
|
||||||
try {
|
try {
|
||||||
await form.validateFields(['chainName', ...(table ? [] : ['tableName', 'family'])]);
|
// validate name fields: chainName required; if not prefilled, tableName & family required
|
||||||
|
const requiredFields = ['chainName'];
|
||||||
|
if (!isPrefilled) requiredFields.push('tableName', 'family');
|
||||||
|
await form.validateFields(requiredFields as any);
|
||||||
setStep(1);
|
setStep(1);
|
||||||
} catch (e) {
|
} catch {
|
||||||
// validation errors shown by AntD
|
// AntD will show validation messages; no extra handling required
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -221,7 +247,7 @@ export default function FirewallAddChainModal({
|
|||||||
|
|
||||||
if (loadingRuleset) {
|
if (loadingRuleset) {
|
||||||
return (
|
return (
|
||||||
<Modal title="Add Chain" open={open} onCancel={onClose} footer={null} width={800}>
|
<Modal title="Add Chain" open={open} onCancel={() => onClose?.(false)} footer={null} width={800} destroyOnClose>
|
||||||
<div style={{ textAlign: 'center', padding: 28 }}>
|
<div style={{ textAlign: 'center', padding: 28 }}>
|
||||||
<Spin />
|
<Spin />
|
||||||
</div>
|
</div>
|
||||||
@@ -229,10 +255,15 @@ export default function FirewallAddChainModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPrefilled = Boolean(table?.family && table?.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Add Chain" open={open} onCancel={onClose} width={800} footer={renderFooter()} destroyOnClose>
|
<Modal
|
||||||
|
title="Add Chain"
|
||||||
|
open={open}
|
||||||
|
onCancel={() => onClose?.(false)}
|
||||||
|
width={800}
|
||||||
|
footer={renderFooter()}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
{/* Step 0: Form */}
|
{/* Step 0: Form */}
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -346,9 +377,14 @@ export default function FirewallAddChainModal({
|
|||||||
<Space style={{ marginTop: 12 }}>
|
<Space style={{ marginTop: 12 }}>
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
const txt = buildCommands(form.getFieldsValue()).join('\n');
|
const txt = buildCommands(form.getFieldsValue()).join('\n');
|
||||||
navigator.clipboard.writeText(txt).then(() => message.success('Command copied'));
|
try {
|
||||||
|
await navigator.clipboard.writeText(txt);
|
||||||
|
notification.success({ message: 'Command copied to clipboard' });
|
||||||
|
} catch {
|
||||||
|
notification.warning({ message: 'Unable to copy to clipboard' });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy command
|
Copy command
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/components/FirewallManager.tsx
|
// src/components/FirewallManager.tsx
|
||||||
import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Typography, message } from 'antd';
|
import { Button, Card, Col, Form, Input, Modal, Row, Select, Space, Spin, Typography, notification } from 'antd';
|
||||||
import { ReactElement, useEffect, useState } from 'react';
|
import { ReactElement, useEffect, useState } from 'react';
|
||||||
import { execFirewallRaw, fetchRuleset } from '../api/apiClient';
|
import { execFirewallRaw, fetchRuleset } from '../api/apiClient';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ type CmdResult = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose?: (created?: boolean) => void; // created === true when a table was created
|
||||||
startOnPreview?: boolean;
|
startOnPreview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +70,13 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
setRulesetEmpty(false);
|
setRulesetEmpty(false);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.warning('Could not load ruleset: ' + (err?.message ?? String(err)));
|
// use notification instead of message
|
||||||
|
notification.warning({
|
||||||
|
message: 'Could not load ruleset',
|
||||||
|
description: err?.message ?? String(err),
|
||||||
|
duration: 6,
|
||||||
|
});
|
||||||
|
setRulesetEmpty(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLocalLoadingRuleset(false);
|
setLocalLoadingRuleset(false);
|
||||||
}
|
}
|
||||||
@@ -78,8 +84,8 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
|
|
||||||
// Build commands: only create table
|
// Build commands: only create table
|
||||||
function buildCommands(values: any): string[] {
|
function buildCommands(values: any): string[] {
|
||||||
const family = values.family ?? 'bridge';
|
const family = (values.family ?? 'bridge').trim();
|
||||||
const table = values.tableName ?? 'filter';
|
const table = (values.tableName ?? 'filter').trim();
|
||||||
return [`add table ${family} ${table}`];
|
return [`add table ${family} ${table}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +113,25 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
try {
|
try {
|
||||||
await refreshRuleset();
|
await refreshRuleset();
|
||||||
} catch {
|
} catch {
|
||||||
// ignore — refreshRuleset handles messaging
|
// ignore — refreshRuleset handles notifications on error
|
||||||
}
|
}
|
||||||
|
|
||||||
const hadError = acc.some((r) => r.err);
|
const hadError = acc.some((r) => r.err);
|
||||||
if (!hadError) {
|
if (!hadError) {
|
||||||
message.success('Table created and ruleset refreshed');
|
// success -> notify briefly and inform parent that a resource was created
|
||||||
onClose();
|
notification.success({
|
||||||
|
message: 'Table created',
|
||||||
|
description: 'Table was created and ruleset has been refreshed locally in the modal.',
|
||||||
|
duration: 4,
|
||||||
|
});
|
||||||
|
onClose?.(true); // signal parent to refresh and close modal
|
||||||
} else {
|
} else {
|
||||||
message.error('Some commands returned errors — see results in the modal');
|
// error -> open results view and show notification
|
||||||
|
notification.error({
|
||||||
|
message: 'Some commands returned errors',
|
||||||
|
description: 'See execution results below for details.',
|
||||||
|
duration: 6,
|
||||||
|
});
|
||||||
setStep(1);
|
setStep(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +141,7 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (step === 0) onClose();
|
if (step === 0) onClose?.(false);
|
||||||
else setStep(0);
|
else setStep(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -134,7 +150,21 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
|
|
||||||
{step === 0 ? (
|
{step === 0 ? (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" onClick={() => setStep(1)}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
// validate form before preview
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then(() => setStep(1))
|
||||||
|
.catch(() =>
|
||||||
|
notification.warning({
|
||||||
|
message: 'Validation',
|
||||||
|
description: 'Please fill required fields before preview.',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
Preview
|
Preview
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -154,7 +184,7 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
// show spinner while checking ruleset
|
// show spinner while checking ruleset
|
||||||
if (localLoadingRuleset) {
|
if (localLoadingRuleset) {
|
||||||
return (
|
return (
|
||||||
<Modal title="Create Table" open={open} onCancel={onClose} footer={null} width={700}>
|
<Modal title="Create Table" open={open} onCancel={() => onClose?.(false)} footer={null} width={700}>
|
||||||
<div style={{ textAlign: 'center', padding: 28 }}>
|
<div style={{ textAlign: 'center', padding: 28 }}>
|
||||||
<Spin />
|
<Spin />
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +193,14 @@ export default function FirewallAddTableModal({ open, onClose, startOnPreview =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Create Table" open={open} onCancel={onClose} width={700} footer={renderFooter()} destroyOnClose>
|
<Modal
|
||||||
|
title="Create Table"
|
||||||
|
open={open}
|
||||||
|
onCancel={() => onClose?.(false)}
|
||||||
|
width={700}
|
||||||
|
footer={renderFooter()}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
{/* Step 0: minimal form */}
|
{/* Step 0: minimal form */}
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/components/FirewallTables.tsx
|
// src/components/FirewallTables.tsx
|
||||||
import { ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { ArrowDownOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
import { Alert, Button, Card, Divider, message, Modal, Space, Table, Typography } from 'antd';
|
import { Alert, Button, Card, Divider, Modal, notification, Space, Spin, Table, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||||
import { execFirewallRaw } from '../api/apiClient';
|
import { execFirewallRaw } from '../api/apiClient';
|
||||||
@@ -12,18 +12,10 @@ const { Paragraph, Text, Title } = Typography;
|
|||||||
|
|
||||||
/* ---------- Helpers ---------- */
|
/* ---------- Helpers ---------- */
|
||||||
|
|
||||||
/**
|
|
||||||
* Render a human friendly one-line representation of a rule.
|
|
||||||
* Accepts RuleOut (typed) but remains defensive against unexpected shapes.
|
|
||||||
*/
|
|
||||||
function renderRuleFriendly(rule: RuleOut | any): string {
|
function renderRuleFriendly(rule: RuleOut | any): string {
|
||||||
// prefer `text` if present
|
|
||||||
if (rule?.text && typeof rule.text === 'string' && rule.text.trim() !== '') return rule.text;
|
if (rule?.text && typeof rule.text === 'string' && rule.text.trim() !== '') return rule.text;
|
||||||
|
|
||||||
// if expr is a string, show it
|
|
||||||
if (rule?.expr && typeof rule.expr === 'string') return rule.expr;
|
if (rule?.expr && typeof rule.expr === 'string') return rule.expr;
|
||||||
|
|
||||||
// if expr is an array, try to pretty print common tokens
|
|
||||||
const expr = rule?.expr ?? rule;
|
const expr = rule?.expr ?? rule;
|
||||||
if (Array.isArray(expr)) {
|
if (Array.isArray(expr)) {
|
||||||
const tokens: string[] = [];
|
const tokens: string[] = [];
|
||||||
@@ -107,7 +99,6 @@ function renderRuleFriendly(rule: RuleOut | any): string {
|
|||||||
if (tokens.length > 0) return tokens.join(' ');
|
if (tokens.length > 0) return tokens.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback: if expr is an object, stringify a short excerpt
|
|
||||||
if (rule?.expr && typeof rule.expr === 'object') {
|
if (rule?.expr && typeof rule.expr === 'object') {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(rule.expr, (_k, v) => (v === undefined ? null : v)).slice(0, 500);
|
return JSON.stringify(rule.expr, (_k, v) => (v === undefined ? null : v)).slice(0, 500);
|
||||||
@@ -116,7 +107,6 @@ function renderRuleFriendly(rule: RuleOut | any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// final fallback: stringify whole rule
|
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(rule, null, 2);
|
return JSON.stringify(rule, null, 2);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -180,12 +170,20 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
try {
|
try {
|
||||||
const res = await runCommands([cmd]);
|
const res = await runCommands([cmd]);
|
||||||
const first = res[0];
|
const first = res[0];
|
||||||
if (!first.err) message.success('Rule deleted');
|
if (!first.err) {
|
||||||
else message.error('Delete returned error — check results panel');
|
notification.success({ message: 'Rule deleted', description: cmd });
|
||||||
|
} else {
|
||||||
|
notification.error({ message: 'Delete returned error', description: first.err });
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error('Delete failed: ' + (err?.message ?? String(err)));
|
notification.error({ message: 'Delete failed', description: err?.message ?? String(err) });
|
||||||
} finally {
|
} finally {
|
||||||
await refresh();
|
// auto-refresh after change (no refresh notification shown here)
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -212,12 +210,20 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
try {
|
try {
|
||||||
const res = await runCommands([cmd]);
|
const res = await runCommands([cmd]);
|
||||||
const first = res[0];
|
const first = res[0];
|
||||||
if (!first.err) message.success(`Chain ${chain} deleted`);
|
if (!first.err) {
|
||||||
else message.error(`Chain deletion returned error — check results panel`);
|
notification.success({ message: `Chain ${chain} deleted`, description: cmd });
|
||||||
|
} else {
|
||||||
|
notification.error({ message: 'Chain deletion returned error', description: first.err });
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error('Chain deletion failed: ' + (err?.message ?? String(err)));
|
notification.error({ message: 'Chain deletion failed', description: err?.message ?? String(err) });
|
||||||
} finally {
|
} finally {
|
||||||
await refresh();
|
// auto-refresh after change (no notification)
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -244,12 +250,20 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
try {
|
try {
|
||||||
const res = await runCommands([cmd]);
|
const res = await runCommands([cmd]);
|
||||||
const first = res[0];
|
const first = res[0];
|
||||||
if (!first.err) message.success(`Table ${table} deleted`);
|
if (!first.err) {
|
||||||
else message.error(`Table deletion returned error — check results panel`);
|
notification.success({ message: `Table ${table} deleted`, description: cmd });
|
||||||
|
} else {
|
||||||
|
notification.error({ message: 'Table deletion returned error', description: first.err });
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
message.error('Table deletion failed: ' + (err?.message ?? String(err)));
|
notification.error({ message: 'Table deletion failed', description: err?.message ?? String(err) });
|
||||||
} finally {
|
} finally {
|
||||||
await refresh();
|
// auto-refresh after change (no notification)
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -262,9 +276,10 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
try {
|
try {
|
||||||
await refresh();
|
await refresh();
|
||||||
message.success('Ruleset refreshed');
|
// Only show notification when user pressed the refresh button
|
||||||
} catch {
|
notification.success({ message: 'Ruleset refreshed' });
|
||||||
// ignore
|
} catch (err: any) {
|
||||||
|
notification.error({ message: 'Refresh failed', description: err?.message ?? String(err) });
|
||||||
} finally {
|
} finally {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
@@ -277,22 +292,40 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FirewallAddTableModal open={isOpenTableCreatorModal} onClose={() => setIsOpenTableCreatorModal(false)} />
|
{/* Pass onClose that accepts optional 'created' boolean. If the modal
|
||||||
|
calls onClose(true) we will auto-refresh (no refresh notification). */}
|
||||||
|
<FirewallAddTableModal
|
||||||
|
open={isOpenTableCreatorModal}
|
||||||
|
onClose={(created?: boolean) => {
|
||||||
|
setIsOpenTableCreatorModal(false);
|
||||||
|
if (created) {
|
||||||
|
// auto-refresh after create (no notification)
|
||||||
|
void refresh().catch(() => {});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title="Firewall Tables"
|
title="Firewall Tables"
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<Button onClick={handleRefresh} loading={refreshing}>
|
<Button
|
||||||
Refresh
|
onClick={handleRefresh}
|
||||||
|
loading={refreshing}
|
||||||
|
icon={refreshing ? <Spin size="small" /> : <ReloadOutlined />}
|
||||||
|
>
|
||||||
|
Refresh Ruleset
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={() => setIsOpenTableCreatorModal(true)}>
|
||||||
|
Add Table
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setIsOpenTableCreatorModal(true)}>Add Table</Button>
|
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{tablesToRender.length === 0 && (
|
{tablesToRender.length === 0 && (
|
||||||
<Alert
|
<Alert
|
||||||
type="info"
|
type="info"
|
||||||
title="No firewall tables found"
|
message="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 }}
|
||||||
/>
|
/>
|
||||||
@@ -321,20 +354,25 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => handleDeleteTable(table.family, table.name)}
|
onClick={() => handleDeleteTable(table.family, table.name)}
|
||||||
/>
|
/>
|
||||||
<Button size="small" onClick={() => setIsOpenChainCreatorModal(true)}>
|
<Button type="primary" onClick={() => setIsOpenChainCreatorModal(true)}>
|
||||||
Add Chain
|
Add Chain
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{/* Chain modal: same optional 'created' signal */}
|
||||||
<FirewallAddChainModal
|
<FirewallAddChainModal
|
||||||
open={isOpenChainCreatorModal}
|
open={isOpenChainCreatorModal}
|
||||||
onClose={() => setIsOpenChainCreatorModal(false)}
|
onClose={(created?: boolean) => {
|
||||||
|
setIsOpenChainCreatorModal(false);
|
||||||
|
if (created) {
|
||||||
|
void refresh().catch(() => {});
|
||||||
|
}
|
||||||
|
}}
|
||||||
table={{ family: table.family ?? '', name: table.name }}
|
table={{ family: table.family ?? '', name: table.name }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -406,7 +444,6 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
size="small"
|
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={() => handleDeleteChain(table.family, table.name, chain.name)}
|
onClick={() => handleDeleteChain(table.family, table.name, chain.name)}
|
||||||
/>
|
/>
|
||||||
@@ -437,9 +474,7 @@ export default function FirewallTables({ tables, error, refreshRules: refresh }:
|
|||||||
<div key={i} style={{ marginBottom: 12 }}>
|
<div key={i} style={{ marginBottom: 12 }}>
|
||||||
<Text strong>{r.cmd}</Text>
|
<Text strong>{r.cmd}</Text>
|
||||||
{r.err ? (
|
{r.err ? (
|
||||||
<Paragraph type="danger" style={{ marginTop: 6 }}>
|
<Paragraph style={{ color: 'var(--ant-error-color)', marginTop: 6 }}>{r.err}</Paragraph>
|
||||||
{r.err}
|
|
||||||
</Paragraph>
|
|
||||||
) : r.out ? (
|
) : r.out ? (
|
||||||
<>
|
<>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
FileAddOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
@@ -60,6 +61,7 @@ const { Paragraph } = Typography;
|
|||||||
* - Table action buttons are icon-only with Tooltip
|
* - Table action buttons are icon-only with Tooltip
|
||||||
* - Expands nested rows by default
|
* - Expands nested rows by default
|
||||||
* - Focus helper attempts to put caret at end inside editors when modals open
|
* - Focus helper attempts to put caret at end inside editors when modals open
|
||||||
|
* - New: Add requirements UI for scripts that have no requirements
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (code: string) => void }) {
|
export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (code: string) => void }) {
|
||||||
@@ -88,6 +90,13 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
const [reqEditorValue, setReqEditorValue] = useState<string>('');
|
const [reqEditorValue, setReqEditorValue] = useState<string>('');
|
||||||
const [reqEditingName, setReqEditingName] = useState<string | null>(null);
|
const [reqEditingName, setReqEditingName] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// add-requirements modal (for scripts that have no requirements)
|
||||||
|
const [addReqModalVisible, setAddReqModalVisible] = useState(false);
|
||||||
|
const [addReqTarget, setAddReqTarget] = useState<string | null>(null);
|
||||||
|
const [addReqUseInline, setAddReqUseInline] = useState(false);
|
||||||
|
const [addReqInlineValue, setAddReqInlineValue] = useState('');
|
||||||
|
const [addReqFile, setAddReqFile] = useState<File | null>(null);
|
||||||
|
|
||||||
// enable modal
|
// enable modal
|
||||||
const [enableModalVisible, setEnableModalVisible] = useState(false);
|
const [enableModalVisible, setEnableModalVisible] = useState(false);
|
||||||
const [enableTarget, setEnableTarget] = useState<string | null>(null);
|
const [enableTarget, setEnableTarget] = useState<string | null>(null);
|
||||||
@@ -105,7 +114,31 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
const all = await fetchScriptsAll();
|
const all = await fetchScriptsAll();
|
||||||
setScripts(all || []);
|
setScripts(all || []);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
notification.error({ message: 'Failed to fetch scripts', description: err?.message ?? String(err) });
|
notification.error({
|
||||||
|
message: 'Failed to fetch scripts',
|
||||||
|
description: err?.message ?? String(err),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleManualRefresh = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const all = await fetchScriptsAll();
|
||||||
|
setScripts(all || []);
|
||||||
|
|
||||||
|
const count = (all || []).length;
|
||||||
|
notification.success({
|
||||||
|
message: 'Refresh complete',
|
||||||
|
description: `Loaded ${count} script${count === 1 ? '' : 's'}.`,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
notification.error({
|
||||||
|
message: 'Failed to fetch scripts',
|
||||||
|
description: err?.message ?? String(err),
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -168,6 +201,10 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
if (reqModalVisible) focusEditorInModal('.ant-modal');
|
if (reqModalVisible) focusEditorInModal('.ant-modal');
|
||||||
}, [reqModalVisible, focusEditorInModal]);
|
}, [reqModalVisible, focusEditorInModal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addReqModalVisible) focusEditorInModal('.ant-modal');
|
||||||
|
}, [addReqModalVisible, focusEditorInModal]);
|
||||||
|
|
||||||
// show pip modal if pip output is present
|
// show pip modal if pip output is present
|
||||||
const showPipIfPresent = useCallback(
|
const showPipIfPresent = useCallback(
|
||||||
(resp?: ScriptUploadResponse | { pip?: { stdout?: string; stderr?: string } } | null) => {
|
(resp?: ScriptUploadResponse | { pip?: { stdout?: string; stderr?: string } } | null) => {
|
||||||
@@ -472,6 +509,57 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// Add Requirements (for scripts missing requirements)
|
||||||
|
// --------------------
|
||||||
|
const openAddRequirements = useCallback((scriptName: string) => {
|
||||||
|
setAddReqTarget(scriptName);
|
||||||
|
setAddReqUseInline(false);
|
||||||
|
setAddReqInlineValue('');
|
||||||
|
setAddReqFile(null);
|
||||||
|
setAddReqModalVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveAddRequirements = useCallback(async () => {
|
||||||
|
if (!addReqTarget) return;
|
||||||
|
let payload: File | Blob | null = null;
|
||||||
|
if (addReqFile) {
|
||||||
|
payload = addReqFile;
|
||||||
|
} else if (addReqUseInline && addReqInlineValue.trim() !== '') {
|
||||||
|
payload = new Blob([addReqInlineValue], { type: 'text/plain' });
|
||||||
|
} else {
|
||||||
|
notification.warning({
|
||||||
|
message: 'No requirements provided',
|
||||||
|
description: 'Upload a file or enter inline content.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await uploadRequirements(addReqTarget, payload);
|
||||||
|
notification.success({ message: 'requirements uploaded', description: `${addReqTarget} updated` });
|
||||||
|
showPipIfPresent(resp);
|
||||||
|
setAddReqModalVisible(false);
|
||||||
|
setAddReqTarget(null);
|
||||||
|
setAddReqInlineValue('');
|
||||||
|
setAddReqFile(null);
|
||||||
|
await refreshAll();
|
||||||
|
} catch (err: any) {
|
||||||
|
notification.error({ message: 'Failed to upload requirements', description: err?.message ?? String(err) });
|
||||||
|
}
|
||||||
|
}, [addReqFile, addReqInlineValue, addReqTarget, refreshAll, showPipIfPresent]);
|
||||||
|
|
||||||
|
// helper for Upload change inside add-req modal
|
||||||
|
const onAddReqUploadChange = useCallback((info: any) => {
|
||||||
|
const list: UploadFile[] = info.fileList || [];
|
||||||
|
if (list.length === 0) {
|
||||||
|
setAddReqFile(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const last = list[list.length - 1] as any;
|
||||||
|
setAddReqFile(last.originFileObj ?? last);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// --------------------
|
// --------------------
|
||||||
// Table columns
|
// Table columns
|
||||||
// --------------------
|
// --------------------
|
||||||
@@ -513,7 +601,13 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ color: '#999' }}>—</span>
|
<Space>
|
||||||
|
<IconButtonTooltip
|
||||||
|
title="Add requirements"
|
||||||
|
onClick={() => openAddRequirements(record.name)}
|
||||||
|
icon={<FileAddOutlined />}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -627,15 +721,13 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row justify="space-between" style={{ marginBottom: 12 }}>
|
<Row justify="space-between" style={{ marginBottom: 12 }}>
|
||||||
<Col>
|
<Col span={24} style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Space>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setUploadModalVisible(true)}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setUploadModalVisible(true)}>
|
Upload / New
|
||||||
Upload / New
|
</Button>
|
||||||
</Button>
|
<Button onClick={handleManualRefresh} icon={loading ? <Spin size="small" /> : <ReloadOutlined />}>
|
||||||
<Button onClick={refreshAll} icon={loading ? <Spin size="small" /> : <ReloadOutlined />}>
|
Refresh
|
||||||
Refresh
|
</Button>
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
@@ -821,6 +913,60 @@ export default function ScriptsManager({ onOpenInEditor }: { onOpenInEditor?: (c
|
|||||||
<PythonEditor value={reqEditorValue} onChange={setReqEditorValue} height={420} />
|
<PythonEditor value={reqEditorValue} onChange={setReqEditorValue} height={420} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Add Requirements Modal (for scripts without requirements) */}
|
||||||
|
<Modal
|
||||||
|
open={addReqModalVisible}
|
||||||
|
title={addReqTarget ? `Add requirements — ${addReqTarget}` : 'Add requirements'}
|
||||||
|
onCancel={() => {
|
||||||
|
setAddReqModalVisible(false);
|
||||||
|
setAddReqTarget(null);
|
||||||
|
setAddReqInlineValue('');
|
||||||
|
setAddReqFile(null);
|
||||||
|
setAddReqUseInline(false);
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setAddReqModalVisible(false);
|
||||||
|
setAddReqTarget(null);
|
||||||
|
setAddReqInlineValue('');
|
||||||
|
setAddReqFile(null);
|
||||||
|
setAddReqUseInline(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" onClick={saveAddRequirements}>
|
||||||
|
Save requirements
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
width={700}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8 }}>Upload a requirements.txt file (preferred)</div>
|
||||||
|
<Upload beforeUpload={() => false} maxCount={1} accept=".txt" onChange={onAddReqUploadChange}>
|
||||||
|
<Button icon={<UploadOutlined />}>Choose requirements file</Button>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Checkbox checked={addReqUseInline} onChange={(e) => setAddReqUseInline(e.target.checked)}>
|
||||||
|
Edit requirements inline
|
||||||
|
</Checkbox>
|
||||||
|
<div style={{ color: '#666' }}>If both a file and inline content are present, file takes precedence.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{addReqUseInline && (
|
||||||
|
<div>
|
||||||
|
<PythonEditor value={addReqInlineValue} onChange={setAddReqInlineValue} height={220} wrap />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Pip output modal */}
|
{/* Pip output modal */}
|
||||||
<Modal
|
<Modal
|
||||||
open={pipModalVisible}
|
open={pipModalVisible}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/pages/Firewall.tsx
|
// src/pages/Firewall.tsx
|
||||||
import { Alert, Col, Row, Spin } from 'antd';
|
import { Alert, Col, Row } from 'antd';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { fetchRuleset } from '../api/apiClient';
|
import { fetchRuleset } from '../api/apiClient';
|
||||||
import RuleBuilder from '../components/FirewallRuleBuilder';
|
import RuleBuilder from '../components/FirewallRuleBuilder';
|
||||||
@@ -41,9 +41,7 @@ export const Firewall: React.FC = () => {
|
|||||||
await load();
|
await load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
|
|
||||||
return loading ? (
|
return (
|
||||||
<Spin tip="Loading firewall rules..." />
|
|
||||||
) : (
|
|
||||||
<div className="firewall-page">
|
<div className="firewall-page">
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col xs={24}>
|
<Col xs={24}>
|
||||||
|
|||||||
Reference in New Issue
Block a user