diff --git a/app/api/supervisor/config/group/route.ts b/app/api/supervisor/config/group/route.ts new file mode 100644 index 0000000..67f028f --- /dev/null +++ b/app/api/supervisor/config/group/route.ts @@ -0,0 +1,60 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +// POST - Add a process group +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { name } = body; + + if (!name) { + return NextResponse.json( + { error: 'Group name is required' }, + { status: 400 } + ); + } + + const client = createSupervisorClient(); + const result = await client.addProcessGroup(name); + + return NextResponse.json({ + success: result, + message: `Process group '${name}' added successfully`, + }); + } catch (error: any) { + console.error('Supervisor add process group error:', error); + return NextResponse.json( + { error: error.message || 'Failed to add process group' }, + { status: 500 } + ); + } +} + +// DELETE - Remove a process group +export async function DELETE(request: NextRequest) { + try { + const body = await request.json(); + const { name } = body; + + if (!name) { + return NextResponse.json( + { error: 'Group name is required' }, + { status: 400 } + ); + } + + const client = createSupervisorClient(); + const result = await client.removeProcessGroup(name); + + return NextResponse.json({ + success: result, + message: `Process group '${name}' removed successfully`, + }); + } catch (error: any) { + console.error('Supervisor remove process group error:', error); + return NextResponse.json( + { error: error.message || 'Failed to remove process group' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/config/reload/route.ts b/app/api/supervisor/config/reload/route.ts new file mode 100644 index 0000000..4ee3e39 --- /dev/null +++ b/app/api/supervisor/config/reload/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +// POST - Reload configuration +export async function POST() { + try { + const client = createSupervisorClient(); + const result = await client.reloadConfig(); + return NextResponse.json({ + success: true, + message: 'Configuration reloaded', + result, + }); + } catch (error: any) { + console.error('Supervisor reload config error:', error); + return NextResponse.json( + { error: error.message || 'Failed to reload configuration' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/config/route.ts b/app/api/supervisor/config/route.ts new file mode 100644 index 0000000..70330ce --- /dev/null +++ b/app/api/supervisor/config/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +// GET - Get all process configurations +export async function GET() { + try { + const client = createSupervisorClient(); + const configs = await client.getAllConfigInfo(); + return NextResponse.json(configs); + } catch (error: any) { + console.error('Supervisor get config error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch configuration' }, + { status: 500 } + ); + } +} diff --git a/app/config/page.tsx b/app/config/page.tsx index fe53395..34dc945 100644 --- a/app/config/page.tsx +++ b/app/config/page.tsx @@ -1,25 +1,76 @@ 'use client'; -import { Settings } from 'lucide-react'; -import { Card, CardContent } from '@/components/ui/card'; +import { useConfig } from '@/lib/hooks/useSupervisor'; +import { ConfigTable } from '@/components/config/ConfigTable'; +import { ReloadConfigButton } from '@/components/config/ReloadConfigButton'; +import { ProcessGroupForm } from '@/components/config/ProcessGroupForm'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { AlertCircle } from 'lucide-react'; export default function ConfigPage() { + const { data: configs, isLoading, isError } = useConfig(); + + if (isError) { + return ( +
+

Configuration

+ + + +

Failed to load configuration

+

+ Could not connect to Supervisor. Please check your configuration. +

