fmt: use biome
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
pkg-config
|
||||
sqlite
|
||||
gemini-cli
|
||||
biome
|
||||
];
|
||||
buildInputs = [ ];
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (nativeBuildInputs ++ buildInputs);
|
||||
|
||||
41
frontend/biome.json
Normal file
41
frontend/biome.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,4 @@ export default {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Component, createSignal, onMount, Show } from 'solid-js';
|
||||
import RulesList from './components/RulesList';
|
||||
import CommandQueue from './components/CommandQueue';
|
||||
import Login from './components/Login';
|
||||
import ChangePassword from './components/ChangePassword';
|
||||
import RequestLog from './components/RequestLog';
|
||||
import { authStore } from './api/auth';
|
||||
import { Component, createSignal, onMount, Show } from "solid-js";
|
||||
import RulesList from "./components/RulesList";
|
||||
import CommandQueue from "./components/CommandQueue";
|
||||
import Login from "./components/Login";
|
||||
import ChangePassword from "./components/ChangePassword";
|
||||
import RequestLog from "./components/RequestLog";
|
||||
import { authStore } from "./api/auth";
|
||||
|
||||
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 [showChangePassword, setShowChangePassword] = createSignal(false);
|
||||
|
||||
@@ -31,9 +31,7 @@ const App: Component = () => {
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
<header class="bg-white shadow">
|
||||
<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">
|
||||
My Linspirer Control Panel
|
||||
</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-900">My Linspirer Control Panel</h1>
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
onClick={() => setShowChangePassword(true)}
|
||||
@@ -55,31 +53,31 @@ const App: Component = () => {
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
<div class="flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('rules')}
|
||||
onClick={() => setActiveTab("rules")}
|
||||
class={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab() === 'rules'
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
activeTab() === "rules"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Interception Rules
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('commands')}
|
||||
onClick={() => setActiveTab("commands")}
|
||||
class={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab() === 'commands'
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
activeTab() === "commands"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Command Queue
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('logs')}
|
||||
onClick={() => setActiveTab("logs")}
|
||||
class={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab() === 'logs'
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
activeTab() === "logs"
|
||||
? "border-indigo-500 text-indigo-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Request Log
|
||||
@@ -89,16 +87,13 @@ const App: Component = () => {
|
||||
</nav>
|
||||
|
||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||
{activeTab() === 'rules' && <RulesList />}
|
||||
{activeTab() === 'commands' && <CommandQueue />}
|
||||
{activeTab() === 'logs' && <RequestLog />}
|
||||
{activeTab() === "rules" && <RulesList />}
|
||||
{activeTab() === "commands" && <CommandQueue />}
|
||||
{activeTab() === "logs" && <RequestLog />}
|
||||
</main>
|
||||
|
||||
<Show when={showChangePassword()}>
|
||||
<ChangePassword
|
||||
onClose={() => setShowChangePassword(false)}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
<ChangePassword onClose={() => setShowChangePassword(false)} onLogout={handleLogout} />
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const TOKEN_KEY = 'admin_token';
|
||||
const TOKEN_KEY = "admin_token";
|
||||
|
||||
export const authStore = {
|
||||
getToken(): string | null {
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import type { InterceptionRule, Command, CreateRuleRequest, UpdateRuleRequest, UpdateCommandRequest, RequestLog } from '../types';
|
||||
import { authStore } from './auth';
|
||||
import type {
|
||||
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>(
|
||||
endpoint: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||
const token = authStore.getToken();
|
||||
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options?.headers,
|
||||
},
|
||||
...options,
|
||||
@@ -22,7 +26,7 @@ async function request<T>(
|
||||
// Unauthorized - clear token and trigger re-authentication
|
||||
authStore.clearToken();
|
||||
window.location.reload();
|
||||
throw new Error('Unauthorized');
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -32,14 +36,14 @@ async function request<T>(
|
||||
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
// Check if response has content
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
// Check if there's actually content to parse
|
||||
const text = await response.text();
|
||||
if (!text || text.trim() === '') {
|
||||
if (!text || text.trim() === "") {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
@@ -51,43 +55,40 @@ async function request<T>(
|
||||
|
||||
// Rules API
|
||||
export const rulesApi = {
|
||||
list: () =>
|
||||
request<InterceptionRule[]>('/rules'),
|
||||
list: () => request<InterceptionRule[]>("/rules"),
|
||||
|
||||
create: (rule: CreateRuleRequest) =>
|
||||
request<InterceptionRule>('/rules', {
|
||||
method: 'POST',
|
||||
request<InterceptionRule>("/rules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(rule),
|
||||
}),
|
||||
|
||||
update: (id: number, rule: UpdateRuleRequest) =>
|
||||
request<InterceptionRule>(`/rules/${id}`, {
|
||||
method: 'PUT',
|
||||
method: "PUT",
|
||||
body: JSON.stringify(rule),
|
||||
}),
|
||||
|
||||
delete: (id: number) =>
|
||||
request<void>(`/rules/${id}`, { method: 'DELETE' }),
|
||||
delete: (id: number) => request<void>(`/rules/${id}`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
// Commands API
|
||||
export const commandsApi = {
|
||||
list: (status?: string) =>
|
||||
request<Command[]>(`/commands${status ? `?status=${status}` : ''}`),
|
||||
list: (status?: string) => request<Command[]>(`/commands${status ? `?status=${status}` : ""}`),
|
||||
|
||||
updateStatus: (id: number, req: UpdateCommandRequest) =>
|
||||
request<Command>(`/commands/${id}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify(req),
|
||||
}),
|
||||
};
|
||||
|
||||
// Config API
|
||||
export const configApi = {
|
||||
get: () => request<Record<string, string>>('/config'),
|
||||
get: () => request<Record<string, string>>("/config"),
|
||||
update: (config: Record<string, string>) =>
|
||||
request<void>('/config', {
|
||||
method: 'PUT',
|
||||
request<void>("/config", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(config),
|
||||
}),
|
||||
};
|
||||
@@ -95,8 +96,8 @@ export const configApi = {
|
||||
// Auth API
|
||||
export const authApi = {
|
||||
changePassword: (oldPassword: string, newPassword: string) =>
|
||||
request<void>('/password', {
|
||||
method: 'PUT',
|
||||
request<void>("/password", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
@@ -109,12 +110,12 @@ export const logsApi = {
|
||||
list: (params?: { method?: string; search?: string }) => {
|
||||
const query = new URLSearchParams();
|
||||
if (params?.method) {
|
||||
query.set('method', params.method);
|
||||
query.set("method", params.method);
|
||||
}
|
||||
if (params?.search) {
|
||||
query.set('search', params.search);
|
||||
query.set("search", params.search);
|
||||
}
|
||||
const queryString = query.toString();
|
||||
return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ''}`);
|
||||
return request<RequestLog[]>(`/logs${queryString ? `?${queryString}` : ""}`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, createSignal } from 'solid-js';
|
||||
import { authApi } from '../api/client';
|
||||
import { authStore } from '../api/auth';
|
||||
import { Button } from './ui/Button';
|
||||
import { CardContent, CardFooter, CardHeader } from './ui/Card';
|
||||
import { Input } from './ui/Input';
|
||||
import { Modal, ModalContent } from './ui/Modal';
|
||||
import { Component, createSignal } from "solid-js";
|
||||
import { authApi } from "../api/client";
|
||||
import { authStore } from "../api/auth";
|
||||
import { Button } from "./ui/Button";
|
||||
import { CardContent, CardFooter, CardHeader } from "./ui/Card";
|
||||
import { Input } from "./ui/Input";
|
||||
import { Modal, ModalContent } from "./ui/Modal";
|
||||
|
||||
interface ChangePasswordProps {
|
||||
onClose: () => void;
|
||||
@@ -12,9 +12,9 @@ interface ChangePasswordProps {
|
||||
}
|
||||
|
||||
const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
const [oldPassword, setOldPassword] = createSignal('');
|
||||
const [newPassword, setNewPassword] = createSignal('');
|
||||
const [confirmPassword, setConfirmPassword] = createSignal('');
|
||||
const [oldPassword, setOldPassword] = createSignal("");
|
||||
const [newPassword, setNewPassword] = createSignal("");
|
||||
const [confirmPassword, setConfirmPassword] = createSignal("");
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [success, setSuccess] = createSignal(false);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
@@ -26,17 +26,17 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
|
||||
// Validation
|
||||
if (!oldPassword() || !newPassword() || !confirmPassword()) {
|
||||
setError('Please fill in all fields');
|
||||
setError("Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword() !== confirmPassword()) {
|
||||
setError('New passwords do not match');
|
||||
setError("New passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword().length < 6) {
|
||||
setError('New password must be at least 6 characters');
|
||||
setError("New password must be at least 6 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
try {
|
||||
await authApi.changePassword(oldPassword(), newPassword());
|
||||
setSuccess(true);
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
setOldPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
|
||||
// Close modal and logout after 2 seconds
|
||||
setTimeout(() => {
|
||||
@@ -56,7 +56,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
props.onLogout();
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to change password');
|
||||
setError(err instanceof Error ? err.message : "Failed to change password");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -72,9 +72,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent>
|
||||
{error() && (
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||
{error()}
|
||||
</div>
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">{error()}</div>
|
||||
)}
|
||||
|
||||
{success() && (
|
||||
@@ -84,9 +82,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Old Password
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Old Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={oldPassword()}
|
||||
@@ -96,9 +92,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newPassword()}
|
||||
@@ -108,9 +102,7 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Confirm New Password</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={confirmPassword()}
|
||||
@@ -120,19 +112,11 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-end space-x-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={props.onClose}
|
||||
disabled={loading()}
|
||||
>
|
||||
<Button type="button" variant="secondary" onClick={props.onClose} disabled={loading()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading()}
|
||||
>
|
||||
{loading() ? 'Changing...' : 'Change Password'}
|
||||
<Button type="submit" disabled={loading()}>
|
||||
{loading() ? "Changing..." : "Change Password"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, createResource, For, Show } from 'solid-js';
|
||||
import { commandsApi } from '../api/client';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Component, createResource, For, Show } from "solid-js";
|
||||
import { commandsApi } from "../api/client";
|
||||
import { Button } from "./ui/Button";
|
||||
import { Card } from "./ui/Card";
|
||||
|
||||
const CommandQueue: Component = () => {
|
||||
const [commands, { refetch }] = createResource(commandsApi.list);
|
||||
@@ -11,7 +11,7 @@ const CommandQueue: Component = () => {
|
||||
await commandsApi.updateStatus(id, { status });
|
||||
refetch();
|
||||
} catch (err) {
|
||||
console.error('Failed to update command:', err);
|
||||
console.error("Failed to update command:", err);
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
};
|
||||
@@ -26,26 +26,27 @@ const CommandQueue: Component = () => {
|
||||
|
||||
<Card>
|
||||
<Show when={!commands.loading} fallback={<div class="p-4">Loading...</div>}>
|
||||
<For each={commands()} fallback={
|
||||
<div class="p-8 text-center text-gray-500">
|
||||
No commands in queue
|
||||
</div>
|
||||
}>
|
||||
<For
|
||||
each={commands()}
|
||||
fallback={<div class="p-8 text-center text-gray-500">No commands in queue</div>}
|
||||
>
|
||||
{(cmd) => (
|
||||
<div class="border-b p-4 hover:bg-gray-50">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class={`px-2 py-1 rounded-full text-xs font-semibold ${
|
||||
cmd.status === 'verified' ? 'bg-green-100 text-green-800' :
|
||||
cmd.status === 'rejected' ? 'bg-red-100 text-red-800' :
|
||||
'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
<span
|
||||
class={`px-2 py-1 rounded-full text-xs font-semibold ${
|
||||
cmd.status === "verified"
|
||||
? "bg-green-100 text-green-800"
|
||||
: cmd.status === "rejected"
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-yellow-100 text-yellow-800"
|
||||
}`}
|
||||
>
|
||||
{cmd.status}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">
|
||||
{formatDate(cmd.received_at)}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">{formatDate(cmd.received_at)}</span>
|
||||
</div>
|
||||
<pre class="text-sm bg-gray-100 p-3 rounded overflow-x-auto">
|
||||
{JSON.stringify(cmd.command, null, 2)}
|
||||
@@ -56,19 +57,19 @@ const CommandQueue: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={cmd.status === 'unverified'}>
|
||||
<Show when={cmd.status === "unverified"}>
|
||||
<div class="ml-4 flex flex-col gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="success"
|
||||
onClick={() => updateCommandStatus(cmd.id, 'verified')}
|
||||
onClick={() => updateCommandStatus(cmd.id, "verified")}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => updateCommandStatus(cmd.id, 'rejected')}
|
||||
onClick={() => updateCommandStatus(cmd.id, "rejected")}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { Component, createSignal } from 'solid-js';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Input } from './ui/Input';
|
||||
import { Component, createSignal } from "solid-js";
|
||||
import { Button } from "./ui/Button";
|
||||
import { Card } from "./ui/Card";
|
||||
import { Input } from "./ui/Input";
|
||||
|
||||
interface LoginProps {
|
||||
onLoginSuccess: (token: string) => void;
|
||||
}
|
||||
|
||||
const Login: Component<LoginProps> = (props) => {
|
||||
const [password, setPassword] = createSignal('');
|
||||
const [error, setError] = createSignal('');
|
||||
const [password, setPassword] = createSignal("");
|
||||
const [error, setError] = createSignal("");
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setError("");
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/api/login', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/admin/api/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ password: password() }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Login failed' }));
|
||||
throw new Error(errorData.error || 'Invalid password');
|
||||
const errorData = await response.json().catch(() => ({ error: "Login failed" }));
|
||||
throw new Error(errorData.error || "Invalid password");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
props.onLoginSuccess(data.token);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Login failed');
|
||||
setError(err instanceof Error ? err.message : "Login failed");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -43,9 +43,7 @@ const Login: Component<LoginProps> = (props) => {
|
||||
return (
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<Card class="p-8 w-96">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">
|
||||
My Linspirer Admin Login
|
||||
</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">My Linspirer Admin Login</h1>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class="mb-4">
|
||||
@@ -69,12 +67,8 @@ const Login: Component<LoginProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
disabled={isLoading()}
|
||||
>
|
||||
{isLoading() ? 'Logging in...' : 'Login'}
|
||||
<Button type="submit" size="lg" disabled={isLoading()}>
|
||||
{isLoading() ? "Logging in..." : "Login"}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, createSignal, For, Show } from 'solid-js';
|
||||
import type { RequestLog } from '../types';
|
||||
import { Button } from './ui/Button';
|
||||
import { CardContent, CardFooter, CardHeader } from './ui/Card';
|
||||
import { Modal, ModalContent } from './ui/Modal';
|
||||
import { Component, createSignal, For, Show } from "solid-js";
|
||||
import type { RequestLog } from "../types";
|
||||
import { Button } from "./ui/Button";
|
||||
import { CardContent, CardFooter, CardHeader } from "./ui/Card";
|
||||
import { Modal, ModalContent } from "./ui/Modal";
|
||||
|
||||
interface TreeViewProps {
|
||||
data: any;
|
||||
@@ -12,17 +12,17 @@ interface TreeViewProps {
|
||||
|
||||
const TreeView: Component<TreeViewProps> = (props) => {
|
||||
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) => {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
case "string":
|
||||
return <span class="text-green-600">"{value}"</span>;
|
||||
case 'number':
|
||||
case "number":
|
||||
return <span class="text-blue-600">{value}</span>;
|
||||
case 'boolean':
|
||||
case "boolean":
|
||||
return <span class="text-purple-600">{String(value)}</span>;
|
||||
case 'object':
|
||||
case "object":
|
||||
if (value === null) return <span class="text-gray-500">null</span>;
|
||||
// This case is handled by recursive TreeView
|
||||
default:
|
||||
@@ -36,9 +36,7 @@ const TreeView: Component<TreeViewProps> = (props) => {
|
||||
class="flex items-center cursor-pointer hover:bg-gray-100 rounded px-1"
|
||||
onClick={() => setIsOpen(!isOpen())}
|
||||
>
|
||||
<span class="w-4 inline-block text-gray-500">
|
||||
{isObject ? (isOpen() ? '▼' : '►') : ''}
|
||||
</span>
|
||||
<span class="w-4 inline-block text-gray-500">{isObject ? (isOpen() ? "▼" : "►") : ""}</span>
|
||||
<span class="font-semibold text-gray-800">{props.name}:</span>
|
||||
<Show when={!isObject}>
|
||||
<span class="ml-2">{renderValue(props.data)}</span>
|
||||
@@ -55,7 +53,6 @@ const TreeView: Component<TreeViewProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
interface RequestDetailsProps {
|
||||
log: RequestLog;
|
||||
onClose: () => void;
|
||||
@@ -67,7 +64,9 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
|
||||
<ModalContent class="max-w-3xl h-auto max-h-[90vh]">
|
||||
<CardHeader>
|
||||
<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>
|
||||
|
||||
<CardContent class="max-h-[70vh] overflow-y-auto">
|
||||
@@ -88,11 +87,7 @@ const RequestDetails: Component<RequestDetailsProps> = (props) => {
|
||||
</CardContent>
|
||||
|
||||
<CardFooter class="flex justify-end pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={props.onClose}
|
||||
>
|
||||
<Button type="button" variant="secondary" onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Component, createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||
import { logsApi } from '../api/client';
|
||||
import type { RequestLog as RequestLogType } from '../types';
|
||||
import { Card } from './ui/Card';
|
||||
import { Input } from './ui/Input';
|
||||
import { Select } from './ui/Select';
|
||||
import RequestDetails from './RequestDetails';
|
||||
import { Component, createMemo, createResource, createSignal, For, Show } from "solid-js";
|
||||
import { logsApi } from "../api/client";
|
||||
import type { RequestLog as RequestLogType } from "../types";
|
||||
import { Card } from "./ui/Card";
|
||||
import { Input } from "./ui/Input";
|
||||
import { Select } from "./ui/Select";
|
||||
import RequestDetails from "./RequestDetails";
|
||||
|
||||
const RequestLog: Component = () => {
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [method, setMethod] = createSignal('');
|
||||
const [search, setSearch] = createSignal("");
|
||||
const [method, setMethod] = createSignal("");
|
||||
const [selectedLog, setSelectedLog] = createSignal<RequestLogType | null>(null);
|
||||
|
||||
const [logs] = createResource(
|
||||
@@ -17,12 +17,12 @@ const RequestLog: Component = () => {
|
||||
// Solid's createResource refetches when the source accessor changes.
|
||||
// We can add a debounce here if we want to avoid too many requests.
|
||||
return logsApi.list(filters);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const methods = createMemo(() => {
|
||||
if (!logs()) return [];
|
||||
const allMethods = logs()!.map(log => log.method);
|
||||
const allMethods = logs()!.map((log) => log.method);
|
||||
return [...new Set(allMethods)];
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ const RequestLog: Component = () => {
|
||||
return new Date(dateStr).toLocaleString();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="space-y-4">
|
||||
@@ -61,9 +60,7 @@ const RequestLog: Component = () => {
|
||||
class="min-w-[200px]"
|
||||
>
|
||||
<option value="">All Methods</option>
|
||||
<For each={methods()}>
|
||||
{(m) => <option value={m}>{m}</option>}
|
||||
</For>
|
||||
<For each={methods()}>{(m) => <option value={m}>{m}</option>}</For>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,22 +71,40 @@ const RequestLog: Component = () => {
|
||||
<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">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>
|
||||
</thead>
|
||||
<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>}>
|
||||
<For each={logs()} fallback={
|
||||
<Show
|
||||
when={!logs.loading}
|
||||
fallback={
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-8 text-gray-500">
|
||||
No logs found.
|
||||
<td colspan="3" class="text-center py-4">
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
}>
|
||||
}
|
||||
>
|
||||
<For
|
||||
each={logs()}
|
||||
fallback={
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-8 text-gray-500">
|
||||
No logs found.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
>
|
||||
{(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 text-gray-500">{formatDate(log.created_at)}</td>
|
||||
<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 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">
|
||||
{JSON.stringify(log.request_body)}
|
||||
</td>
|
||||
@@ -102,9 +117,7 @@ const RequestLog: Component = () => {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Show when={selectedLog()}>
|
||||
{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}
|
||||
</Show>
|
||||
<Show when={selectedLog()}>{(log) => <RequestDetails log={log()} onClose={handleCloseDetails} />}</Show>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { 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';
|
||||
import { 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 [editingMethod, setEditingMethod] = createSignal("");
|
||||
const [editingAction, setEditingAction] = createSignal<"passthrough" | "modify" | "replace">("passthrough");
|
||||
const [editingResponse, setEditingResponse] = createSignal("");
|
||||
|
||||
const toggleRule = async (rule: InterceptionRule) => {
|
||||
try {
|
||||
@@ -22,13 +22,13 @@ const RulesList: Component = () => {
|
||||
});
|
||||
refetch();
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle rule:', 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;
|
||||
if (!confirm("Are you sure you want to delete this rule?")) return;
|
||||
|
||||
try {
|
||||
if (id === editingId()) {
|
||||
@@ -37,14 +37,14 @@ const RulesList: Component = () => {
|
||||
await rulesApi.delete(id);
|
||||
refetch();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete rule:', err);
|
||||
console.error("Failed to delete rule:", err);
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewRule = async () => {
|
||||
if (!editingMethod()) {
|
||||
alert('Please enter a method name');
|
||||
alert("Please enter a method name");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ const RulesList: Component = () => {
|
||||
await rulesApi.create({
|
||||
method_name: editingMethod(),
|
||||
action: editingAction(),
|
||||
custom_response: editingAction() === 'replace' ? editingResponse() : undefined,
|
||||
custom_response: editingAction() === "replace" ? editingResponse() : undefined,
|
||||
});
|
||||
setShowEditor(false);
|
||||
setEditingMethod('');
|
||||
setEditingResponse('');
|
||||
setEditingMethod("");
|
||||
setEditingResponse("");
|
||||
refetch();
|
||||
} catch (err) {
|
||||
console.error('Failed to create rule:', err);
|
||||
console.error("Failed to create rule:", err);
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
};
|
||||
@@ -68,15 +68,15 @@ const RulesList: Component = () => {
|
||||
setEditingId(rule.id);
|
||||
setEditingMethod(rule.method_name);
|
||||
setEditingAction(rule.action);
|
||||
setEditingResponse(rule.custom_response || '');
|
||||
setEditingResponse(rule.custom_response || "");
|
||||
setShowEditor(true);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null);
|
||||
setEditingMethod('');
|
||||
setEditingAction('passthrough');
|
||||
setEditingResponse('');
|
||||
setEditingMethod("");
|
||||
setEditingAction("passthrough");
|
||||
setEditingResponse("");
|
||||
setShowEditor(false);
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const RulesList: Component = () => {
|
||||
}
|
||||
|
||||
if (!editingMethod()) {
|
||||
alert('Please enter a method name');
|
||||
alert("Please enter a method name");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,12 +96,12 @@ const RulesList: Component = () => {
|
||||
await rulesApi.update(id, {
|
||||
method_name: editingMethod(),
|
||||
action: editingAction(),
|
||||
custom_response: editingAction() === 'replace' ? editingResponse() : undefined,
|
||||
custom_response: editingAction() === "replace" ? editingResponse() : undefined,
|
||||
});
|
||||
cancelEdit();
|
||||
refetch();
|
||||
} catch (err) {
|
||||
console.error('Failed to update rule:', err);
|
||||
console.error("Failed to update rule:", err);
|
||||
alert(`Error: ${err}`);
|
||||
}
|
||||
};
|
||||
@@ -119,20 +119,16 @@ const RulesList: Component = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showEditor() ? 'Cancel' : '+ New Rule'}
|
||||
{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>
|
||||
<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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Method Name</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={editingMethod()}
|
||||
@@ -142,9 +138,7 @@ const RulesList: Component = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Action
|
||||
</label>
|
||||
<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)}
|
||||
@@ -154,11 +148,9 @@ const RulesList: Component = () => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Show when={editingAction() === 'replace'}>
|
||||
<Show when={editingAction() === "replace"}>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Custom Response (JSON)
|
||||
</label>
|
||||
<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)}
|
||||
@@ -168,11 +160,7 @@ const RulesList: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Button
|
||||
onClick={saveEdit}
|
||||
>
|
||||
{editingId() !== null ? 'Update Rule' : 'Create Rule'}
|
||||
</Button>
|
||||
<Button onClick={saveEdit}>{editingId() !== null ? "Update Rule" : "Create Rule"}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Show>
|
||||
@@ -182,65 +170,64 @@ const RulesList: Component = () => {
|
||||
<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>
|
||||
<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="text-center py-4">Loading...</td></tr>
|
||||
}>
|
||||
<For each={rules()} fallback={
|
||||
<Show
|
||||
when={!rules.loading}
|
||||
fallback={
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-8 text-gray-500">
|
||||
No rules configured yet. Click "+ New Rule" to create one.
|
||||
<td colspan="4" class="text-center py-4">
|
||||
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 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'
|
||||
}`}>
|
||||
<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'}
|
||||
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">
|
||||
<Button variant="ghost" onClick={() => startEdit(rule)} class="mr-4">
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => deleteRule(rule.id)}
|
||||
>
|
||||
<Button variant="link" onClick={() => deleteRule(rule.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
primary: 'bg-indigo-600 text-white hover:bg-indigo-700',
|
||||
secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700',
|
||||
success: 'bg-green-600 text-white hover:bg-green-700',
|
||||
ghost: 'text-indigo-600 hover:text-indigo-900',
|
||||
link: 'text-red-600 hover:text-red-900',
|
||||
primary: "bg-indigo-600 text-white hover:bg-indigo-700",
|
||||
secondary: "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50",
|
||||
danger: "bg-red-600 text-white hover:bg-red-700",
|
||||
success: "bg-green-600 text-white hover:bg-green-700",
|
||||
ghost: "text-indigo-600 hover:text-indigo-900",
|
||||
link: "text-red-600 hover:text-red-900",
|
||||
},
|
||||
size: {
|
||||
sm: 'px-3 py-1',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'w-full px-4 py-2',
|
||||
sm: "px-3 py-1",
|
||||
md: "px-4 py-2",
|
||||
lg: "w-full px-4 py-2",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
variant: "primary",
|
||||
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 [local, others] = splitProps(props, ['variant', 'size', 'class']);
|
||||
const [local, others] = splitProps(props, ["variant", "size", "class"]);
|
||||
return (
|
||||
<button
|
||||
class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}
|
||||
|
||||
@@ -1,59 +1,39 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
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: {
|
||||
variant: {
|
||||
primary: 'overflow-hidden',
|
||||
withPadding: 'p-6',
|
||||
withPaddingAndDivider: 'p-4 divide-y divide-gray-200',
|
||||
primary: "overflow-hidden",
|
||||
withPadding: "p-6",
|
||||
withPaddingAndDivider: "p-4 divide-y divide-gray-200",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
variant: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {}
|
||||
|
||||
const Card: Component<CardProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<div
|
||||
class={cardVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["variant", "class"]);
|
||||
return <div class={cardVariants({ variant: local.variant, class: local.class })} {...others} />;
|
||||
};
|
||||
|
||||
const CardHeader: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`px-6 py-4 border-b border-gray-200 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["class"]);
|
||||
return <div class={`px-6 py-4 border-b border-gray-200 ${local.class}`} {...others} />;
|
||||
};
|
||||
|
||||
const CardContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`px-6 py-4 space-y-4 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["class"]);
|
||||
return <div class={`px-6 py-4 space-y-4 ${local.class}`} {...others} />;
|
||||
};
|
||||
|
||||
const CardFooter: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`px-6 py-4 border-t border-gray-200 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["class"]);
|
||||
return <div class={`px-6 py-4 border-t border-gray-200 ${local.class}`} {...others} />;
|
||||
};
|
||||
|
||||
export { Card, CardHeader, CardContent, CardFooter, cardVariants };
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
primary: '',
|
||||
primary: "",
|
||||
},
|
||||
},
|
||||
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 [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<input
|
||||
class={inputVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["variant", "class"]);
|
||||
return <input class={inputVariants({ variant: local.variant, class: local.class })} {...others} />;
|
||||
};
|
||||
|
||||
export { Input, inputVariants };
|
||||
|
||||
@@ -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 [local, others] = splitProps(props, ['class']);
|
||||
const [local, others] = splitProps(props, ["class"]);
|
||||
return (
|
||||
<div
|
||||
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 [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`bg-white rounded-lg shadow-xl w-full mx-4 ${local.class || ""}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["class"]);
|
||||
return <div class={`bg-white rounded-lg shadow-xl w-full mx-4 ${local.class || ""}`} {...others} />;
|
||||
};
|
||||
|
||||
export { Modal, ModalContent };
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
const selectVariants = cva(
|
||||
'w-full border border-gray-300 rounded-md px-3 py-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '',
|
||||
},
|
||||
const selectVariants = cva("w-full border border-gray-300 rounded-md px-3 py-2", {
|
||||
variants: {
|
||||
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 [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<select
|
||||
class={selectVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["variant", "class"]);
|
||||
return <select class={selectVariants({ variant: local.variant, class: local.class })} {...others} />;
|
||||
};
|
||||
|
||||
export { Select, selectVariants };
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Component, JSX, splitProps } from "solid-js";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
const textareaVariants = cva(
|
||||
'w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '',
|
||||
},
|
||||
const textareaVariants = cva("w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm", {
|
||||
variants: {
|
||||
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 [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<textarea
|
||||
class={textareaVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
const [local, others] = splitProps(props, ["variant", "class"]);
|
||||
return <textarea class={textareaVariants({ variant: local.variant, class: local.class })} {...others} />;
|
||||
};
|
||||
|
||||
export { Textarea, textareaVariants };
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* @refresh reload */
|
||||
import { render } from 'solid-js/web';
|
||||
import App from './App';
|
||||
import './styles/index.css';
|
||||
import { render } from "solid-js/web";
|
||||
import App from "./App";
|
||||
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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface InterceptionRule {
|
||||
id: number;
|
||||
method_name: string;
|
||||
action: 'passthrough' | 'modify' | 'replace';
|
||||
action: "passthrough" | "modify" | "replace";
|
||||
custom_response: any | null;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
@@ -11,7 +11,7 @@ export interface InterceptionRule {
|
||||
export interface Command {
|
||||
id: number;
|
||||
command: any;
|
||||
status: 'unverified' | 'verified' | 'rejected';
|
||||
status: "unverified" | "verified" | "rejected";
|
||||
received_at: string;
|
||||
processed_at?: string;
|
||||
notes?: string;
|
||||
@@ -36,21 +36,21 @@ export interface UpdateCommandRequest {
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
headers: Record<string, {value: string, modified: boolean}>;
|
||||
body: {value: any, modified: boolean};
|
||||
headers: Record<string, { value: string; modified: boolean }>;
|
||||
body: { value: any; modified: boolean };
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
headers: Record<string, {value: string, modified: boolean}>;
|
||||
body: {value: any, modified: boolean};
|
||||
headers: Record<string, { value: string; modified: boolean }>;
|
||||
body: { value: any; modified: boolean };
|
||||
}
|
||||
|
||||
export interface RequestLog {
|
||||
id: number;
|
||||
method: string;
|
||||
request_body: object;
|
||||
response_body: object;
|
||||
request_interception_action: string,
|
||||
response_interception_action: string,
|
||||
created_at: string;
|
||||
id: number;
|
||||
method: string;
|
||||
request_body: object;
|
||||
response_body: object;
|
||||
request_interception_action: string;
|
||||
response_interception_action: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import solid from 'vite-plugin-solid';
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
base: '/admin/',
|
||||
base: "/admin/",
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
outDir: "dist",
|
||||
assetsDir: "assets",
|
||||
sourcemap: false,
|
||||
minify: 'terser',
|
||||
minify: "terser",
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/admin/api': {
|
||||
target: 'http://localhost:8080',
|
||||
"/admin/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user