Skip to main content
Glama
ApprovalQueue.tsx8.61 kB
import { useState, useEffect } from 'react'; import { approvalsApi } from '../api/client'; export default function ApprovalQueue() { const [approvals, setApprovals] = useState<any[]>([]); const [loading, setLoading] = useState(true); const [showAll, setShowAll] = useState(false); const [actionModal, setActionModal] = useState<{ type: 'approve' | 'reject' | null; changeId: string; }>({ type: null, changeId: '' }); const [actionInput, setActionInput] = useState({ name: '', comment: '' }); useEffect(() => { fetchApprovals(); }, [showAll]); async function fetchApprovals() { setLoading(true); try { const res = showAll ? await approvalsApi.list() : await approvalsApi.listPending(); setApprovals(res.approvals); } catch (error) { console.error('Failed to fetch approvals:', error); } finally { setLoading(false); } } const handleApprove = async () => { if (!actionInput.name) return; try { await approvalsApi.approve( actionModal.changeId, actionInput.name, actionInput.comment || undefined ); setActionModal({ type: null, changeId: '' }); setActionInput({ name: '', comment: '' }); fetchApprovals(); } catch (error) { console.error('Failed to approve:', error); } }; const handleReject = async () => { if (!actionInput.name || !actionInput.comment) return; try { await approvalsApi.reject(actionModal.changeId, actionInput.name, actionInput.comment); setActionModal({ type: null, changeId: '' }); setActionInput({ name: '', comment: '' }); fetchApprovals(); } catch (error) { console.error('Failed to reject:', error); } }; const getStatusColor = (status: string) => { const colors: Record<string, string> = { draft: 'status-draft', pending_approval: 'status-pending', approved: 'status-approved', rejected: 'status-rejected', implementing: 'status-implementing', completed: 'status-completed', }; return colors[status] || 'status-draft'; }; if (loading) { return ( <div className="flex justify-center items-center h-64"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> ); } return ( <div className="space-y-6"> <div className="flex justify-between items-center"> <h2 className="text-2xl font-bold text-gray-900">Approval Queue</h2> <label className="flex items-center space-x-2 text-sm"> <input type="checkbox" checked={showAll} onChange={(e) => setShowAll(e.target.checked)} className="rounded border-gray-300" /> <span>Show all</span> </label> </div> {approvals.length === 0 ? ( <div className="bg-white rounded-lg shadow p-8 text-center text-gray-500"> {showAll ? 'No approval records found.' : 'No pending approvals.'} </div> ) : ( <div className="bg-white rounded-lg shadow overflow-hidden"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> Change </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> Status </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> Requested </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> Actions </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {approvals.map((approval) => ( <tr key={approval.changeId} className="hover:bg-gray-50"> <td className="px-6 py-4"> <div className="font-medium text-gray-900">{approval.changeId}</div> {approval.requestedBy && ( <div className="text-sm text-gray-500">by {approval.requestedBy}</div> )} </td> <td className="px-6 py-4"> <span className={`status-badge ${getStatusColor(approval.status)}`}> {approval.status.replace('_', ' ')} </span> </td> <td className="px-6 py-4 text-sm text-gray-500"> {approval.requestedAt ? new Date(approval.requestedAt).toLocaleString() : '-'} </td> <td className="px-6 py-4"> {approval.status === 'pending_approval' && ( <div className="flex space-x-2"> <button onClick={() => setActionModal({ type: 'approve', changeId: approval.changeId }) } className="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600" > Approve </button> <button onClick={() => setActionModal({ type: 'reject', changeId: approval.changeId }) } className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600" > Reject </button> </div> )} </td> </tr> ))} </tbody> </table> </div> )} {/* Action Modal */} {actionModal.type && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md"> <h3 className="text-lg font-semibold mb-4"> {actionModal.type === 'approve' ? 'Approve Change' : 'Reject Change'} </h3> <div className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Your Name </label> <input type="text" value={actionInput.name} onChange={(e) => setActionInput({ ...actionInput, name: e.target.value })} className="w-full border rounded-md px-3 py-2" placeholder="Enter your name" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> {actionModal.type === 'approve' ? 'Comment (optional)' : 'Reason (required)'} </label> <textarea value={actionInput.comment} onChange={(e) => setActionInput({ ...actionInput, comment: e.target.value })} className="w-full border rounded-md px-3 py-2" rows={3} placeholder={ actionModal.type === 'approve' ? 'Optional comment...' : 'Explain why this is rejected...' } /> </div> </div> <div className="flex justify-end space-x-3 mt-6"> <button onClick={() => { setActionModal({ type: null, changeId: '' }); setActionInput({ name: '', comment: '' }); }} className="px-4 py-2 border rounded-md hover:bg-gray-50" > Cancel </button> <button onClick={actionModal.type === 'approve' ? handleApprove : handleReject} className={`px-4 py-2 text-white rounded-md ${ actionModal.type === 'approve' ? 'bg-green-500 hover:bg-green-600' : 'bg-red-500 hover:bg-red-600' }`} > {actionModal.type === 'approve' ? 'Approve' : 'Reject'} </button> </div> </div> </div> )} </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Lumiaqian/openspec-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server