+
+
+
+ ); + } + return (
-
-

Configuration

-

Manage Supervisor settings

+
+
+

Configuration

+

+ Manage process configurations and supervisor settings +

+
+
- - - -

Configuration Coming Soon

-

- This feature will allow you to reload configuration, add/remove process groups, and manage settings. + {/* Process Group Management */} + + + {/* Configuration Table */} +

+ + Process Configurations +

+ {configs?.length ?? 0} process{configs?.length !== 1 ? 'es' : ''} configured

- - +
+ + {isLoading ? ( + + +
+
+
+
+
+
+
+ ) : configs && configs.length > 0 ? ( + + ) : ( + + +

No process configurations found

+
+
+ )} +
); } diff --git a/components/config/ConfigTable.tsx b/components/config/ConfigTable.tsx new file mode 100644 index 0000000..bc94832 --- /dev/null +++ b/components/config/ConfigTable.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { useState } from 'react'; +import { ConfigInfo } from '@/lib/supervisor/types'; +import { Card, CardContent } from '@/components/ui/card'; +import { ChevronUp, ChevronDown } from 'lucide-react'; +import { cn } from '@/lib/utils/cn'; + +interface ConfigTableProps { + configs: ConfigInfo[]; +} + +type SortField = 'group' | 'name' | 'command' | 'autostart' | 'directory'; +type SortDirection = 'asc' | 'desc'; + +export function ConfigTable({ configs }: ConfigTableProps) { + const [sortField, setSortField] = useState('group'); + const [sortDirection, setSortDirection] = useState('asc'); + + const handleSort = (field: SortField) => { + if (sortField === field) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortDirection('asc'); + } + }; + + const sortedConfigs = [...configs].sort((a, b) => { + let aVal: any = a[sortField]; + let bVal: any = b[sortField]; + + // Handle boolean values + if (typeof aVal === 'boolean') { + aVal = aVal ? 1 : 0; + bVal = bVal ? 1 : 0; + } + + // Handle string values + if (typeof aVal === 'string') { + aVal = aVal.toLowerCase(); + bVal = bVal.toLowerCase(); + } + + if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; + if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; + return 0; + }); + + const SortIcon = ({ field }: { field: SortField }) => { + if (sortField !== field) return null; + return sortDirection === 'asc' ? ( + + ) : ( + + ); + }; + + return ( + + +
+ + + + + + + + + + + + + + {sortedConfigs.map((config, index) => ( + + + + + + + + + + ))} + +
handleSort('group')} + > + Group + handleSort('name')} + > + Name + handleSort('command')} + > + Command + Directory handleSort('autostart')} + > + Autostart + PriorityProcesses
{config.group}{config.name} + {config.command} + {config.directory} + + {config.priority}{config.numprocs}
+
+
+
+ ); +} diff --git a/components/config/ProcessGroupForm.tsx b/components/config/ProcessGroupForm.tsx new file mode 100644 index 0000000..ab63c7c --- /dev/null +++ b/components/config/ProcessGroupForm.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useState } from 'react'; +import { useAddProcessGroup, useRemoveProcessGroup } from '@/lib/hooks/useSupervisor'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Plus, Trash2 } from 'lucide-react'; + +export function ProcessGroupForm() { + const [groupName, setGroupName] = useState(''); + const [removeGroupName, setRemoveGroupName] = useState(''); + + const addMutation = useAddProcessGroup(); + const removeMutation = useRemoveProcessGroup(); + + const handleAdd = (e: React.FormEvent) => { + e.preventDefault(); + if (groupName.trim()) { + addMutation.mutate(groupName.trim()); + setGroupName(''); + } + }; + + const handleRemove = (e: React.FormEvent) => { + e.preventDefault(); + if (removeGroupName.trim()) { + if (confirm(`Are you sure you want to remove the process group "${removeGroupName}"?`)) { + removeMutation.mutate(removeGroupName.trim()); + setRemoveGroupName(''); + } + } + }; + + return ( +
+ + + Add Process Group + + +
+ setGroupName(e.target.value)} + disabled={addMutation.isPending} + className="flex-1" + /> + +
+
+
+ + + + Remove Process Group + + +
+ setRemoveGroupName(e.target.value)} + disabled={removeMutation.isPending} + className="flex-1" + /> + +
+
+
+
+ ); +} diff --git a/components/config/ReloadConfigButton.tsx b/components/config/ReloadConfigButton.tsx new file mode 100644 index 0000000..8998507 --- /dev/null +++ b/components/config/ReloadConfigButton.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { useReloadConfig } from '@/lib/hooks/useSupervisor'; +import { Button } from '@/components/ui/button'; +import { RefreshCw } from 'lucide-react'; +import { cn } from '@/lib/utils/cn'; + +export function ReloadConfigButton() { + const reloadMutation = useReloadConfig(); + + const handleReload = () => { + if (confirm('Are you sure you want to reload the configuration? This will apply any changes made to supervisord.conf.')) { + reloadMutation.mutate(); + } + }; + + return ( + + ); +} diff --git a/lib/hooks/useSupervisor.ts b/lib/hooks/useSupervisor.ts index 1cd0698..9c162d7 100644 --- a/lib/hooks/useSupervisor.ts +++ b/lib/hooks/useSupervisor.ts @@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; -import type { ProcessInfo, SystemInfo, LogTailResult } from '@/lib/supervisor/types'; +import type { ProcessInfo, SystemInfo, LogTailResult, ConfigInfo } from '@/lib/supervisor/types'; // Query Keys export const supervisorKeys = { @@ -12,6 +12,7 @@ export const supervisorKeys = { process: (name: string) => [...supervisorKeys.processes(), name] as const, logs: (name: string, type: 'stdout' | 'stderr') => [...supervisorKeys.process(name), 'logs', type] as const, + config: () => [...supervisorKeys.all, 'config'] as const, }; // API Client Functions @@ -456,3 +457,104 @@ export function useRestartAllProcesses() { }, }); } + +// Configuration Management + +async function fetchConfig(): Promise { + const response = await fetch('/api/supervisor/config'); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to fetch configuration'); + } + return response.json(); +} + +async function reloadConfig(): Promise<{ success: boolean; message: string; result: any }> { + const response = await fetch('/api/supervisor/config/reload', { + method: 'POST', + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to reload configuration'); + } + return response.json(); +} + +async function addProcessGroup(name: string): Promise<{ success: boolean; message: string }> { + const response = await fetch('/api/supervisor/config/group', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to add process group'); + } + return response.json(); +} + +async function removeProcessGroup(name: string): Promise<{ success: boolean; message: string }> { + const response = await fetch('/api/supervisor/config/group', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to remove process group'); + } + return response.json(); +} + +export function useConfig() { + return useQuery({ + queryKey: supervisorKeys.config(), + queryFn: fetchConfig, + refetchInterval: 10000, // Refetch every 10 seconds + }); +} + +export function useReloadConfig() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: reloadConfig, + onSuccess: (data) => { + toast.success(data.message); + queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); + }, + onError: (error: Error) => { + toast.error(`Failed to reload configuration: ${error.message}`); + }, + }); +} + +export function useAddProcessGroup() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (name: string) => addProcessGroup(name), + onSuccess: (data) => { + toast.success(data.message); + queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); + }, + onError: (error: Error) => { + toast.error(`Failed to add process group: ${error.message}`); + }, + }); +} + +export function useRemoveProcessGroup() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (name: string) => removeProcessGroup(name), + onSuccess: (data) => { + toast.success(data.message); + queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); + }, + onError: (error: Error) => { + toast.error(`Failed to remove process group: ${error.message}`); + }, + }); +}