247 lines
8.3 KiB
TypeScript
247 lines
8.3 KiB
TypeScript
import { type Component, createSignal, createResource, For, Show } from "solid-js";
|
|
import { rulesApi } from "../api/client";
|
|
import type { InterceptionRule } from "../types";
|
|
import { Button } from "./ui/Button";
|
|
import { Card } from "./ui/Card";
|
|
import { Input } from "./ui/Input";
|
|
import { Select } from "./ui/Select";
|
|
import { Textarea } from "./ui/Textarea";
|
|
|
|
const RulesList: Component = () => {
|
|
const [rules, { refetch }] = createResource(rulesApi.list);
|
|
const [showEditor, setShowEditor] = createSignal(false);
|
|
const [editingId, setEditingId] = createSignal<number | null>(null);
|
|
const [editingMethod, setEditingMethod] = createSignal("");
|
|
const [editingAction, setEditingAction] = createSignal<"passthrough" | "modify" | "replace">("passthrough");
|
|
const [editingResponse, setEditingResponse] = createSignal("");
|
|
|
|
const toggleRule = async (rule: InterceptionRule) => {
|
|
try {
|
|
await rulesApi.update(rule.id, {
|
|
is_enabled: !rule.is_enabled,
|
|
});
|
|
refetch();
|
|
} catch (err) {
|
|
console.error("Failed to toggle rule:", err);
|
|
alert(`Error: ${err}`);
|
|
}
|
|
};
|
|
|
|
const deleteRule = async (id: number) => {
|
|
if (!confirm("Are you sure you want to delete this rule?")) return;
|
|
|
|
try {
|
|
if (id === editingId()) {
|
|
setShowEditor(false);
|
|
}
|
|
await rulesApi.delete(id);
|
|
refetch();
|
|
} catch (err) {
|
|
console.error("Failed to delete rule:", err);
|
|
alert(`Error: ${err}`);
|
|
}
|
|
};
|
|
|
|
const createNewRule = async () => {
|
|
if (!editingMethod()) {
|
|
alert("Please enter a method name");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await rulesApi.create({
|
|
method_name: editingMethod(),
|
|
action: editingAction(),
|
|
custom_response: editingAction() === "replace" ? editingResponse() : undefined,
|
|
});
|
|
setShowEditor(false);
|
|
setEditingMethod("");
|
|
setEditingResponse("");
|
|
refetch();
|
|
} catch (err) {
|
|
console.error("Failed to create rule:", err);
|
|
alert(`Error: ${err}`);
|
|
}
|
|
};
|
|
|
|
const startEdit = (rule: InterceptionRule) => {
|
|
setEditingId(rule.id);
|
|
setEditingMethod(rule.method_name);
|
|
setEditingAction(rule.action);
|
|
setEditingResponse(rule.custom_response || "");
|
|
setShowEditor(true);
|
|
};
|
|
|
|
const cancelEdit = () => {
|
|
setEditingId(null);
|
|
setEditingMethod("");
|
|
setEditingAction("passthrough");
|
|
setEditingResponse("");
|
|
setShowEditor(false);
|
|
};
|
|
|
|
const saveEdit = async () => {
|
|
const id = editingId();
|
|
if (id === null) {
|
|
await createNewRule();
|
|
return;
|
|
}
|
|
|
|
if (!editingMethod()) {
|
|
alert("Please enter a method name");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await rulesApi.update(id, {
|
|
method_name: editingMethod(),
|
|
action: editingAction(),
|
|
custom_response: editingAction() === "replace" ? editingResponse() : undefined,
|
|
});
|
|
cancelEdit();
|
|
refetch();
|
|
} catch (err) {
|
|
console.error("Failed to update rule:", err);
|
|
alert(`Error: ${err}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div class="space-y-4">
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="text-2xl font-semibold">Interception Rules</h2>
|
|
<Button
|
|
onClick={() => {
|
|
if (showEditor()) {
|
|
cancelEdit();
|
|
} else {
|
|
setShowEditor(true);
|
|
}
|
|
}}
|
|
>
|
|
{showEditor() ? "Cancel" : "+ New Rule"}
|
|
</Button>
|
|
</div>
|
|
|
|
<Show when={showEditor()}>
|
|
<Card class="p-6 mb-4">
|
|
<h3 class="text-lg font-semibold mb-4">{editingId() !== null ? "Edit Rule" : "Create New Rule"}</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Method Name</label>
|
|
<Input
|
|
type="text"
|
|
value={editingMethod()}
|
|
onInput={(e) => setEditingMethod(e.currentTarget.value)}
|
|
placeholder="com.linspirer.method.name"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Action</label>
|
|
<Select
|
|
value={editingAction()}
|
|
onChange={(e) => setEditingAction(e.currentTarget.value as any)}
|
|
>
|
|
<option value="passthrough">Passthrough (Forward to server)</option>
|
|
<option value="replace">Replace (Custom response)</option>
|
|
</Select>
|
|
</div>
|
|
|
|
<Show when={editingAction() === "replace"}>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Custom Response (JSON)</label>
|
|
<Textarea
|
|
value={editingResponse()}
|
|
onInput={(e) => setEditingResponse(e.currentTarget.value)}
|
|
rows={10}
|
|
placeholder='{"code": 0, "type": "object", "data": {...}}'
|
|
/>
|
|
</div>
|
|
</Show>
|
|
|
|
<Button onClick={saveEdit}>{editingId() !== null ? "Update Rule" : "Create Rule"}</Button>
|
|
</div>
|
|
</Card>
|
|
</Show>
|
|
|
|
<Card>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method Name</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<Show
|
|
when={!rules.loading}
|
|
fallback={
|
|
<tr>
|
|
<td colspan="4" class="p-8 text-center text-gray-500">
|
|
Loading...
|
|
</td>
|
|
</tr>
|
|
}
|
|
>
|
|
<For
|
|
each={rules()}
|
|
fallback={
|
|
<tr>
|
|
<td colspan="4" class="text-center py-8 text-gray-500">
|
|
No rules configured yet. Click "+ New Rule" to create one.
|
|
</td>
|
|
</tr>
|
|
}
|
|
>
|
|
{(rule) => (
|
|
<tr>
|
|
<td class="px-6 py-4 text-sm font-medium text-gray-900">{rule.method_name}</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<span
|
|
class={`px-2 py-1 rounded-full text-xs ${
|
|
rule.action === "replace"
|
|
? "bg-purple-100 text-purple-800"
|
|
: rule.action === "modify"
|
|
? "bg-blue-100 text-blue-800"
|
|
: "bg-gray-100 text-gray-800"
|
|
}`}
|
|
>
|
|
{rule.action}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
<button
|
|
onClick={() => toggleRule(rule)}
|
|
class={`px-3 py-1 rounded-md text-sm font-medium ${
|
|
rule.is_enabled ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
|
|
}`}
|
|
>
|
|
{rule.is_enabled ? "✓ Enabled" : "✗ Disabled"}
|
|
</button>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<Button variant="ghost" onClick={() => startEdit(rule)} class="mr-4">
|
|
Edit
|
|
</Button>
|
|
<Button variant="link" onClick={() => deleteRule(rule.id)}>
|
|
Delete
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RulesList;
|