fmt: use biome

This commit is contained in:
2025-12-06 22:26:28 +08:00
parent 8be7af6815
commit 9a82ecedac
22 changed files with 400 additions and 429 deletions

View File

@@ -35,6 +35,7 @@
pkg-config pkg-config
sqlite sqlite
gemini-cli gemini-cli
biome
]; ];
buildInputs = [ ]; buildInputs = [ ];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs); LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs);

41
frontend/biome.json Normal file
View File

@@ -0,0 +1,41 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.6/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"attributePosition": "auto",
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 110,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingCommas": "all"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}

View File

@@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@@ -1,13 +1,13 @@
import { Component, createSignal, onMount, Show } from 'solid-js'; import { Component, createSignal, onMount, Show } from "solid-js";
import RulesList from './components/RulesList'; import RulesList from "./components/RulesList";
import CommandQueue from './components/CommandQueue'; import CommandQueue from "./components/CommandQueue";
import Login from './components/Login'; import Login from "./components/Login";
import ChangePassword from './components/ChangePassword'; import ChangePassword from "./components/ChangePassword";
import RequestLog from './components/RequestLog'; import RequestLog from "./components/RequestLog";
import { authStore } from './api/auth'; import { authStore } from "./api/auth";
const App: Component = () => { const App: Component = () => {
const [activeTab, setActiveTab] = createSignal<'rules' | 'commands' | 'logs'>('rules'); const [activeTab, setActiveTab] = createSignal<"rules" | "commands" | "logs">("rules");
const [isAuthenticated, setIsAuthenticated] = createSignal(false); const [isAuthenticated, setIsAuthenticated] = createSignal(false);
const [showChangePassword, setShowChangePassword] = createSignal(false); const [showChangePassword, setShowChangePassword] = createSignal(false);
@@ -31,9 +31,7 @@ const App: Component = () => {
<div class="min-h-screen bg-gray-100"> <div class="min-h-screen bg-gray-100">
<header class="bg-white shadow"> <header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-6 flex justify-between items-center"> <div class="max-w-7xl mx-auto px-4 py-6 flex justify-between items-center">
<h1 class="text-3xl font-bold text-gray-900"> <h1 class="text-3xl font-bold text-gray-900">My Linspirer Control Panel</h1>
My Linspirer Control Panel
</h1>
<div class="flex space-x-3"> <div class="flex space-x-3">
<button <button
onClick={() => setShowChangePassword(true)} onClick={() => setShowChangePassword(true)}
@@ -55,31 +53,31 @@ const App: Component = () => {
<div class="max-w-7xl mx-auto px-4"> <div class="max-w-7xl mx-auto px-4">
<div class="flex space-x-8"> <div class="flex space-x-8">
<button <button
onClick={() => setActiveTab('rules')} onClick={() => setActiveTab("rules")}
class={`py-4 px-1 border-b-2 font-medium text-sm ${ class={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab() === 'rules' activeTab() === "rules"
? 'border-indigo-500 text-indigo-600' ? "border-indigo-500 text-indigo-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Interception Rules Interception Rules
</button> </button>
<button <button
onClick={() => setActiveTab('commands')} onClick={() => setActiveTab("commands")}
class={`py-4 px-1 border-b-2 font-medium text-sm ${ class={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab() === 'commands' activeTab() === "commands"
? 'border-indigo-500 text-indigo-600' ? "border-indigo-500 text-indigo-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Command Queue Command Queue
</button> </button>
<button <button
onClick={() => setActiveTab('logs')} onClick={() => setActiveTab("logs")}
class={`py-4 px-1 border-b-2 font-medium text-sm ${ class={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab() === 'logs' activeTab() === "logs"
? 'border-indigo-500 text-indigo-600' ? "border-indigo-500 text-indigo-600"
: 'border-transparent text-gray-500 hover:text-gray-700' : "border-transparent text-gray-500 hover:text-gray-700"
}`} }`}
> >
Request Log Request Log
@@ -89,16 +87,13 @@ const App: Component = () => {
</nav> </nav>
<main class="max-w-7xl mx-auto px-4 py-6"> <main class="max-w-7xl mx-auto px-4 py-6">
{activeTab() === 'rules' && <RulesList />} {activeTab() === "rules" && <RulesList />}
{activeTab() === 'commands' && <CommandQueue />} {activeTab() === "commands" && <CommandQueue />}
{activeTab() === 'logs' && <RequestLog />} {activeTab() === "logs" && <RequestLog />}
</main> </main>
<Show when={showChangePassword()}> <Show when={showChangePassword()}>
<ChangePassword <ChangePassword onClose={() => setShowChangePassword(false)} onLogout={handleLogout} />
onClose={() => setShowChangePassword(false)}
onLogout={handleLogout}
/>
</Show> </Show>
</div> </div>
</Show> </Show>

View File

@@ -1,4 +1,4 @@
const TOKEN_KEY = 'admin_token'; const TOKEN_KEY = "admin_token";
export const authStore = { export const authStore = {
getToken(): string | null { getToken(): string | null {

View File

@@ -1,18 +1,22 @@
import type { InterceptionRule, Command, CreateRuleRequest, UpdateRuleRequest, UpdateCommandRequest, RequestLog } from '../types'; import type {
import { authStore } from './auth'; InterceptionRule,
Command,
CreateRuleRequest,
UpdateRuleRequest,
UpdateCommandRequest,
RequestLog,
} from "../types";
import { authStore } from "./auth";
const API_BASE = '/admin/api'; const API_BASE = "/admin/api";
async function request<T>( async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
endpoint: string,
options?: RequestInit
): Promise<T> {
const token = authStore.getToken(); const token = authStore.getToken();
const response = await fetch(`${API_BASE}${endpoint}`, { const response = await fetch(`${API_BASE}${endpoint}`, {
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
...(token ? { 'Authorization': `Bearer ${token}` } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}),
...options?.headers, ...options?.headers,
}, },
...options, ...options,
@@ -22,7 +26,7 @@ async function request<T>(
// Unauthorized - clear token and trigger re-authentication // Unauthorized - clear token and trigger re-authentication
authStore.clearToken(); authStore.clearToken();
window.location.reload(); window.location.reload();
throw new Error('Unauthorized'); throw new Error("Unauthorized");
} }
if (!response.ok) { if (!response.ok) {
@@ -32,14 +36,14 @@ async function request<T>(
if (response.status === 204 || response.status === 200) { if (response.status === 204 || response.status === 200) {
// Check if response has content // Check if response has content
const contentType = response.headers.get('content-type'); const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes('application/json')) { if (!contentType || !contentType.includes("application/json")) {
return undefined as T; return undefined as T;
} }
// Check if there's actually content to parse // Check if there's actually content to parse
const text = await response.text(); const text = await response.text();
if (!text || text.trim() === '') { if (!text || text.trim() === "") {
return undefined as T; return undefined as T;
} }
@@ -51,43 +55,40 @@ async function request<T>(
// Rules API // Rules API
export const rulesApi = { export const rulesApi = {
list: () => list: () => request<InterceptionRule[]>("/rules"),
request<InterceptionRule[]>('/rules'),
create: (rule: CreateRuleRequest) => create: (rule: CreateRuleRequest) =>
request<InterceptionRule>('/rules', { request<InterceptionRule>("/rules", {
method: 'POST', method: "POST",
body: JSON.stringify(rule), body: JSON.stringify(rule),
}), }),
update: (id: number, rule: UpdateRuleRequest) => update: (id: number, rule: UpdateRuleRequest) =>
request<InterceptionRule>(`/rules/${id}`, { request<InterceptionRule>(`/rules/${id}`, {
method: 'PUT', method: "PUT",
body: JSON.stringify(rule), body: JSON.stringify(rule),
}), }),
delete: (id: number) => delete: (id: number) => request<void>(`/rules/${id}`, { method: "DELETE" }),
request<void>(`/rules/${id}`, { method: 'DELETE' }),
}; };
// Commands API // Commands API
export const commandsApi = { export const commandsApi = {
list: (status?: string) => list: (status?: string) => request<Command[]>(`/commands${status ? `?status=${status}` : ""}`),
request<Command[]>(`/commands${status ? `?status=${status}` : ''}`),
updateStatus: (id: number, req: UpdateCommandRequest) => updateStatus: (id: number, req: UpdateCommandRequest) =>
request<Command>(`/commands/${id}`, { request<Command>(`/commands/${id}`, {
method: 'POST', method: "POST",
body: JSON.stringify(req), body: JSON.stringify(req),
}), }),
}; };
// Config API // Config API
export const configApi = { export const configApi = {
get: () => request<Record<string, string>>('/config'), get: () => request<Record<string, string>>("/config"),
update: (config: Record<string, string>) => update: (config: Record<string, string>) =>
request<void>('/config', { request<void>("/config", {
method: 'PUT', method: "PUT",
body: JSON.stringify(config), body: JSON.stringify(config),
}), }),
}; };
@@ -95,8 +96,8 @@ export const configApi = {
// Auth API // Auth API
export const authApi = { export const authApi = {
changePassword: (oldPassword: string, newPassword: string) => changePassword: (oldPassword: string, newPassword: string) =>
request<void>('/password', { request<void>("/password", {
method: 'PUT', method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
old_password: oldPassword, old_password: oldPassword,
new_password: newPassword, new_password: newPassword,
@@ -109,12 +110,12 @@ export const logsApi = {
list: (params?: { method?: string; search?: string }) => { list: (params?: { method?: string; search?: string }) => {
const query = new URLSearchParams(); const query = new URLSearchParams();
if (params?.method) { if (params?.method) {
query.set('method', params.method); query.set("method", params.method);
} }
if (params?.search) { if (params?.search) {
query.set('search', params.search); query.set("search", params.search);
} }
const queryString = query.toString(); const queryString = query.toString();
return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ''}`); return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ""}`);
}, },
}; };

View File

@@ -1,10 +1,10 @@
import { Component, createSignal } from 'solid-js'; import { Component, createSignal } from "solid-js";
import { authApi } from '../api/client'; import { authApi } from "../api/client";
import { authStore } from '../api/auth'; import { authStore } from "../api/auth";
import { Button } from './ui/Button'; import { Button } from "./ui/Button";
import { CardContent, CardFooter, CardHeader } from './ui/Card'; import { CardContent, CardFooter, CardHeader } from "./ui/Card";
import { Input } from './ui/Input'; import { Input } from "./ui/Input";
import { Modal, ModalContent } from './ui/Modal'; import { Modal, ModalContent } from "./ui/Modal";
interface ChangePasswordProps { interface ChangePasswordProps {
onClose: () => void; onClose: () => void;
@@ -12,9 +12,9 @@ interface ChangePasswordProps {
} }
const ChangePassword: Component<ChangePasswordProps> = (props) => { const ChangePassword: Component<ChangePasswordProps> = (props) => {
const [oldPassword, setOldPassword] = createSignal(''); const [oldPassword, setOldPassword] = createSignal("");
const [newPassword, setNewPassword] = createSignal(''); const [newPassword, setNewPassword] = createSignal("");
const [confirmPassword, setConfirmPassword] = createSignal(''); const [confirmPassword, setConfirmPassword] = createSignal("");
const [error, setError] = createSignal<string | null>(null); const [error, setError] = createSignal<string | null>(null);
const [success, setSuccess] = createSignal(false); const [success, setSuccess] = createSignal(false);
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
@@ -26,17 +26,17 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
// Validation // Validation
if (!oldPassword() || !newPassword() || !confirmPassword()) { if (!oldPassword() || !newPassword() || !confirmPassword()) {
setError('Please fill in all fields'); setError("Please fill in all fields");
return; return;
} }
if (newPassword() !== confirmPassword()) { if (newPassword() !== confirmPassword()) {
setError('New passwords do not match'); setError("New passwords do not match");
return; return;
} }
if (newPassword().length < 6) { if (newPassword().length < 6) {
setError('New password must be at least 6 characters'); setError("New password must be at least 6 characters");
return; return;
} }
@@ -45,9 +45,9 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
try { try {
await authApi.changePassword(oldPassword(), newPassword()); await authApi.changePassword(oldPassword(), newPassword());
setSuccess(true); setSuccess(true);
setOldPassword(''); setOldPassword("");
setNewPassword(''); setNewPassword("");
setConfirmPassword(''); setConfirmPassword("");
// Close modal and logout after 2 seconds // Close modal and logout after 2 seconds
setTimeout(() => { setTimeout(() => {
@@ -56,7 +56,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
props.onLogout(); props.onLogout();
}, 2000); }, 2000);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to change password'); setError(err instanceof Error ? err.message : "Failed to change password");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -72,9 +72,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<CardContent> <CardContent>
{error() && ( {error() && (
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"> <div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">{error()}</div>
{error()}
</div>
)} )}
{success() && ( {success() && (
@@ -84,9 +82,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
)} )}
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">Old Password</label>
Old Password
</label>
<Input <Input
type="password" type="password"
value={oldPassword()} value={oldPassword()}
@@ -96,9 +92,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
New Password
</label>
<Input <Input
type="password" type="password"
value={newPassword()} value={newPassword()}
@@ -108,9 +102,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">Confirm New Password</label>
Confirm New Password
</label>
<Input <Input
type="password" type="password"
value={confirmPassword()} value={confirmPassword()}
@@ -120,19 +112,11 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
</div> </div>
</CardContent> </CardContent>
<CardFooter class="flex justify-end space-x-3 pt-4"> <CardFooter class="flex justify-end space-x-3 pt-4">
<Button <Button type="button" variant="secondary" onClick={props.onClose} disabled={loading()}>
type="button"
variant="secondary"
onClick={props.onClose}
disabled={loading()}
>
Cancel Cancel
</Button> </Button>
<Button <Button type="submit" disabled={loading()}>
type="submit" {loading() ? "Changing..." : "Change Password"}
disabled={loading()}
>
{loading() ? 'Changing...' : 'Change Password'}
</Button> </Button>
</CardFooter> </CardFooter>
</form> </form>

View File

@@ -1,7 +1,7 @@
import { Component, createResource, For, Show } from 'solid-js'; import { Component, createResource, For, Show } from "solid-js";
import { commandsApi } from '../api/client'; import { commandsApi } from "../api/client";
import { Button } from './ui/Button'; import { Button } from "./ui/Button";
import { Card } from './ui/Card'; import { Card } from "./ui/Card";
const CommandQueue: Component = () => { const CommandQueue: Component = () => {
const [commands, { refetch }] = createResource(commandsApi.list); const [commands, { refetch }] = createResource(commandsApi.list);
@@ -11,7 +11,7 @@ const CommandQueue: Component = () => {
await commandsApi.updateStatus(id, { status }); await commandsApi.updateStatus(id, { status });
refetch(); refetch();
} catch (err) { } catch (err) {
console.error('Failed to update command:', err); console.error("Failed to update command:", err);
alert(`Error: ${err}`); alert(`Error: ${err}`);
} }
}; };
@@ -26,26 +26,27 @@ const CommandQueue: Component = () => {
<Card> <Card>
<Show when={!commands.loading} fallback={<div class="p-4">Loading...</div>}> <Show when={!commands.loading} fallback={<div class="p-4">Loading...</div>}>
<For each={commands()} fallback={ <For
<div class="p-8 text-center text-gray-500"> each={commands()}
No commands in queue fallback={<div class="p-8 text-center text-gray-500">No commands in queue</div>}
</div> >
}>
{(cmd) => ( {(cmd) => (
<div class="border-b p-4 hover:bg-gray-50"> <div class="border-b p-4 hover:bg-gray-50">
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<span class={`px-2 py-1 rounded-full text-xs font-semibold ${ <span
cmd.status === 'verified' ? 'bg-green-100 text-green-800' : class={`px-2 py-1 rounded-full text-xs font-semibold ${
cmd.status === 'rejected' ? 'bg-red-100 text-red-800' : cmd.status === "verified"
'bg-yellow-100 text-yellow-800' ? "bg-green-100 text-green-800"
}`}> : cmd.status === "rejected"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{cmd.status} {cmd.status}
</span> </span>
<span class="text-sm text-gray-500"> <span class="text-sm text-gray-500">{formatDate(cmd.received_at)}</span>
{formatDate(cmd.received_at)}
</span>
</div> </div>
<pre class="text-sm bg-gray-100 p-3 rounded overflow-x-auto"> <pre class="text-sm bg-gray-100 p-3 rounded overflow-x-auto">
{JSON.stringify(cmd.command, null, 2)} {JSON.stringify(cmd.command, null, 2)}
@@ -56,19 +57,19 @@ const CommandQueue: Component = () => {
</div> </div>
</Show> </Show>
</div> </div>
<Show when={cmd.status === 'unverified'}> <Show when={cmd.status === "unverified"}>
<div class="ml-4 flex flex-col gap-2"> <div class="ml-4 flex flex-col gap-2">
<Button <Button
size="sm" size="sm"
variant="success" variant="success"
onClick={() => updateCommandStatus(cmd.id, 'verified')} onClick={() => updateCommandStatus(cmd.id, "verified")}
> >
Verify Verify
</Button> </Button>
<Button <Button
size="sm" size="sm"
variant="danger" variant="danger"
onClick={() => updateCommandStatus(cmd.id, 'rejected')} onClick={() => updateCommandStatus(cmd.id, "rejected")}
> >
Reject Reject
</Button> </Button>

View File

@@ -1,40 +1,40 @@
import { Component, createSignal } from 'solid-js'; import { Component, createSignal } from "solid-js";
import { Button } from './ui/Button'; import { Button } from "./ui/Button";
import { Card } from './ui/Card'; import { Card } from "./ui/Card";
import { Input } from './ui/Input'; import { Input } from "./ui/Input";
interface LoginProps { interface LoginProps {
onLoginSuccess: (token: string) => void; onLoginSuccess: (token: string) => void;
} }
const Login: Component<LoginProps> = (props) => { const Login: Component<LoginProps> = (props) => {
const [password, setPassword] = createSignal(''); const [password, setPassword] = createSignal("");
const [error, setError] = createSignal(''); const [error, setError] = createSignal("");
const [isLoading, setIsLoading] = createSignal(false); const [isLoading, setIsLoading] = createSignal(false);
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
setError(''); setError("");
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch('/admin/api/login', { const response = await fetch("/admin/api/login", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ password: password() }), body: JSON.stringify({ password: password() }),
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Login failed' })); const errorData = await response.json().catch(() => ({ error: "Login failed" }));
throw new Error(errorData.error || 'Invalid password'); throw new Error(errorData.error || "Invalid password");
} }
const data = await response.json(); const data = await response.json();
props.onLoginSuccess(data.token); props.onLoginSuccess(data.token);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Login failed'); setError(err instanceof Error ? err.message : "Login failed");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -43,9 +43,7 @@ const Login: Component<LoginProps> = (props) => {
return ( return (
<div class="min-h-screen bg-gray-100 flex items-center justify-center"> <div class="min-h-screen bg-gray-100 flex items-center justify-center">
<Card class="p-8 w-96"> <Card class="p-8 w-96">
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center"> <h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">My Linspirer Admin Login</h1>
My Linspirer Admin Login
</h1>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div class="mb-4"> <div class="mb-4">
@@ -69,12 +67,8 @@ const Login: Component<LoginProps> = (props) => {
</div> </div>
)} )}
<Button <Button type="submit" size="lg" disabled={isLoading()}>
type="submit" {isLoading() ? "Logging in..." : "Login"}
size="lg"
disabled={isLoading()}
>
{isLoading() ? 'Logging in...' : 'Login'}
</Button> </Button>
</form> </form>

View File

@@ -1,8 +1,8 @@
import { Component, createSignal, For, Show } from 'solid-js'; import { Component, createSignal, For, Show } from "solid-js";
import type { RequestLog } from '../types'; import type { RequestLog } from "../types";
import { Button } from './ui/Button'; import { Button } from "./ui/Button";
import { CardContent, CardFooter, CardHeader } from './ui/Card'; import { CardContent, CardFooter, CardHeader } from "./ui/Card";
import { Modal, ModalContent } from './ui/Modal'; import { Modal, ModalContent } from "./ui/Modal";
interface TreeViewProps { interface TreeViewProps {
data: any; data: any;
@@ -12,17 +12,17 @@ interface TreeViewProps {
const TreeView: Component<TreeViewProps> = (props) => { const TreeView: Component<TreeViewProps> = (props) => {
const [isOpen, setIsOpen] = createSignal(props.isRoot ?? false); const [isOpen, setIsOpen] = createSignal(props.isRoot ?? false);
const isObject = typeof props.data === 'object' && props.data !== null; const isObject = typeof props.data === "object" && props.data !== null;
const renderValue = (value: any) => { const renderValue = (value: any) => {
switch (typeof value) { switch (typeof value) {
case 'string': case "string":
return <span class="text-green-600">"{value}"</span>; return <span class="text-green-600">"{value}"</span>;
case 'number': case "number":
return <span class="text-blue-600">{value}</span>; return <span class="text-blue-600">{value}</span>;
case 'boolean': case "boolean":
return <span class="text-purple-600">{String(value)}</span>; return <span class="text-purple-600">{String(value)}</span>;
case 'object': case "object":
if (value === null) return <span class="text-gray-500">null</span>; if (value === null) return <span class="text-gray-500">null</span>;
// This case is handled by recursive TreeView // This case is handled by recursive TreeView
default: default:
@@ -36,9 +36,7 @@ const TreeView: Component<TreeViewProps> = (props) => {
class="flex items-center cursor-pointer hover:bg-gray-100 rounded px-1" class="flex items-center cursor-pointer hover:bg-gray-100 rounded px-1"
onClick={() => setIsOpen(!isOpen())} onClick={() => setIsOpen(!isOpen())}
> >
<span class="w-4 inline-block text-gray-500"> <span class="w-4 inline-block text-gray-500">{isObject ? (isOpen() ? "▼" : "►") : ""}</span>
{isObject ? (isOpen() ? '▼' : '►') : ''}
</span>
<span class="font-semibold text-gray-800">{props.name}:</span> <span class="font-semibold text-gray-800">{props.name}:</span>
<Show when={!isObject}> <Show when={!isObject}>
<span class="ml-2">{renderValue(props.data)}</span> <span class="ml-2">{renderValue(props.data)}</span>
@@ -55,7 +53,6 @@ const TreeView: Component<TreeViewProps> = (props) => {
); );
}; };
interface RequestDetailsProps { interface RequestDetailsProps {
log: RequestLog; log: RequestLog;
onClose: () => void; onClose: () => void;
@@ -67,7 +64,9 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
<ModalContent class="max-w-3xl h-auto max-h-[90vh]"> <ModalContent class="max-w-3xl h-auto max-h-[90vh]">
<CardHeader> <CardHeader>
<h3 class="text-lg font-medium text-gray-900">Request Details</h3> <h3 class="text-lg font-medium text-gray-900">Request Details</h3>
<p class="text-sm text-gray-500">{props.log.method} at {new Date(props.log.created_at).toLocaleString()}</p> <p class="text-sm text-gray-500">
{props.log.method} at {new Date(props.log.created_at).toLocaleString()}
</p>
</CardHeader> </CardHeader>
<CardContent class="max-h-[70vh] overflow-y-auto"> <CardContent class="max-h-[70vh] overflow-y-auto">
@@ -88,11 +87,7 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
</CardContent> </CardContent>
<CardFooter class="flex justify-end pt-4"> <CardFooter class="flex justify-end pt-4">
<Button <Button type="button" variant="secondary" onClick={props.onClose}>
type="button"
variant="secondary"
onClick={props.onClose}
>
Close Close
</Button> </Button>
</CardFooter> </CardFooter>

View File

@@ -1,14 +1,14 @@
import { Component, createMemo, createResource, createSignal, For, Show } from 'solid-js'; import { Component, createMemo, createResource, createSignal, For, Show } from "solid-js";
import { logsApi } from '../api/client'; import { logsApi } from "../api/client";
import type { RequestLog as RequestLogType } from '../types'; import type { RequestLog as RequestLogType } from "../types";
import { Card } from './ui/Card'; import { Card } from "./ui/Card";
import { Input } from './ui/Input'; import { Input } from "./ui/Input";
import { Select } from './ui/Select'; import { Select } from "./ui/Select";
import RequestDetails from './RequestDetails'; import RequestDetails from "./RequestDetails";
const RequestLog: Component = () => { const RequestLog: Component = () => {
const [search, setSearch] = createSignal(''); const [search, setSearch] = createSignal("");
const [method, setMethod] = createSignal(''); const [method, setMethod] = createSignal("");
const [selectedLog, setSelectedLog] = createSignal<RequestLogType | null>(null); const [selectedLog, setSelectedLog] = createSignal<RequestLogType | null>(null);
const [logs] = createResource( const [logs] = createResource(
@@ -17,12 +17,12 @@ const RequestLog: Component = () => {
// Solid's createResource refetches when the source accessor changes. // Solid's createResource refetches when the source accessor changes.
// We can add a debounce here if we want to avoid too many requests. // We can add a debounce here if we want to avoid too many requests.
return logsApi.list(filters); return logsApi.list(filters);
} },
); );
const methods = createMemo(() => { const methods = createMemo(() => {
if (!logs()) return []; if (!logs()) return [];
const allMethods = logs()!.map(log => log.method); const allMethods = logs()!.map((log) => log.method);
return [...new Set(allMethods)]; return [...new Set(allMethods)];
}); });
@@ -38,7 +38,6 @@ const RequestLog: Component = () => {
return new Date(dateStr).toLocaleString(); return new Date(dateStr).toLocaleString();
}; };
return ( return (
<> <>
<div class="space-y-4"> <div class="space-y-4">
@@ -61,9 +60,7 @@ const RequestLog: Component = () => {
class="min-w-[200px]" class="min-w-[200px]"
> >
<option value="">All Methods</option> <option value="">All Methods</option>
<For each={methods()}> <For each={methods()}>{(m) => <option value={m}>{m}</option>}</For>
{(m) => <option value={m}>{m}</option>}
</For>
</Select> </Select>
</div> </div>
</div> </div>
@@ -74,22 +71,40 @@ const RequestLog: Component = () => {
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Time</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Time</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Request Body</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Request Body
</th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
<Show when={!logs.loading} fallback={<tr><td colspan="3" class="text-center py-4">Loading...</td></tr>}> <Show
<For each={logs()} fallback={ when={!logs.loading}
fallback={
<tr> <tr>
<td colspan="3" class="text-center py-8 text-gray-500"> <td colspan="3" class="text-center py-4">
No logs found. Loading...
</td> </td>
</tr> </tr>
}> }
>
<For
each={logs()}
fallback={
<tr>
<td colspan="3" class="text-center py-8 text-gray-500">
No logs found.
</td>
</tr>
}
>
{(log) => ( {(log) => (
<tr class="hover:bg-gray-50 cursor-pointer" onClick={() => handleLogClick(log)}> <tr class="hover:bg-gray-50 cursor-pointer" onClick={() => handleLogClick(log)}>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{log.method}</td> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{formatDate(log.created_at)}</td> {log.method}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(log.created_at)}
</td>
<td class="px-6 py-4 text-sm text-gray-500 font-mono truncate max-w-lg"> <td class="px-6 py-4 text-sm text-gray-500 font-mono truncate max-w-lg">
{JSON.stringify(log.request_body)} {JSON.stringify(log.request_body)}
</td> </td>
@@ -102,9 +117,7 @@ const RequestLog: Component = () => {
</div> </div>
</Card> </Card>
</div> </div>
<Show when={selectedLog()}> <Show when={selectedLog()}>{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}</Show>
{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}
</Show>
</> </>
); );
}; };

View File

@@ -1,19 +1,19 @@
import { Component, createSignal, createResource, For, Show } from 'solid-js'; import { Component, createSignal, createResource, For, Show } from "solid-js";
import { rulesApi } from '../api/client'; import { rulesApi } from "../api/client";
import type { InterceptionRule } from '../types'; import type { InterceptionRule } from "../types";
import { Button } from './ui/Button'; import { Button } from "./ui/Button";
import { Card } from './ui/Card'; import { Card } from "./ui/Card";
import { Input } from './ui/Input'; import { Input } from "./ui/Input";
import { Select } from './ui/Select'; import { Select } from "./ui/Select";
import { Textarea } from './ui/Textarea'; import { Textarea } from "./ui/Textarea";
const RulesList: Component = () => { const RulesList: Component = () => {
const [rules, { refetch }] = createResource(rulesApi.list); const [rules, { refetch }] = createResource(rulesApi.list);
const [showEditor, setShowEditor] = createSignal(false); const [showEditor, setShowEditor] = createSignal(false);
const [editingId, setEditingId] = createSignal<number | null>(null); const [editingId, setEditingId] = createSignal<number | null>(null);
const [editingMethod, setEditingMethod] = createSignal(''); const [editingMethod, setEditingMethod] = createSignal("");
const [editingAction, setEditingAction] = createSignal<'passthrough' | 'modify' | 'replace'>('passthrough'); const [editingAction, setEditingAction] = createSignal<"passthrough" | "modify" | "replace">("passthrough");
const [editingResponse, setEditingResponse] = createSignal(''); const [editingResponse, setEditingResponse] = createSignal("");
const toggleRule = async (rule: InterceptionRule) => { const toggleRule = async (rule: InterceptionRule) => {
try { try {
@@ -22,13 +22,13 @@ const RulesList: Component = () => {
}); });
refetch(); refetch();
} catch (err) { } catch (err) {
console.error('Failed to toggle rule:', err); console.error("Failed to toggle rule:", err);
alert(`Error: ${err}`); alert(`Error: ${err}`);
} }
}; };
const deleteRule = async (id: number) => { const deleteRule = async (id: number) => {
if (!confirm('Are you sure you want to delete this rule?')) return; if (!confirm("Are you sure you want to delete this rule?")) return;
try { try {
if (id === editingId()) { if (id === editingId()) {
@@ -37,14 +37,14 @@ const RulesList: Component = () => {
await rulesApi.delete(id); await rulesApi.delete(id);
refetch(); refetch();
} catch (err) { } catch (err) {
console.error('Failed to delete rule:', err); console.error("Failed to delete rule:", err);
alert(`Error: ${err}`); alert(`Error: ${err}`);
} }
}; };
const createNewRule = async () => { const createNewRule = async () => {
if (!editingMethod()) { if (!editingMethod()) {
alert('Please enter a method name'); alert("Please enter a method name");
return; return;
} }
@@ -52,14 +52,14 @@ const RulesList: Component = () => {
await rulesApi.create({ await rulesApi.create({
method_name: editingMethod(), method_name: editingMethod(),
action: editingAction(), action: editingAction(),
custom_response: editingAction() === 'replace' ? editingResponse() : undefined, custom_response: editingAction() === "replace" ? editingResponse() : undefined,
}); });
setShowEditor(false); setShowEditor(false);
setEditingMethod(''); setEditingMethod("");
setEditingResponse(''); setEditingResponse("");
refetch(); refetch();
} catch (err) { } catch (err) {
console.error('Failed to create rule:', err); console.error("Failed to create rule:", err);
alert(`Error: ${err}`); alert(`Error: ${err}`);
} }
}; };
@@ -68,15 +68,15 @@ const RulesList: Component = () => {
setEditingId(rule.id); setEditingId(rule.id);
setEditingMethod(rule.method_name); setEditingMethod(rule.method_name);
setEditingAction(rule.action); setEditingAction(rule.action);
setEditingResponse(rule.custom_response || ''); setEditingResponse(rule.custom_response || "");
setShowEditor(true); setShowEditor(true);
}; };
const cancelEdit = () => { const cancelEdit = () => {
setEditingId(null); setEditingId(null);
setEditingMethod(''); setEditingMethod("");
setEditingAction('passthrough'); setEditingAction("passthrough");
setEditingResponse(''); setEditingResponse("");
setShowEditor(false); setShowEditor(false);
}; };
@@ -88,7 +88,7 @@ const RulesList: Component = () => {
} }
if (!editingMethod()) { if (!editingMethod()) {
alert('Please enter a method name'); alert("Please enter a method name");
return; return;
} }
@@ -96,12 +96,12 @@ const RulesList: Component = () => {
await rulesApi.update(id, { await rulesApi.update(id, {
method_name: editingMethod(), method_name: editingMethod(),
action: editingAction(), action: editingAction(),
custom_response: editingAction() === 'replace' ? editingResponse() : undefined, custom_response: editingAction() === "replace" ? editingResponse() : undefined,
}); });
cancelEdit(); cancelEdit();
refetch(); refetch();
} catch (err) { } catch (err) {
console.error('Failed to update rule:', err); console.error("Failed to update rule:", err);
alert(`Error: ${err}`); alert(`Error: ${err}`);
} }
}; };
@@ -119,20 +119,16 @@ const RulesList: Component = () => {
} }
}} }}
> >
{showEditor() ? 'Cancel' : '+ New Rule'} {showEditor() ? "Cancel" : "+ New Rule"}
</Button> </Button>
</div> </div>
<Show when={showEditor()}> <Show when={showEditor()}>
<Card class="p-6 mb-4"> <Card class="p-6 mb-4">
<h3 class="text-lg font-semibold mb-4"> <h3 class="text-lg font-semibold mb-4">{editingId() !== null ? "Edit Rule" : "Create New Rule"}</h3>
{editingId() !== null ? 'Edit Rule' : 'Create New Rule'}
</h3>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">Method Name</label>
Method Name
</label>
<Input <Input
type="text" type="text"
value={editingMethod()} value={editingMethod()}
@@ -142,9 +138,7 @@ const RulesList: Component = () => {
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">Action</label>
Action
</label>
<Select <Select
value={editingAction()} value={editingAction()}
onChange={(e) => setEditingAction(e.currentTarget.value as any)} onChange={(e) => setEditingAction(e.currentTarget.value as any)}
@@ -154,11 +148,9 @@ const RulesList: Component = () => {
</Select> </Select>
</div> </div>
<Show when={editingAction() === 'replace'}> <Show when={editingAction() === "replace"}>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1"> <label class="block text-sm font-medium text-gray-700 mb-1">Custom Response (JSON)</label>
Custom Response (JSON)
</label>
<Textarea <Textarea
value={editingResponse()} value={editingResponse()}
onInput={(e) => setEditingResponse(e.currentTarget.value)} onInput={(e) => setEditingResponse(e.currentTarget.value)}
@@ -168,11 +160,7 @@ const RulesList: Component = () => {
</div> </div>
</Show> </Show>
<Button <Button onClick={saveEdit}>{editingId() !== null ? "Update Rule" : "Create Rule"}</Button>
onClick={saveEdit}
>
{editingId() !== null ? 'Update Rule' : 'Create Rule'}
</Button>
</div> </div>
</Card> </Card>
</Show> </Show>
@@ -182,65 +170,64 @@ const RulesList: Component = () => {
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method Name</th>
Method Name <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Action</th>
</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-left text-xs font-medium text-gray-500 uppercase"> <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
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> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white divide-y divide-gray-200">
<Show when={!rules.loading} fallback={ <Show
<tr><td colspan="4" class="text-center py-4">Loading...</td></tr> when={!rules.loading}
}> fallback={
<For each={rules()} fallback={
<tr> <tr>
<td colspan="4" class="text-center py-8 text-gray-500"> <td colspan="4" class="text-center py-4">
No rules configured yet. Click "+ New Rule" to create one. Loading...
</td> </td>
</tr> </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) => ( {(rule) => (
<tr> <tr>
<td class="px-6 py-4 text-sm font-medium text-gray-900"> <td class="px-6 py-4 text-sm font-medium text-gray-900">{rule.method_name}</td>
{rule.method_name}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <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' : <span
rule.action === 'modify' ? 'bg-blue-100 text-blue-800' : class={`px-2 py-1 rounded-full text-xs ${
'bg-gray-100 text-gray-800' 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} {rule.action}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button <button
onClick={() => toggleRule(rule)} onClick={() => toggleRule(rule)}
class={`px-3 py-1 rounded-md text-sm font-medium ${rule.is_enabled class={`px-3 py-1 rounded-md text-sm font-medium ${
? 'bg-green-100 text-green-800' rule.is_enabled ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
: 'bg-gray-100 text-gray-800' }`}
}`}> >
{rule.is_enabled ? '✓ Enabled' : '✗ Disabled'} {rule.is_enabled ? "✓ Enabled" : "✗ Disabled"}
</button> </button>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<Button <Button variant="ghost" onClick={() => startEdit(rule)} class="mr-4">
variant="ghost"
onClick={() => startEdit(rule)}
class="mr-4">
Edit Edit
</Button> </Button>
<Button <Button variant="link" onClick={() => deleteRule(rule.id)}>
variant="link"
onClick={() => deleteRule(rule.id)}
>
Delete Delete
</Button> </Button>
</td> </td>

View File

@@ -1,35 +1,37 @@
import { Component, JSX, splitProps } from 'solid-js'; import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed', "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed",
{ {
variants: { variants: {
variant: { variant: {
primary: 'bg-indigo-600 text-white hover:bg-indigo-700', primary: "bg-indigo-600 text-white hover:bg-indigo-700",
secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50', secondary: "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50",
danger: 'bg-red-600 text-white hover:bg-red-700', danger: "bg-red-600 text-white hover:bg-red-700",
success: 'bg-green-600 text-white hover:bg-green-700', success: "bg-green-600 text-white hover:bg-green-700",
ghost: 'text-indigo-600 hover:text-indigo-900', ghost: "text-indigo-600 hover:text-indigo-900",
link: 'text-red-600 hover:text-red-900', link: "text-red-600 hover:text-red-900",
}, },
size: { size: {
sm: 'px-3 py-1', sm: "px-3 py-1",
md: 'px-4 py-2', md: "px-4 py-2",
lg: 'w-full px-4 py-2', lg: "w-full px-4 py-2",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'primary', variant: "primary",
size: 'md', size: "md",
}, },
} },
); );
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {} export interface ButtonProps
extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button: Component<ButtonProps> = (props) => { const Button: Component<ButtonProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'size', 'class']); const [local, others] = splitProps(props, ["variant", "size", "class"]);
return ( return (
<button <button
class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })} class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}

View File

@@ -1,59 +1,39 @@
import { Component, JSX, splitProps } from 'solid-js'; import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
const cardVariants = cva('bg-white shadow rounded-lg', { const cardVariants = cva("bg-white shadow rounded-lg", {
variants: { variants: {
variant: { variant: {
primary: 'overflow-hidden', primary: "overflow-hidden",
withPadding: 'p-6', withPadding: "p-6",
withPaddingAndDivider: 'p-4 divide-y divide-gray-200', withPaddingAndDivider: "p-4 divide-y divide-gray-200",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'primary', variant: "primary",
}, },
}); });
export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {} export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {}
const Card: Component<CardProps> = (props) => { const Card: Component<CardProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']); const [local, others] = splitProps(props, ["variant", "class"]);
return ( return <div class={cardVariants({ variant: local.variant, class: local.class })} {...others} />;
<div
class={cardVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
}; };
const CardHeader: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => { const CardHeader: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']); const [local, others] = splitProps(props, ["class"]);
return ( return <div class={`px-6 py-4 border-b border-gray-200 ${local.class}`} {...others} />;
<div
class={`px-6 py-4 border-b border-gray-200 ${local.class}`}
{...others}
/>
);
}; };
const CardContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => { const CardContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']); const [local, others] = splitProps(props, ["class"]);
return ( return <div class={`px-6 py-4 space-y-4 ${local.class}`} {...others} />;
<div
class={`px-6 py-4 space-y-4 ${local.class}`}
{...others}
/>
);
}; };
const CardFooter: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => { const CardFooter: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']); const [local, others] = splitProps(props, ["class"]);
return ( return <div class={`px-6 py-4 border-t border-gray-200 ${local.class}`} {...others} />;
<div
class={`px-6 py-4 border-t border-gray-200 ${local.class}`}
{...others}
/>
);
}; };
export { Card, CardHeader, CardContent, CardFooter, cardVariants }; export { Card, CardHeader, CardContent, CardFooter, cardVariants };

View File

@@ -1,30 +1,27 @@
import { Component, JSX, splitProps } from 'solid-js'; import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
const inputVariants = cva( const inputVariants = cva(
'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500', "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500",
{ {
variants: { variants: {
variant: { variant: {
primary: '', primary: "",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: 'primary', variant: "primary",
}, },
} },
); );
export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof inputVariants> {} export interface InputProps
extends JSX.InputHTMLAttributes<HTMLInputElement>,
VariantProps<typeof inputVariants> {}
const Input: Component<InputProps> = (props) => { const Input: Component<InputProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']); const [local, others] = splitProps(props, ["variant", "class"]);
return ( return <input class={inputVariants({ variant: local.variant, class: local.class })} {...others} />;
<input
class={inputVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
}; };
export { Input, inputVariants }; export { Input, inputVariants };

View File

@@ -1,7 +1,7 @@
import { Component, JSX, splitProps } from 'solid-js'; import { Component, JSX, splitProps } from "solid-js";
const Modal: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => { const Modal: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']); const [local, others] = splitProps(props, ["class"]);
return ( return (
<div <div
class={`fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 ${local.class || ""}`} class={`fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 ${local.class || ""}`}
@@ -11,13 +11,8 @@ const Modal: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
}; };
const ModalContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => { const ModalContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
const [local, others] = splitProps(props, ['class']); const [local, others] = splitProps(props, ["class"]);
return ( return <div class={`bg-white rounded-lg shadow-xl w-full mx-4 ${local.class || ""}`} {...others} />;
<div
class={`bg-white rounded-lg shadow-xl w-full mx-4 ${local.class || ""}`}
{...others}
/>
);
}; };
export { Modal, ModalContent }; export { Modal, ModalContent };

View File

@@ -1,30 +1,24 @@
import { Component, JSX, splitProps } from 'solid-js'; import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
const selectVariants = cva( const selectVariants = cva("w-full border border-gray-300 rounded-md px-3 py-2", {
'w-full border border-gray-300 rounded-md px-3 py-2', variants: {
{ variant: {
variants: { primary: "",
variant: {
primary: '',
},
}, },
defaultVariants: { },
variant: 'primary', defaultVariants: {
}, variant: "primary",
} },
); });
export interface SelectProps extends JSX.SelectHTMLAttributes<HTMLSelectElement>, VariantProps<typeof selectVariants> {} export interface SelectProps
extends JSX.SelectHTMLAttributes<HTMLSelectElement>,
VariantProps<typeof selectVariants> {}
const Select: Component<SelectProps> = (props) => { const Select: Component<SelectProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']); const [local, others] = splitProps(props, ["variant", "class"]);
return ( return <select class={selectVariants({ variant: local.variant, class: local.class })} {...others} />;
<select
class={selectVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
}; };
export { Select, selectVariants }; export { Select, selectVariants };

View File

@@ -1,30 +1,24 @@
import { Component, JSX, splitProps } from 'solid-js'; import { Component, JSX, splitProps } from "solid-js";
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from "class-variance-authority";
const textareaVariants = cva( const textareaVariants = cva("w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm", {
'w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm', variants: {
{ variant: {
variants: { primary: "",
variant: {
primary: '',
},
}, },
defaultVariants: { },
variant: 'primary', defaultVariants: {
}, variant: "primary",
} },
); });
export interface TextareaProps extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, VariantProps<typeof textareaVariants> {} export interface TextareaProps
extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof textareaVariants> {}
const Textarea: Component<TextareaProps> = (props) => { const Textarea: Component<TextareaProps> = (props) => {
const [local, others] = splitProps(props, ['variant', 'class']); const [local, others] = splitProps(props, ["variant", "class"]);
return ( return <textarea class={textareaVariants({ variant: local.variant, class: local.class })} {...others} />;
<textarea
class={textareaVariants({ variant: local.variant, class: local.class })}
{...others}
/>
);
}; };
export { Textarea, textareaVariants }; export { Textarea, textareaVariants };

View File

@@ -1,10 +1,10 @@
/* @refresh reload */ /* @refresh reload */
import { render } from 'solid-js/web'; import { render } from "solid-js/web";
import App from './App'; import App from "./App";
import './styles/index.css'; import "./styles/index.css";
const root = document.getElementById('root'); const root = document.getElementById("root");
if (!root) throw new Error('Root element not found'); if (!root) throw new Error("Root element not found");
render(() => <App />, root); render(() => <App />, root);

View File

@@ -1,7 +1,7 @@
export interface InterceptionRule { export interface InterceptionRule {
id: number; id: number;
method_name: string; method_name: string;
action: 'passthrough' | 'modify' | 'replace'; action: "passthrough" | "modify" | "replace";
custom_response: any | null; custom_response: any | null;
is_enabled: boolean; is_enabled: boolean;
created_at: string; created_at: string;
@@ -11,7 +11,7 @@ export interface InterceptionRule {
export interface Command { export interface Command {
id: number; id: number;
command: any; command: any;
status: 'unverified' | 'verified' | 'rejected'; status: "unverified" | "verified" | "rejected";
received_at: string; received_at: string;
processed_at?: string; processed_at?: string;
notes?: string; notes?: string;
@@ -36,21 +36,21 @@ export interface UpdateCommandRequest {
} }
export interface Request { export interface Request {
headers: Record<string, {value: string, modified: boolean}>; headers: Record<string, { value: string; modified: boolean }>;
body: {value: any, modified: boolean}; body: { value: any; modified: boolean };
} }
export interface Response { export interface Response {
headers: Record<string, {value: string, modified: boolean}>; headers: Record<string, { value: string; modified: boolean }>;
body: {value: any, modified: boolean}; body: { value: any; modified: boolean };
} }
export interface RequestLog { export interface RequestLog {
id: number; id: number;
method: string; method: string;
request_body: object; request_body: object;
response_body: object; response_body: object;
request_interception_action: string, request_interception_action: string;
response_interception_action: string, response_interception_action: string;
created_at: string; created_at: string;
} }

View File

@@ -1,11 +1,8 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
} };

View File

@@ -1,21 +1,21 @@
import { defineConfig } from 'vite'; import { defineConfig } from "vite";
import solid from 'vite-plugin-solid'; import solid from "vite-plugin-solid";
export default defineConfig({ export default defineConfig({
plugins: [solid()], plugins: [solid()],
base: '/admin/', base: "/admin/",
build: { build: {
outDir: 'dist', outDir: "dist",
assetsDir: 'assets', assetsDir: "assets",
sourcemap: false, sourcemap: false,
minify: 'terser', minify: "terser",
}, },
server: { server: {
proxy: { proxy: {
'/admin/api': { "/admin/api": {
target: 'http://localhost:8080', target: "http://localhost:8080",
changeOrigin: true, changeOrigin: true,
} },
} },
} },
}); });