Skip to main content
Glama
TenantsPage.tsx21.1 kB
import React, { useState } from 'react'; import { Search, Filter, MoreHorizontal, Shield, AlertTriangle, Settings, Bell, Plus, X, CheckCircle } from 'lucide-react'; import AdminLayout from '@/components/layout/AdminLayout'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { formatDate } from '@/lib/utils'; import { useNavigate } from 'react-router-dom'; const mockTenants = [ { id: '1', name: 'TechCorp', plan: 'Pro', status: 'active', users: 12, bots: 8, messagesThisMonth: 9842, storageUsed: '45 MB', owner: 'John Smith', created: new Date('2023-11-15') }, { id: '2', name: 'MarketingPro', plan: 'Starter', status: 'active', users: 5, bots: 3, messagesThisMonth: 3241, storageUsed: '12 MB', owner: 'Sarah Johnson', created: new Date('2023-12-02') }, { id: '3', name: 'DesignMasters', plan: 'Enterprise', status: 'active', users: 45, bots: 15, messagesThisMonth: 21504, storageUsed: '120 MB', owner: 'Michael Brown', created: new Date('2023-10-10') }, { id: '4', name: 'EcomStore', plan: 'Free', status: 'pending', users: 1, bots: 1, messagesThisMonth: 154, storageUsed: '2 MB', owner: 'Emma Wilson', created: new Date('2024-01-05') }, { id: '5', name: 'HealthTech', plan: 'Starter', status: 'suspended', users: 8, bots: 2, messagesThisMonth: 0, storageUsed: '8 MB', owner: 'David Lee', created: new Date('2023-08-22') }, { id: '6', name: 'FinanceApp', plan: 'Pro', status: 'active', users: 15, bots: 5, messagesThisMonth: 7623, storageUsed: '32 MB', owner: 'Jessica Taylor', created: new Date('2023-09-30') }, { id: '7', name: 'EdTech Solutions', plan: 'Free', status: 'active', users: 3, bots: 1, messagesThisMonth: 947, storageUsed: '5 MB', owner: 'Robert Johnson', created: new Date('2024-01-15') }, { id: '8', name: 'GameDev Studio', plan: 'Starter', status: 'active', users: 7, bots: 2, messagesThisMonth: 2156, storageUsed: '18 MB', owner: 'Amanda Clark', created: new Date('2023-11-20') }, ]; // Add Tenant Modal Component const AddTenantModal = ({ isOpen, onClose, onAdd }: { isOpen: boolean; onClose: () => void; onAdd: (tenant: any) => void; }) => { const [formData, setFormData] = useState({ name: '', owner: '', email: '', plan: 'Free' }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const newTenant = { id: Math.random().toString(36).substr(2, 9), name: formData.name, owner: formData.owner, email: formData.email, plan: formData.plan, status: 'active', users: 1, bots: 0, messagesThisMonth: 0, storageUsed: '0 MB', created: new Date() }; onAdd(newTenant); setFormData({ name: '', owner: '', email: '', plan: 'Free' }); onClose(); }; if (!isOpen) return null; return ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="bg-white rounded-xl p-6 w-full max-w-md mx-4"> <div className="flex items-center justify-between mb-4"> <h3 className="text-lg font-semibold">Add New Tenant</h3> <button onClick={onClose}> <X className="h-5 w-5 text-gray-500" /> </button> </div> <form onSubmit={handleSubmit} className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Company Name</label> <input type="text" required value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Enter company name" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Owner Name</label> <input type="text" required value={formData.owner} onChange={(e) => setFormData({ ...formData, owner: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Enter owner name" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Email</label> <input type="email" required value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Enter email address" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Plan</label> <select value={formData.plan} onChange={(e) => setFormData({ ...formData, plan: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" > <option value="Free">Free</option> <option value="Starter">Starter</option> <option value="Pro">Pro</option> <option value="Enterprise">Enterprise</option> </select> </div> <div className="flex space-x-3 mt-6"> <Button type="button" variant="outline" onClick={onClose} className="flex-1"> Cancel </Button> <Button type="submit" className="flex-1"> Add Tenant </Button> </div> </form> </div> </div> ); }; // Action Menu Component const ActionMenu = ({ tenant, onView, onEdit, onDelete }: { tenant: any; onView: (id: string) => void; onEdit: (id: string) => void; onDelete: (id: string) => void; }) => { const [showMenu, setShowMenu] = useState(false); return ( <div className="relative"> <button onClick={() => setShowMenu(!showMenu)} className="text-gray-500 hover:text-gray-700 p-1" > <MoreHorizontal className="h-5 w-5" /> </button> {showMenu && ( <> <div className="absolute right-0 mt-1 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-10"> <div className="py-1"> <button onClick={() => { onView(tenant.id); setShowMenu(false); }} className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" > View Details </button> <button onClick={() => { onEdit(tenant.id); setShowMenu(false); }} className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" > Edit Tenant </button> <hr className="my-1" /> <button onClick={() => { onDelete(tenant.id); setShowMenu(false); }} className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50" > Delete Tenant </button> </div> </div> <div className="fixed inset-0 z-5" onClick={() => setShowMenu(false)} /> </> )} </div> ); }; const TenantsPage: React.FC = () => { const navigate = useNavigate(); const [searchTerm, setSearchTerm] = useState(''); const [filterStatus, setFilterStatus] = useState<string | null>(null); const [filterPlan, setFilterPlan] = useState<string | null>(null); const [showAddModal, setShowAddModal] = useState(false); const [showNotifications, setShowNotifications] = useState(false); const [tenants, setTenants] = useState(mockTenants); // Mock notifications for admin const notifications = [ { id: 1, title: 'New Tenant', message: 'GameDev Studio just signed up', time: '10 min ago', type: 'success' }, { id: 2, title: 'Suspension Alert', message: 'HealthTech account suspended due to payment issues', time: '1 hour ago', type: 'warning' }, { id: 3, title: 'Upgrade', message: 'TechCorp upgraded to Enterprise plan', time: '2 hours ago', type: 'info' } ]; const filteredTenants = tenants.filter(tenant => { // Apply search filter const matchesSearch = tenant.name.toLowerCase().includes(searchTerm.toLowerCase()) || tenant.owner.toLowerCase().includes(searchTerm.toLowerCase()); // Apply status filter const matchesStatus = filterStatus ? tenant.status === filterStatus : true; // Apply plan filter const matchesPlan = filterPlan ? tenant.plan === filterPlan : true; return matchesSearch && matchesStatus && matchesPlan; }); const handleAddTenant = (newTenant: any) => { setTenants([...tenants, newTenant]); }; const handleViewTenant = (tenantId: string) => { navigate(`/admin/tenants/${tenantId}`); }; const handleEditTenant = (tenantId: string) => { console.log('Edit tenant:', tenantId); }; const handleDeleteTenant = (tenantId: string) => { if (confirm('Are you sure you want to delete this tenant? This action cannot be undone.')) { setTenants(tenants.filter(t => t.id !== tenantId)); } }; const handleSettings = () => { navigate('/admin/settings'); }; return ( <AdminLayout> <div className="space-y-6"> {/* Modern Header */} <div className="bg-gradient-to-r from-white via-blue-50 to-indigo-50 border-b border-gray-100 shadow-soft -mx-6 px-6 py-6"> <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> <div className="space-y-3"> <div className="flex items-center space-x-4"> <h2 className="text-4xl font-bold bg-gradient-to-r from-gray-900 via-primary-600 to-purple-600 bg-clip-text text-transparent"> Tenants </h2> <div className="flex items-center bg-blue-100 text-blue-700 px-3 py-1.5 rounded-full shadow-sm"> <Shield className="h-4 w-4 mr-2" /> <span className="text-sm font-medium">{tenants.length} Organizations</span> </div> </div> <p className="text-lg text-gray-600">Manage all tenant organizations and their subscriptions.</p> <div className="flex items-center text-sm text-gray-500"> <span>Last updated: {new Date().toLocaleString()}</span> </div> </div> <div className="flex items-center space-x-3"> <Button onClick={handleSettings} variant="outline" className="btn-secondary" > <Settings className="h-4 w-4 mr-2" /> Settings </Button> <div className="relative"> <Button onClick={() => setShowNotifications(!showNotifications)} variant="outline" className="btn-secondary relative" > <Bell className="h-4 w-4 mr-2" /> Notifications {notifications.length > 0 && ( <div className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center"> {notifications.length} </div> )} </Button> {showNotifications && ( <div className="absolute right-0 mt-2 w-80 bg-white border border-gray-200 rounded-xl shadow-large z-50"> <div className="p-4 border-b border-gray-100"> <h3 className="font-semibold text-gray-900">Tenant Notifications</h3> </div> <div className="max-h-64 overflow-y-auto"> {notifications.map((notification) => ( <div key={notification.id} className="p-4 border-b border-gray-50 hover:bg-gray-50"> <div className="flex items-start space-x-3"> <div className={`w-2 h-2 rounded-full mt-2 ${ notification.type === 'success' ? 'bg-green-500' : notification.type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500' }`}></div> <div className="flex-1"> <p className="text-sm font-medium text-gray-900">{notification.title}</p> <p className="text-sm text-gray-600 mt-1">{notification.message}</p> <p className="text-xs text-gray-500 mt-2">{notification.time}</p> </div> </div> </div> ))} </div> </div> )} </div> <Button onClick={() => setShowAddModal(true)} variant="default" className="btn-modern" > <Plus className="h-4 w-4 mr-2" /> Add Tenant </Button> </div> </div> {/* Click outside to close notifications */} {showNotifications && ( <div className="fixed inset-0 z-40" onClick={() => setShowNotifications(false)} /> )} </div> <Card> <CardHeader> <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> <CardTitle>All Tenants</CardTitle> <div className="flex gap-2"> <div className="relative"> <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500" /> <input type="text" placeholder="Search tenants..." className="pl-8 pr-4 py-2 border rounded-md w-full focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} /> </div> <div className="relative inline-block"> <select className="appearance-none pl-4 pr-8 py-2 border rounded-md w-full bg-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" value={filterStatus || ''} onChange={(e) => setFilterStatus(e.target.value || null)} > <option value="">All Status</option> <option value="active">Active</option> <option value="pending">Pending</option> <option value="suspended">Suspended</option> </select> <Filter className="absolute right-2.5 top-2.5 h-4 w-4 text-gray-500 pointer-events-none" /> </div> <div className="relative inline-block"> <select className="appearance-none pl-4 pr-8 py-2 border rounded-md w-full bg-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" value={filterPlan || ''} onChange={(e) => setFilterPlan(e.target.value || null)} > <option value="">All Plans</option> <option value="Free">Free</option> <option value="Starter">Starter</option> <option value="Pro">Pro</option> <option value="Enterprise">Enterprise</option> </select> <Filter className="absolute right-2.5 top-2.5 h-4 w-4 text-gray-500 pointer-events-none" /> </div> </div> </div> </CardHeader> <CardContent> <div className="overflow-x-auto"> <table className="w-full text-sm"> <thead> <tr className="bg-gray-50"> <th className="px-4 py-3 text-left">Tenant</th> <th className="px-4 py-3 text-left">Plan</th> <th className="px-4 py-3 text-left">Status</th> <th className="px-4 py-3 text-center">Users</th> <th className="px-4 py-3 text-center">Bots</th> <th className="px-4 py-3 text-right">Messages</th> <th className="px-4 py-3 text-right">Storage</th> <th className="px-4 py-3 text-left">Created</th> <th className="px-4 py-3 text-center">Actions</th> </tr> </thead> <tbody className="divide-y divide-gray-200"> {filteredTenants.map((tenant) => ( <tr key={tenant.id} className="hover:bg-gray-50"> <td className="px-4 py-4"> <div className="flex items-center"> <div className="flex-shrink-0 h-8 w-8 bg-primary/10 rounded-md flex items-center justify-center text-primary"> {tenant.status === 'suspended' ? <AlertTriangle className="h-4 w-4" /> : <Shield className="h-4 w-4" /> } </div> <div className="ml-3"> <p className="font-medium text-gray-900 hover:text-primary-600 cursor-pointer transition-colors" onClick={() => handleViewTenant(tenant.id)} > {tenant.name} </p> <p className="text-xs text-gray-500">{tenant.owner}</p> </div> </div> </td> <td className="px-4 py-4"> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ tenant.plan === 'Free' ? 'bg-gray-100 text-gray-800' : tenant.plan === 'Starter' ? 'bg-blue-100 text-blue-800' : tenant.plan === 'Pro' ? 'bg-purple-100 text-purple-800' : 'bg-green-100 text-green-800' }`}> {tenant.plan} </span> </td> <td className="px-4 py-4"> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ tenant.status === 'active' ? 'bg-green-100 text-green-800' : tenant.status === 'suspended' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800' }`}> {tenant.status.charAt(0).toUpperCase() + tenant.status.slice(1)} </span> </td> <td className="px-4 py-4 text-center">{tenant.users}</td> <td className="px-4 py-4 text-center">{tenant.bots}</td> <td className="px-4 py-4 text-right">{tenant.messagesThisMonth.toLocaleString()}</td> <td className="px-4 py-4 text-right">{tenant.storageUsed}</td> <td className="px-4 py-4">{formatDate(tenant.created)}</td> <td className="px-4 py-4 text-center"> <ActionMenu tenant={tenant} onView={handleViewTenant} onEdit={handleEditTenant} onDelete={handleDeleteTenant} /> </td> </tr> ))} </tbody> </table> {filteredTenants.length === 0 && ( <div className="text-center py-8"> <p className="text-gray-500">No tenants found matching your filters.</p> </div> )} </div> </CardContent> </Card> </div> {/* Add Tenant Modal */} <AddTenantModal isOpen={showAddModal} onClose={() => setShowAddModal(false)} onAdd={handleAddTenant} /> </AdminLayout> ); }; export default TenantsPage;

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/ChiragPatankar/MCP'

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