Files
drone-detector/management/src/components/TenantModal.jsx
2025-09-13 13:57:40 +02:00

792 lines
33 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { XMarkIcon, EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import toast from 'react-hot-toast'
const TenantModal = ({ isOpen, onClose, tenant = null, onSave }) => {
const [formData, setFormData] = useState({
name: '',
slug: '',
domain: '',
subscription_type: 'basic',
auth_provider: 'local',
is_active: true,
admin_email: '',
admin_phone: '',
billing_email: '',
auth_config: {
// Local auth config
allow_registration: true,
require_email_verification: false,
password_policy: {
min_length: 8,
require_uppercase: false,
require_lowercase: false,
require_numbers: false,
require_symbols: false
},
// SAML config
sso_url: '',
certificate: '',
issuer: '',
logout_url: '',
// OAuth config
client_id: '',
client_secret: '',
authorization_url: '',
token_url: '',
userinfo_url: '',
scopes: ['openid', 'profile', 'email'],
// LDAP config
url: '',
base_dn: '',
bind_dn: '',
bind_password: '',
user_search_filter: '(sAMAccountName={username})',
domain_name: ''
},
user_mapping: {
username: ['preferred_username', 'username', 'user'],
email: ['email', 'mail'],
firstName: ['given_name', 'givenName', 'first_name'],
lastName: ['family_name', 'surname', 'last_name'],
phoneNumber: ['phone_number', 'phone']
},
role_mapping: {
admin: ['admin', 'administrator', 'Domain Admins'],
operator: ['operator', 'user', 'Users'],
viewer: ['viewer', 'guest', 'read-only'],
default: 'viewer'
},
features: {
max_devices: 50,
max_users: 10,
api_rate_limit: 5000,
data_retention_days: 365,
features: ['basic_detection', 'alerts', 'dashboard']
},
branding: {
logo_url: '',
primary_color: '#3B82F6',
secondary_color: '#1F2937',
company_name: ''
},
// IP Restriction settings
ip_restriction_enabled: false,
ip_whitelist: [],
ip_restriction_message: 'Access denied. Your IP address is not authorized to access this tenant.'
})
const [showSecrets, setShowSecrets] = useState(false)
const [loading, setLoading] = useState(false)
const [newIP, setNewIP] = useState('')
// Load tenant data when editing
useEffect(() => {
if (tenant) {
setFormData({
...formData,
...tenant,
auth_config: { ...formData.auth_config, ...tenant.auth_config },
user_mapping: { ...formData.user_mapping, ...tenant.user_mapping },
role_mapping: { ...formData.role_mapping, ...tenant.role_mapping },
features: { ...formData.features, ...tenant.features },
branding: { ...formData.branding, ...tenant.branding },
// IP restriction fields with fallbacks
ip_restriction_enabled: tenant.ip_restriction_enabled || false,
ip_whitelist: tenant.ip_whitelist || [],
ip_restriction_message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this tenant.'
})
}
}, [tenant])
// Auto-generate slug from name
const handleNameChange = (e) => {
const name = e.target.value
const slug = name.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim()
setFormData(prev => ({
...prev,
name,
slug: tenant ? prev.slug : slug // Don't auto-update slug when editing
}))
}
// IP Management functions
const addIPToWhitelist = () => {
if (!newIP.trim()) {
toast.error('Please enter an IP address or CIDR block')
return
}
// Basic validation for IP format
const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/(?:[0-9]|[1-2][0-9]|3[0-2]))?$|^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^\d{1,3}\.\d{1,3}\.\d{1,3}\.\*$/
if (!ipPattern.test(newIP.trim())) {
toast.error('Please enter a valid IP address, CIDR block (e.g., 192.168.1.0/24), or wildcard (e.g., 192.168.1.*)')
return
}
const ip = newIP.trim()
if (formData.ip_whitelist.includes(ip)) {
toast.error('This IP is already in the whitelist')
return
}
setFormData(prev => ({
...prev,
ip_whitelist: [...prev.ip_whitelist, ip]
}))
setNewIP('')
toast.success('IP added to whitelist')
}
const removeIPFromWhitelist = (ipToRemove) => {
setFormData(prev => ({
...prev,
ip_whitelist: prev.ip_whitelist.filter(ip => ip !== ipToRemove)
}))
toast.success('IP removed from whitelist')
}
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
try {
// Validate required fields
if (!formData.name || !formData.slug) {
toast.error('Name and slug are required')
return
}
// Clean up empty auth config fields
const cleanAuthConfig = Object.fromEntries(
Object.entries(formData.auth_config).filter(([_, value]) => {
if (typeof value === 'string') return value.trim() !== ''
if (Array.isArray(value)) return value.length > 0
if (typeof value === 'object') return Object.keys(value).length > 0
return value !== null && value !== undefined
})
)
const tenantData = {
...formData,
auth_config: cleanAuthConfig,
// Convert empty strings to null to avoid validation errors
domain: formData.domain?.trim() || null,
admin_email: formData.admin_email?.trim() || null,
billing_email: formData.billing_email?.trim() || null
}
await onSave(tenantData)
onClose()
toast.success(tenant ? 'Tenant updated successfully' : 'Tenant created successfully')
} catch (error) {
toast.error(error.message || 'Failed to save tenant')
} finally {
setLoading(false)
}
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-8 mx-auto p-5 border w-full max-w-4xl shadow-lg rounded-md bg-white mb-8">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-gray-900">
{tenant ? 'Edit Tenant' : 'Create New Tenant'}
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Basic Information */}
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="text-md font-medium text-gray-900 mb-4">Basic Information</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Organization Name *
</label>
<input
type="text"
value={formData.name}
onChange={handleNameChange}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="ACME Corporation"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Slug (Subdomain) *
</label>
<input
type="text"
value={formData.slug}
onChange={(e) => setFormData(prev => ({ ...prev, slug: e.target.value.toLowerCase() }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="acme"
pattern="^[a-z0-9-]+$"
required
/>
<p className="text-xs text-gray-500 mt-1">
Will be: https://{formData.slug || 'slug'}.dev.uggla.uamils.com
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Custom Domain
</label>
<input
type="text"
value={formData.domain}
onChange={(e) => setFormData(prev => ({ ...prev, domain: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="drones.acme.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Subscription Type
</label>
<select
value={formData.subscription_type}
onChange={(e) => setFormData(prev => ({ ...prev, subscription_type: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="free">Free</option>
<option value="basic">Basic</option>
<option value="premium">Premium</option>
<option value="enterprise">Enterprise</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Admin Email
</label>
<input
type="email"
value={formData.admin_email}
onChange={(e) => setFormData(prev => ({ ...prev, admin_email: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="admin@acme.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Billing Email
</label>
<input
type="email"
value={formData.billing_email}
onChange={(e) => setFormData(prev => ({ ...prev, billing_email: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="billing@acme.com"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="is_active"
checked={formData.is_active}
onChange={(e) => setFormData(prev => ({ ...prev, is_active: e.target.checked }))}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="is_active" className="ml-2 block text-sm text-gray-900">
Active Tenant
</label>
</div>
</div>
</div>
{/* Authentication Configuration */}
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="text-md font-medium text-gray-900 mb-4">Authentication</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Authentication Provider
</label>
<select
value={formData.auth_provider}
onChange={(e) => setFormData(prev => ({ ...prev, auth_provider: e.target.value }))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="local">Local (Username/Password)</option>
<option value="saml">SAML (Single Sign-On)</option>
<option value="oauth">OAuth (Azure AD, Google, etc.)</option>
<option value="ldap">LDAP (Active Directory)</option>
</select>
</div>
{/* Local Auth Config */}
{formData.auth_provider === 'local' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center">
<input
type="checkbox"
id="allow_registration"
checked={formData.auth_config.allow_registration}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, allow_registration: e.target.checked }
}))}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="allow_registration" className="ml-2 block text-sm text-gray-900">
Allow User Registration
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="require_email_verification"
checked={formData.auth_config.require_email_verification}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, require_email_verification: e.target.checked }
}))}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="require_email_verification" className="ml-2 block text-sm text-gray-900">
Require Email Verification
</label>
</div>
</div>
)}
{/* SAML Config */}
{formData.auth_provider === 'saml' && (
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SSO URL *
</label>
<input
type="url"
value={formData.auth_config.sso_url}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, sso_url: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://adfs.company.com/adfs/ls/"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Issuer *
</label>
<input
type="text"
value={formData.auth_config.issuer}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, issuer: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="urn:company:uav-detection"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
X.509 Certificate *
</label>
<textarea
value={formData.auth_config.certificate}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, certificate: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
rows="4"
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
/>
</div>
</div>
)}
{/* OAuth Config */}
{formData.auth_provider === 'oauth' && (
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Client ID *
</label>
<input
type="text"
value={formData.auth_config.client_id}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, client_id: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Client Secret *
</label>
<div className="relative">
<input
type={showSecrets ? "text" : "password"}
value={formData.auth_config.client_secret}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, client_secret: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => setShowSecrets(!showSecrets)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showSecrets ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Authorization URL *
</label>
<input
type="url"
value={formData.auth_config.authorization_url}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, authorization_url: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Token URL *
</label>
<input
type="url"
value={formData.auth_config.token_url}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, token_url: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token"
/>
</div>
</div>
)}
{/* LDAP Config */}
{formData.auth_provider === 'ldap' && (
<div className="grid grid-cols-1 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
LDAP URL *
</label>
<input
type="text"
value={formData.auth_config.url}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, url: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="ldaps://dc.company.com:636"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Base DN *
</label>
<input
type="text"
value={formData.auth_config.base_dn}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, base_dn: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="dc=company,dc=com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bind DN
</label>
<input
type="text"
value={formData.auth_config.bind_dn}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, bind_dn: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="cn=service,ou=accounts,dc=company,dc=com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bind Password
</label>
<input
type={showSecrets ? "text" : "password"}
value={formData.auth_config.bind_password}
onChange={(e) => setFormData(prev => ({
...prev,
auth_config: { ...prev.auth_config, bind_password: e.target.value }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
)}
</div>
</div>
{/* Features & Limits */}
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="text-md font-medium text-gray-900 mb-4">Features & Limits</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Max Devices
</label>
<input
type="number"
value={formData.features.max_devices}
onChange={(e) => setFormData(prev => ({
...prev,
features: { ...prev.features, max_devices: parseInt(e.target.value) || 0 }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Max Users
</label>
<input
type="number"
value={formData.features.max_users}
onChange={(e) => setFormData(prev => ({
...prev,
features: { ...prev.features, max_users: parseInt(e.target.value) || 0 }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
API Rate Limit
</label>
<input
type="number"
value={formData.features.api_rate_limit}
onChange={(e) => setFormData(prev => ({
...prev,
features: { ...prev.features, api_rate_limit: parseInt(e.target.value) || 0 }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Data Retention (Days)
</label>
<input
type="number"
value={formData.features.data_retention_days}
onChange={(e) => setFormData(prev => ({
...prev,
features: { ...prev.features, data_retention_days: parseInt(e.target.value) || 0 }
}))}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0"
/>
</div>
</div>
</div>
{/* IP Restriction Configuration */}
<div className="space-y-4">
<h4 className="text-md font-medium text-gray-900 border-b pb-2">IP Access Control</h4>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="ip_restriction_enabled"
checked={formData.ip_restriction_enabled}
onChange={(e) => setFormData(prev => ({
...prev,
ip_restriction_enabled: e.target.checked
}))}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="ip_restriction_enabled" className="text-sm font-medium text-gray-700">
Enable IP Restrictions
</label>
</div>
{formData.ip_restriction_enabled && (
<div className="space-y-4 p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600">
Only IPs in the whitelist below will be able to access this tenant. You can add individual IP addresses, CIDR blocks, or wildcards.
</p>
{/* Add IP Input */}
<div className="flex space-x-2">
<input
type="text"
placeholder="Enter IP address (e.g., 192.168.1.100, 10.0.0.0/24, or 192.168.1.*)"
value={newIP}
onChange={(e) => setNewIP(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
addIPToWhitelist()
}
}}
className="flex-1 border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={addIPToWhitelist}
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
Add IP
</button>
</div>
{/* IP Whitelist */}
{formData.ip_whitelist.length > 0 && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Allowed IP Addresses ({formData.ip_whitelist.length})
</label>
<div className="space-y-2 max-h-32 overflow-y-auto">
{formData.ip_whitelist.map((ip, index) => (
<div key={index} className="flex items-center justify-between bg-white px-3 py-2 rounded border">
<span className="text-sm font-mono">{ip}</span>
<button
type="button"
onClick={() => removeIPFromWhitelist(ip)}
className="text-red-600 hover:text-red-800 text-sm"
>
Remove
</button>
</div>
))}
</div>
</div>
)}
{/* Custom restriction message */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Access Denied Message
</label>
<textarea
value={formData.ip_restriction_message}
onChange={(e) => setFormData(prev => ({
...prev,
ip_restriction_message: e.target.value
}))}
rows={3}
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Message shown when access is denied due to IP restrictions"
/>
<p className="text-xs text-gray-500 mt-1">
This message will be shown to users whose IP is not in the whitelist.
</p>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
<div className="flex">
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">
Important Security Notes
</h3>
<div className="mt-1 text-sm text-yellow-700">
<ul className="list-disc list-inside space-y-1">
<li>Make sure to include your current IP to avoid being locked out</li>
<li>IP restrictions apply to all tenant access including login and API calls</li>
<li>Use CIDR notation for IP ranges (e.g., 192.168.1.0/24)</li>
<li>Use wildcards for partial matching (e.g., 192.168.1.*)</li>
</ul>
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* Form Actions */}
<div className="flex justify-end space-x-3 pt-6 border-t">
<button
type="button"
onClick={onClose}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
disabled={loading}
>
{loading ? 'Saving...' : (tenant ? 'Update Tenant' : 'Create Tenant')}
</button>
</div>
</form>
</div>
</div>
)
}
export default TenantModal