mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: implement server logging and event hook features
- Introduced server log level configuration and HTTP request logging settings, allowing users to control the verbosity of server logs and enable or disable request logging at runtime. - Added an Event Hook Service to execute custom actions based on system events, supporting shell commands and HTTP webhooks. - Enhanced the UI with new sections for managing server logging preferences and event hooks, including a dialog for creating and editing hooks. - Updated global settings to include server log level and request logging options, ensuring persistence across sessions. These changes aim to improve debugging capabilities and provide users with customizable event-driven actions within the application.
This commit is contained in:
@@ -19,6 +19,7 @@ import { WorktreesSection } from './settings-view/worktrees';
|
||||
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
|
||||
import { AccountSection } from './settings-view/account';
|
||||
import { SecuritySection } from './settings-view/security';
|
||||
import { DeveloperSection } from './settings-view/developer/developer-section';
|
||||
import {
|
||||
ClaudeSettingsTab,
|
||||
CursorSettingsTab,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
} from './settings-view/providers';
|
||||
import { MCPServersSection } from './settings-view/mcp-servers';
|
||||
import { PromptCustomizationSection } from './settings-view/prompts';
|
||||
import { EventHooksSection } from './settings-view/event-hooks';
|
||||
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
|
||||
import type { Project as ElectronProject } from '@/lib/electron';
|
||||
|
||||
@@ -183,6 +185,8 @@ export function SettingsView() {
|
||||
return (
|
||||
<AudioSection muteDoneSound={muteDoneSound} onMuteDoneSoundChange={setMuteDoneSound} />
|
||||
);
|
||||
case 'event-hooks':
|
||||
return <EventHooksSection />;
|
||||
case 'defaults':
|
||||
return (
|
||||
<FeatureDefaultsSection
|
||||
@@ -215,6 +219,8 @@ export function SettingsView() {
|
||||
onSkipSandboxWarningChange={setSkipSandboxWarning}
|
||||
/>
|
||||
);
|
||||
case 'developer':
|
||||
return <DeveloperSection />;
|
||||
case 'danger':
|
||||
return (
|
||||
<DangerZoneSection
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
Shield,
|
||||
Cpu,
|
||||
GitBranch,
|
||||
Code2,
|
||||
Webhook,
|
||||
} from 'lucide-react';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
|
||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||
@@ -63,6 +65,7 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
||||
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
|
||||
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
||||
{ id: 'audio', label: 'Audio', icon: Volume2 },
|
||||
{ id: 'event-hooks', label: 'Event Hooks', icon: Webhook },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -72,6 +75,10 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Advanced',
|
||||
items: [{ id: 'developer', label: 'Developer', icon: Code2 }],
|
||||
},
|
||||
];
|
||||
|
||||
// Flat list of all global nav items for backwards compatibility
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Code2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore, type ServerLogLevel } from '@/store/app-store';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const LOG_LEVEL_OPTIONS: { value: ServerLogLevel; label: string; description: string }[] = [
|
||||
{ value: 'error', label: 'Error', description: 'Only show error messages' },
|
||||
{ value: 'warn', label: 'Warning', description: 'Show warnings and errors' },
|
||||
{ value: 'info', label: 'Info', description: 'Show general information (default)' },
|
||||
{ value: 'debug', label: 'Debug', description: 'Show all messages including debug' },
|
||||
];
|
||||
|
||||
export function DeveloperSection() {
|
||||
const { serverLogLevel, setServerLogLevel, enableRequestLogging, setEnableRequestLogging } =
|
||||
useAppStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 flex items-center justify-center border border-purple-500/20">
|
||||
<Code2 className="w-5 h-5 text-purple-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Developer</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Advanced settings for debugging and development.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Server Log Level */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-foreground font-medium">Server Log Level</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Control the verbosity of API server logs. Set to "Error" to only see error messages in
|
||||
the server console.
|
||||
</p>
|
||||
<select
|
||||
value={serverLogLevel}
|
||||
onChange={(e) => {
|
||||
setServerLogLevel(e.target.value as ServerLogLevel);
|
||||
toast.success(`Log level changed to ${e.target.value}`, {
|
||||
description: 'Server logging verbosity updated',
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-lg',
|
||||
'bg-accent/30 border border-border/50',
|
||||
'text-foreground text-sm',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30'
|
||||
)}
|
||||
>
|
||||
{LOG_LEVEL_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label} - {option.description}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* HTTP Request Logging */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-border/30">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-foreground font-medium">HTTP Request Logging</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Log all HTTP requests (method, URL, status) to the server console.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enableRequestLogging}
|
||||
onCheckedChange={(checked) => {
|
||||
setEnableRequestLogging(checked);
|
||||
toast.success(checked ? 'Request logging enabled' : 'Request logging disabled', {
|
||||
description: 'HTTP request logging updated',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Terminal, Globe } from 'lucide-react';
|
||||
import type {
|
||||
EventHook,
|
||||
EventHookTrigger,
|
||||
EventHookHttpMethod,
|
||||
EventHookShellAction,
|
||||
EventHookHttpAction,
|
||||
} from '@automaker/types';
|
||||
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
|
||||
|
||||
interface EventHookDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingHook: EventHook | null;
|
||||
onSave: (hook: EventHook) => void;
|
||||
}
|
||||
|
||||
type ActionType = 'shell' | 'http';
|
||||
|
||||
const TRIGGER_OPTIONS: EventHookTrigger[] = [
|
||||
'feature_success',
|
||||
'feature_error',
|
||||
'auto_mode_complete',
|
||||
'auto_mode_error',
|
||||
];
|
||||
|
||||
const HTTP_METHODS: EventHookHttpMethod[] = ['POST', 'GET', 'PUT', 'PATCH'];
|
||||
|
||||
export function EventHookDialog({ open, onOpenChange, editingHook, onSave }: EventHookDialogProps) {
|
||||
// Form state
|
||||
const [name, setName] = useState('');
|
||||
const [trigger, setTrigger] = useState<EventHookTrigger>('feature_success');
|
||||
const [actionType, setActionType] = useState<ActionType>('shell');
|
||||
|
||||
// Shell action state
|
||||
const [command, setCommand] = useState('');
|
||||
const [timeout, setTimeout] = useState('30000');
|
||||
|
||||
// HTTP action state
|
||||
const [url, setUrl] = useState('');
|
||||
const [method, setMethod] = useState<EventHookHttpMethod>('POST');
|
||||
const [headers, setHeaders] = useState('');
|
||||
const [body, setBody] = useState('');
|
||||
|
||||
// Reset form when dialog opens/closes or editingHook changes
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (editingHook) {
|
||||
// Populate form with existing hook data
|
||||
setName(editingHook.name || '');
|
||||
setTrigger(editingHook.trigger);
|
||||
setActionType(editingHook.action.type);
|
||||
|
||||
if (editingHook.action.type === 'shell') {
|
||||
const shellAction = editingHook.action as EventHookShellAction;
|
||||
setCommand(shellAction.command);
|
||||
setTimeout(String(shellAction.timeout || 30000));
|
||||
// Reset HTTP fields
|
||||
setUrl('');
|
||||
setMethod('POST');
|
||||
setHeaders('');
|
||||
setBody('');
|
||||
} else {
|
||||
const httpAction = editingHook.action as EventHookHttpAction;
|
||||
setUrl(httpAction.url);
|
||||
setMethod(httpAction.method);
|
||||
setHeaders(httpAction.headers ? JSON.stringify(httpAction.headers, null, 2) : '');
|
||||
setBody(httpAction.body || '');
|
||||
// Reset shell fields
|
||||
setCommand('');
|
||||
setTimeout('30000');
|
||||
}
|
||||
} else {
|
||||
// Reset to defaults for new hook
|
||||
setName('');
|
||||
setTrigger('feature_success');
|
||||
setActionType('shell');
|
||||
setCommand('');
|
||||
setTimeout('30000');
|
||||
setUrl('');
|
||||
setMethod('POST');
|
||||
setHeaders('');
|
||||
setBody('');
|
||||
}
|
||||
}
|
||||
}, [open, editingHook]);
|
||||
|
||||
const handleSave = () => {
|
||||
const hook: EventHook = {
|
||||
id: editingHook?.id || crypto.randomUUID(),
|
||||
name: name.trim() || undefined,
|
||||
trigger,
|
||||
enabled: editingHook?.enabled ?? true,
|
||||
action:
|
||||
actionType === 'shell'
|
||||
? {
|
||||
type: 'shell',
|
||||
command,
|
||||
timeout: parseInt(timeout, 10) || 30000,
|
||||
}
|
||||
: {
|
||||
type: 'http',
|
||||
url,
|
||||
method,
|
||||
headers: headers.trim() ? JSON.parse(headers) : undefined,
|
||||
body: body.trim() || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
onSave(hook);
|
||||
};
|
||||
|
||||
const isValid = actionType === 'shell' ? command.trim().length > 0 : url.trim().length > 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingHook ? 'Edit Event Hook' : 'Add Event Hook'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure an action to run when a specific event occurs.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Name (optional) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hook-name">Name (optional)</Label>
|
||||
<Input
|
||||
id="hook-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My notification hook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Trigger selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hook-trigger">Trigger Event</Label>
|
||||
<Select value={trigger} onValueChange={(v) => setTrigger(v as EventHookTrigger)}>
|
||||
<SelectTrigger id="hook-trigger">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TRIGGER_OPTIONS.map((t) => (
|
||||
<SelectItem key={t} value={t}>
|
||||
{EVENT_HOOK_TRIGGER_LABELS[t]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Action type tabs */}
|
||||
<div className="space-y-2">
|
||||
<Label>Action Type</Label>
|
||||
<Tabs value={actionType} onValueChange={(v) => setActionType(v as ActionType)}>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="shell" className="flex-1 gap-2">
|
||||
<Terminal className="w-4 h-4" />
|
||||
Shell Command
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="http" className="flex-1 gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
HTTP Request
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Shell command form */}
|
||||
<TabsContent value="shell" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="shell-command">Command</Label>
|
||||
<Textarea
|
||||
id="shell-command"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder='echo "Feature {{featureId}} completed!"'
|
||||
className="font-mono text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use {'{{variable}}'} syntax for dynamic values
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="shell-timeout">Timeout (ms)</Label>
|
||||
<Input
|
||||
id="shell-timeout"
|
||||
type="number"
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout(e.target.value)}
|
||||
placeholder="30000"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* HTTP request form */}
|
||||
<TabsContent value="http" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-url">URL</Label>
|
||||
<Input
|
||||
id="http-url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://api.example.com/webhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-method">Method</Label>
|
||||
<Select value={method} onValueChange={(v) => setMethod(v as EventHookHttpMethod)}>
|
||||
<SelectTrigger id="http-method">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{HTTP_METHODS.map((m) => (
|
||||
<SelectItem key={m} value={m}>
|
||||
{m}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-headers">Headers (JSON, optional)</Label>
|
||||
<Textarea
|
||||
id="http-headers"
|
||||
value={headers}
|
||||
onChange={(e) => setHeaders(e.target.value)}
|
||||
placeholder={'{\n "Authorization": "Bearer {{token}}"\n}'}
|
||||
className="font-mono text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="http-body">Body (JSON, optional)</Label>
|
||||
<Textarea
|
||||
id="http-body"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
placeholder={'{\n "feature": "{{featureId}}",\n "status": "{{eventType}}"\n}'}
|
||||
className="font-mono text-sm"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave empty for default body with all event context
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!isValid}>
|
||||
{editingHook ? 'Save Changes' : 'Add Hook'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Webhook, Plus, Trash2, Pencil, Terminal, Globe } from 'lucide-react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { EventHook, EventHookTrigger } from '@automaker/types';
|
||||
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
|
||||
import { EventHookDialog } from './event-hook-dialog';
|
||||
|
||||
export function EventHooksSection() {
|
||||
const { eventHooks, setEventHooks } = useAppStore();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingHook, setEditingHook] = useState<EventHook | null>(null);
|
||||
|
||||
const handleAddHook = () => {
|
||||
setEditingHook(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditHook = (hook: EventHook) => {
|
||||
setEditingHook(hook);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteHook = (hookId: string) => {
|
||||
setEventHooks(eventHooks.filter((h) => h.id !== hookId));
|
||||
};
|
||||
|
||||
const handleToggleHook = (hookId: string, enabled: boolean) => {
|
||||
setEventHooks(eventHooks.map((h) => (h.id === hookId ? { ...h, enabled } : h)));
|
||||
};
|
||||
|
||||
const handleSaveHook = (hook: EventHook) => {
|
||||
if (editingHook) {
|
||||
// Update existing
|
||||
setEventHooks(eventHooks.map((h) => (h.id === hook.id ? hook : h)));
|
||||
} else {
|
||||
// Add new
|
||||
setEventHooks([...eventHooks, hook]);
|
||||
}
|
||||
setDialogOpen(false);
|
||||
setEditingHook(null);
|
||||
};
|
||||
|
||||
// Group hooks by trigger type for better organization
|
||||
const hooksByTrigger = eventHooks.reduce(
|
||||
(acc, hook) => {
|
||||
if (!acc[hook.trigger]) {
|
||||
acc[hook.trigger] = [];
|
||||
}
|
||||
acc[hook.trigger].push(hook);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<EventHookTrigger, EventHook[]>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<Webhook className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Event Hooks</h2>
|
||||
<p className="text-sm text-muted-foreground/80">
|
||||
Run custom commands or webhooks when events occur
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleAddHook} size="sm" className="gap-2">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Hook
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{eventHooks.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Webhook className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm">No event hooks configured</p>
|
||||
<p className="text-xs mt-1">
|
||||
Add hooks to run commands or send webhooks when features complete
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Group by trigger type */}
|
||||
{Object.entries(hooksByTrigger).map(([trigger, hooks]) => (
|
||||
<div key={trigger} className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
{EVENT_HOOK_TRIGGER_LABELS[trigger as EventHookTrigger]}
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{hooks.map((hook) => (
|
||||
<HookCard
|
||||
key={hook.id}
|
||||
hook={hook}
|
||||
onEdit={() => handleEditHook(hook)}
|
||||
onDelete={() => handleDeleteHook(hook.id)}
|
||||
onToggle={(enabled) => handleToggleHook(hook.id, enabled)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Variable reference */}
|
||||
<div className="px-6 pb-6">
|
||||
<div className="rounded-lg bg-muted/30 p-4 text-xs text-muted-foreground">
|
||||
<p className="font-medium mb-2">Available variables:</p>
|
||||
<code className="text-[10px] leading-relaxed">
|
||||
{'{{featureId}}'} {'{{featureName}}'} {'{{projectPath}}'} {'{{projectName}}'}{' '}
|
||||
{'{{error}}'} {'{{timestamp}}'} {'{{eventType}}'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dialog */}
|
||||
<EventHookDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
editingHook={editingHook}
|
||||
onSave={handleSaveHook}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface HookCardProps {
|
||||
hook: EventHook;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
function HookCard({ hook, onEdit, onDelete, onToggle }: HookCardProps) {
|
||||
const isShell = hook.action.type === 'shell';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-3 rounded-lg border',
|
||||
'bg-background/50 hover:bg-background/80 transition-colors',
|
||||
!hook.enabled && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
{/* Type icon */}
|
||||
<div
|
||||
className={cn(
|
||||
'w-8 h-8 rounded-lg flex items-center justify-center',
|
||||
isShell ? 'bg-amber-500/10 text-amber-500' : 'bg-blue-500/10 text-blue-500'
|
||||
)}
|
||||
>
|
||||
{isShell ? <Terminal className="w-4 h-4" /> : <Globe className="w-4 h-4" />}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{hook.name || (isShell ? 'Shell Command' : 'HTTP Webhook')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{isShell
|
||||
? (hook.action as { type: 'shell'; command: string }).command
|
||||
: (hook.action as { type: 'http'; url: string }).url}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={hook.enabled} onCheckedChange={onToggle} />
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onEdit}>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { EventHooksSection } from './event-hooks-section';
|
||||
@@ -15,10 +15,12 @@ export type SettingsViewId =
|
||||
| 'terminal'
|
||||
| 'keyboard'
|
||||
| 'audio'
|
||||
| 'event-hooks'
|
||||
| 'defaults'
|
||||
| 'worktrees'
|
||||
| 'account'
|
||||
| 'security'
|
||||
| 'developer'
|
||||
| 'danger';
|
||||
|
||||
interface UseSettingsViewOptions {
|
||||
|
||||
Reference in New Issue
Block a user