generated from gc/template-java-maven
공통:
- 다크/라이트 모드 (ThemeContext, Tailwind dark variant, 전체 페이지 적용)
- 사이드바 아이콘 링크체인 (#FF2E63), 헤더/사이드바 높이 통일
- 컨텐츠 영역 max-w-7xl 마진 통일 (대시보드 제외)
- 전체 Actions 버튼 bg-color-100 스타일 통일
- date input 달력 아이콘 다크모드 (filter invert)
API Keys:
- Request: 영구 사용 옵션 추가, 프리셋/영구 버튼 다크모드
- My Keys: ADMIN 직접 생성 제거 → Request 페이지 정식 폼으로 통일
- Admin: 키 관리 만료일 컬럼 추가, 권한 편집 제거 (승인 단계에서만 가능)
Gateway:
- API 경로 {변수} 패턴 매칭 지원
Closes #15
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
576 lines
25 KiB
TypeScript
576 lines
25 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import type {
|
|
ServiceInfo,
|
|
ServiceApi,
|
|
CreateServiceRequest,
|
|
UpdateServiceRequest,
|
|
CreateServiceApiRequest,
|
|
} from '../../types/service';
|
|
import {
|
|
getServices,
|
|
createService,
|
|
updateService,
|
|
getServiceApis,
|
|
createServiceApi,
|
|
} from '../../services/serviceService';
|
|
|
|
const HEALTH_BADGE: Record<string, { dot: string; bg: string; text: string }> = {
|
|
UP: { dot: 'bg-green-500', bg: 'bg-green-100', text: 'text-green-800' },
|
|
DOWN: { dot: 'bg-red-500', bg: 'bg-red-100', text: 'text-red-800' },
|
|
UNKNOWN: { dot: 'bg-gray-400', bg: 'bg-gray-100', text: 'text-gray-800' },
|
|
};
|
|
|
|
const METHOD_COLOR: Record<string, string> = {
|
|
GET: 'bg-green-100 text-green-800',
|
|
POST: 'bg-blue-100 text-blue-800',
|
|
PUT: 'bg-orange-100 text-orange-800',
|
|
DELETE: 'bg-red-100 text-red-800',
|
|
};
|
|
|
|
const formatRelativeTime = (dateStr: string | null): string => {
|
|
if (!dateStr) return '-';
|
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
const seconds = Math.floor(diff / 1000);
|
|
if (seconds < 60) return `${seconds}초 전`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return `${minutes}분 전`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}시간 전`;
|
|
const days = Math.floor(hours / 24);
|
|
return `${days}일 전`;
|
|
};
|
|
|
|
const ServicesPage = () => {
|
|
const [services, setServices] = useState<ServiceInfo[]>([]);
|
|
const [selectedService, setSelectedService] = useState<ServiceInfo | null>(null);
|
|
const [serviceApis, setServiceApis] = useState<ServiceApi[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [isServiceModalOpen, setIsServiceModalOpen] = useState(false);
|
|
const [editingService, setEditingService] = useState<ServiceInfo | null>(null);
|
|
const [serviceCode, setServiceCode] = useState('');
|
|
const [serviceName, setServiceName] = useState('');
|
|
const [serviceUrl, setServiceUrl] = useState('');
|
|
const [serviceDescription, setServiceDescription] = useState('');
|
|
const [healthCheckUrl, setHealthCheckUrl] = useState('');
|
|
const [healthCheckInterval, setHealthCheckInterval] = useState(60);
|
|
const [serviceIsActive, setServiceIsActive] = useState(true);
|
|
|
|
const [isApiModalOpen, setIsApiModalOpen] = useState(false);
|
|
const [apiMethod, setApiMethod] = useState('GET');
|
|
const [apiPath, setApiPath] = useState('');
|
|
const [apiName, setApiName] = useState('');
|
|
const [apiDescription, setApiDescription] = useState('');
|
|
|
|
const fetchServices = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const res = await getServices();
|
|
if (res.success && res.data) {
|
|
setServices(res.data);
|
|
} else {
|
|
setError(res.message || '서비스 목록을 불러오는데 실패했습니다.');
|
|
}
|
|
} catch {
|
|
setError('서비스 목록을 불러오는데 실패했습니다.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchApis = async (serviceId: number) => {
|
|
try {
|
|
const res = await getServiceApis(serviceId);
|
|
if (res.success && res.data) {
|
|
setServiceApis(res.data);
|
|
}
|
|
} catch {
|
|
setServiceApis([]);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchServices();
|
|
}, []);
|
|
|
|
const handleSelectService = (service: ServiceInfo) => {
|
|
setSelectedService(service);
|
|
fetchApis(service.serviceId);
|
|
};
|
|
|
|
const handleOpenCreateService = () => {
|
|
setEditingService(null);
|
|
setServiceCode('');
|
|
setServiceName('');
|
|
setServiceUrl('');
|
|
setServiceDescription('');
|
|
setHealthCheckUrl('');
|
|
setHealthCheckInterval(60);
|
|
setServiceIsActive(true);
|
|
setIsServiceModalOpen(true);
|
|
};
|
|
|
|
const handleOpenEditService = (service: ServiceInfo) => {
|
|
setEditingService(service);
|
|
setServiceCode(service.serviceCode);
|
|
setServiceName(service.serviceName);
|
|
setServiceUrl(service.serviceUrl || '');
|
|
setServiceDescription(service.description || '');
|
|
setHealthCheckUrl(service.healthCheckUrl || '');
|
|
setHealthCheckInterval(service.healthCheckInterval);
|
|
setServiceIsActive(service.isActive);
|
|
setIsServiceModalOpen(true);
|
|
};
|
|
|
|
const handleCloseServiceModal = () => {
|
|
setIsServiceModalOpen(false);
|
|
setEditingService(null);
|
|
setError(null);
|
|
};
|
|
|
|
const handleServiceSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError(null);
|
|
|
|
try {
|
|
if (editingService) {
|
|
const req: UpdateServiceRequest = {
|
|
serviceName,
|
|
serviceUrl: serviceUrl || undefined,
|
|
description: serviceDescription || undefined,
|
|
healthCheckUrl: healthCheckUrl || undefined,
|
|
healthCheckInterval,
|
|
isActive: serviceIsActive,
|
|
};
|
|
const res = await updateService(editingService.serviceId, req);
|
|
if (!res.success) {
|
|
setError(res.message || '서비스 수정에 실패했습니다.');
|
|
return;
|
|
}
|
|
} else {
|
|
const req: CreateServiceRequest = {
|
|
serviceCode,
|
|
serviceName,
|
|
serviceUrl: serviceUrl || undefined,
|
|
description: serviceDescription || undefined,
|
|
healthCheckUrl: healthCheckUrl || undefined,
|
|
healthCheckInterval,
|
|
};
|
|
const res = await createService(req);
|
|
if (!res.success) {
|
|
setError(res.message || '서비스 생성에 실패했습니다.');
|
|
return;
|
|
}
|
|
}
|
|
handleCloseServiceModal();
|
|
await fetchServices();
|
|
} catch {
|
|
setError(editingService ? '서비스 수정에 실패했습니다.' : '서비스 생성에 실패했습니다.');
|
|
}
|
|
};
|
|
|
|
const handleOpenCreateApi = () => {
|
|
setApiMethod('GET');
|
|
setApiPath('');
|
|
setApiName('');
|
|
setApiDescription('');
|
|
setIsApiModalOpen(true);
|
|
};
|
|
|
|
const handleCloseApiModal = () => {
|
|
setIsApiModalOpen(false);
|
|
setError(null);
|
|
};
|
|
|
|
const handleApiSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedService) return;
|
|
setError(null);
|
|
|
|
try {
|
|
const req: CreateServiceApiRequest = {
|
|
apiMethod,
|
|
apiPath,
|
|
apiName,
|
|
description: apiDescription || undefined,
|
|
};
|
|
const res = await createServiceApi(selectedService.serviceId, req);
|
|
if (!res.success) {
|
|
setError(res.message || 'API 생성에 실패했습니다.');
|
|
return;
|
|
}
|
|
handleCloseApiModal();
|
|
await fetchApis(selectedService.serviceId);
|
|
} catch {
|
|
setError('API 생성에 실패했습니다.');
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="max-w-7xl mx-auto text-center py-10 text-gray-500 dark:text-gray-400">로딩 중...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Services</h1>
|
|
<button
|
|
onClick={handleOpenCreateService}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
Create Service
|
|
</button>
|
|
</div>
|
|
|
|
{error && !isServiceModalOpen && !isApiModalOpen && (
|
|
<div className="mb-4 p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
|
)}
|
|
|
|
<div className="overflow-x-auto bg-white dark:bg-gray-800 rounded-lg shadow mb-6">
|
|
<table className="w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Code</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Name</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">URL</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Health Status</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Response Time</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Last Checked</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Active</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{services.map((service) => {
|
|
const badge = HEALTH_BADGE[service.healthStatus] || HEALTH_BADGE.UNKNOWN;
|
|
const isSelected = selectedService?.serviceId === service.serviceId;
|
|
return (
|
|
<tr
|
|
key={service.serviceId}
|
|
onClick={() => handleSelectService(service)}
|
|
className={`cursor-pointer ${
|
|
isSelected ? 'bg-blue-50 dark:bg-blue-900/30' : 'hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
<td className="px-4 py-3 font-mono text-gray-900 dark:text-gray-100">{service.serviceCode}</td>
|
|
<td className="px-4 py-3 text-gray-900 dark:text-gray-100">{service.serviceName}</td>
|
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
|
|
{service.serviceUrl || '-'}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span
|
|
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${badge.bg} ${badge.text}`}
|
|
>
|
|
<span className={`w-2 h-2 rounded-full ${badge.dot}`} />
|
|
{service.healthStatus}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">
|
|
{service.healthResponseTime != null
|
|
? `${service.healthResponseTime}ms`
|
|
: '-'}
|
|
</td>
|
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">
|
|
{formatRelativeTime(service.healthCheckedAt)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
service.isActive
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-red-100 text-red-800'
|
|
}`}
|
|
>
|
|
{service.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleOpenEditService(service);
|
|
}}
|
|
className="bg-blue-100 hover:bg-blue-200 text-blue-700 dark:bg-blue-900/30 dark:hover:bg-blue-800/40 dark:text-blue-400 px-3 py-1 rounded-lg text-sm font-medium"
|
|
>
|
|
수정
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
{services.length === 0 && (
|
|
<tr>
|
|
<td colSpan={8} className="px-4 py-8 text-center text-gray-400 dark:text-gray-500">
|
|
등록된 서비스가 없습니다.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{selectedService && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
APIs for {selectedService.serviceName}
|
|
</h2>
|
|
<button
|
|
onClick={handleOpenCreateApi}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium"
|
|
>
|
|
Add API
|
|
</button>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Method</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Path</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Name</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Description</th>
|
|
<th className="px-4 py-3 text-left font-medium text-gray-500 dark:text-gray-400">Active</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{serviceApis.map((api) => (
|
|
<tr key={api.apiId} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<td className="px-4 py-3">
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded text-xs font-bold ${
|
|
METHOD_COLOR[api.apiMethod] || 'bg-gray-100 text-gray-800'
|
|
}`}
|
|
>
|
|
{api.apiMethod}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 font-mono text-gray-900 dark:text-gray-100">{api.apiPath}</td>
|
|
<td className="px-4 py-3 text-gray-900 dark:text-gray-100">{api.apiName}</td>
|
|
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">{api.description || '-'}</td>
|
|
<td className="px-4 py-3">
|
|
<span
|
|
className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${
|
|
api.isActive
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-red-100 text-red-800'
|
|
}`}
|
|
>
|
|
{api.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{serviceApis.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-gray-400 dark:text-gray-500">
|
|
등록된 API가 없습니다.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isServiceModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
{editingService ? '서비스 수정' : '서비스 생성'}
|
|
</h2>
|
|
</div>
|
|
<form onSubmit={handleServiceSubmit}>
|
|
<div className="px-6 py-4 space-y-4 max-h-[70vh] overflow-y-auto">
|
|
{error && (
|
|
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Service Code
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={serviceCode}
|
|
onChange={(e) => setServiceCode(e.target.value)}
|
|
disabled={!!editingService}
|
|
required
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:bg-gray-100 disabled:text-gray-500 dark:disabled:bg-gray-600 dark:disabled:text-gray-400"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Service Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={serviceName}
|
|
onChange={(e) => setServiceName(e.target.value)}
|
|
required
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Service URL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={serviceUrl}
|
|
onChange={(e) => setServiceUrl(e.target.value)}
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
value={serviceDescription}
|
|
onChange={(e) => setServiceDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Health Check URL
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={healthCheckUrl}
|
|
onChange={(e) => setHealthCheckUrl(e.target.value)}
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Health Check Interval (seconds)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={healthCheckInterval}
|
|
onChange={(e) => setHealthCheckInterval(Number(e.target.value))}
|
|
min={10}
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
{editingService && (
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="serviceIsActive"
|
|
checked={serviceIsActive}
|
|
onChange={(e) => setServiceIsActive(e.target.checked)}
|
|
className="rounded"
|
|
/>
|
|
<label htmlFor="serviceIsActive" className="text-sm text-gray-700 dark:text-gray-300">
|
|
Active
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleCloseServiceModal}
|
|
className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isApiModalOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">API 생성</h2>
|
|
</div>
|
|
<form onSubmit={handleApiSubmit}>
|
|
<div className="px-6 py-4 space-y-4">
|
|
{error && (
|
|
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Method</label>
|
|
<select
|
|
value={apiMethod}
|
|
onChange={(e) => setApiMethod(e.target.value)}
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="GET">GET</option>
|
|
<option value="POST">POST</option>
|
|
<option value="PUT">PUT</option>
|
|
<option value="DELETE">DELETE</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Path</label>
|
|
<input
|
|
type="text"
|
|
value={apiPath}
|
|
onChange={(e) => setApiPath(e.target.value)}
|
|
required
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Name</label>
|
|
<input
|
|
type="text"
|
|
value={apiName}
|
|
onChange={(e) => setApiName(e.target.value)}
|
|
required
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
value={apiDescription}
|
|
onChange={(e) => setApiDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleCloseApiModal}
|
|
className="bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ServicesPage;
|