2025-08-11 06:34:44 +00:00
|
|
|
import React, { useState } from 'react'
|
|
|
|
|
import { useEntities } from '../../contexts/EntityContext'
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
import {
|
2025-08-16 19:47:24 +00:00
|
|
|
FaBook,
|
|
|
|
|
FaSearch,
|
|
|
|
|
FaFilter,
|
|
|
|
|
FaGlobe,
|
|
|
|
|
FaCopy,
|
|
|
|
|
FaCheckCircle,
|
|
|
|
|
FaExclamationCircle,
|
|
|
|
|
FaDatabase,
|
|
|
|
|
FaSyncAlt,
|
|
|
|
|
FaPaperPlane,
|
|
|
|
|
FaPlusCircle,
|
|
|
|
|
FaEdit,
|
2025-10-31 09:20:39 +00:00
|
|
|
FaTrash,
|
|
|
|
|
} from 'react-icons/fa'
|
2025-08-11 06:34:44 +00:00
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
|
|
|
|
interface EndpointType {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
method: string
|
|
|
|
|
path: string
|
|
|
|
|
description?: string
|
|
|
|
|
type: 'generated'
|
|
|
|
|
operationType?: string
|
|
|
|
|
entityName?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface TestResult {
|
|
|
|
|
success: boolean
|
|
|
|
|
status: number
|
|
|
|
|
data?: unknown
|
|
|
|
|
error?: unknown
|
|
|
|
|
timestamp: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ParameterInput {
|
|
|
|
|
name: string
|
|
|
|
|
value: string
|
|
|
|
|
type: 'path' | 'query' | 'body'
|
|
|
|
|
required: boolean
|
|
|
|
|
description?: string
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 09:02:16 +00:00
|
|
|
const CrudEndpointManager: React.FC = () => {
|
2025-08-11 06:34:44 +00:00
|
|
|
const { generatedEndpoints } = useEntities()
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
|
|
|
const [filterMethod, setFilterMethod] = useState<'all' | 'GET' | 'POST' | 'PUT' | 'DELETE'>('all')
|
|
|
|
|
const [selectedEndpoint, setSelectedEndpoint] = useState<string | null>(null)
|
|
|
|
|
const [testResults, setTestResults] = useState<Record<string, TestResult>>({})
|
|
|
|
|
const [loadingEndpoints, setLoadingEndpoints] = useState<Set<string>>(new Set())
|
|
|
|
|
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
|
|
|
|
|
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
|
|
|
|
|
|
|
|
|
|
// Only show generated CRUD endpoints
|
|
|
|
|
const allEndpoints: EndpointType[] = [
|
|
|
|
|
...generatedEndpoints
|
|
|
|
|
.filter((e) => e.isActive)
|
|
|
|
|
.map((e) => ({
|
|
|
|
|
id: e.id,
|
|
|
|
|
name: `${e.entityName} ${e.operationType}`,
|
|
|
|
|
method: e.method,
|
|
|
|
|
path: e.path,
|
|
|
|
|
description: `${e.operationType} operation for ${e.entityName} entity`,
|
|
|
|
|
type: 'generated' as const,
|
|
|
|
|
operationType: e.operationType,
|
|
|
|
|
entityName: e.entityName,
|
|
|
|
|
})),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const filteredEndpoints = allEndpoints.filter((endpoint) => {
|
|
|
|
|
const matchesSearch =
|
|
|
|
|
endpoint.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
endpoint.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
(endpoint.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
|
|
|
|
|
|
|
|
const matchesMethodFilter = filterMethod === 'all' || endpoint.method === filterMethod
|
|
|
|
|
|
|
|
|
|
return matchesSearch && matchesMethodFilter
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getMethodColor = (method: string) => {
|
|
|
|
|
switch (method) {
|
|
|
|
|
case 'GET':
|
|
|
|
|
return 'bg-blue-100 text-blue-800 border-blue-200'
|
|
|
|
|
case 'POST':
|
|
|
|
|
return 'bg-green-100 text-green-800 border-green-200'
|
|
|
|
|
case 'PUT':
|
|
|
|
|
return 'bg-yellow-100 text-yellow-800 border-yellow-200'
|
|
|
|
|
case 'DELETE':
|
|
|
|
|
return 'bg-red-100 text-red-800 border-red-200'
|
|
|
|
|
default:
|
|
|
|
|
return 'bg-slate-100 text-slate-800 border-slate-200'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getResponseExample = (endpoint: EndpointType) => {
|
|
|
|
|
switch (endpoint.operationType) {
|
|
|
|
|
case 'GetList':
|
|
|
|
|
case 'GetPaged':
|
|
|
|
|
return {
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
|
|
|
name: 'Sample Item',
|
|
|
|
|
creationTime: '2024-01-15T10:30:00Z',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
totalCount: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
}
|
|
|
|
|
case 'GetById':
|
|
|
|
|
return {
|
|
|
|
|
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
|
|
|
name: 'Sample Item',
|
|
|
|
|
creationTime: '2024-01-15T10:30:00Z',
|
|
|
|
|
lastModificationTime: '2024-01-15T10:30:00Z',
|
|
|
|
|
}
|
|
|
|
|
case 'Create':
|
|
|
|
|
case 'Update':
|
|
|
|
|
return {
|
|
|
|
|
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
|
|
|
name: 'Sample Item',
|
|
|
|
|
creationTime: '2024-01-15T10:30:00Z',
|
|
|
|
|
lastModificationTime: '2024-01-15T10:30:00Z',
|
|
|
|
|
}
|
|
|
|
|
case 'Delete':
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: 'Item deleted successfully',
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return {
|
|
|
|
|
message: 'Hello from API!',
|
|
|
|
|
timestamp: '2024-01-15T10:30:00Z',
|
|
|
|
|
success: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getRequestExample = (endpoint: EndpointType) => {
|
|
|
|
|
if (endpoint.operationType === 'Create' || endpoint.operationType === 'Update') {
|
|
|
|
|
return {
|
|
|
|
|
Name: 'New Item',
|
|
|
|
|
Description: 'Item description',
|
|
|
|
|
IsActive: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
|
|
|
navigator.clipboard.writeText(text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get required parameters for each endpoint type
|
|
|
|
|
const getEndpointParameters = (endpoint: EndpointType): ParameterInput[] => {
|
|
|
|
|
const parameters: ParameterInput[] = []
|
|
|
|
|
const currentValues = parameterValues[endpoint.id] || {}
|
|
|
|
|
|
|
|
|
|
switch (endpoint.operationType) {
|
|
|
|
|
case 'GetById':
|
|
|
|
|
case 'Update':
|
|
|
|
|
case 'Delete':
|
|
|
|
|
parameters.push({
|
|
|
|
|
name: 'id',
|
|
|
|
|
value: currentValues.id || '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
|
|
|
type: 'path',
|
|
|
|
|
required: true,
|
|
|
|
|
description: 'Unique identifier for the entity',
|
|
|
|
|
})
|
|
|
|
|
break
|
|
|
|
|
case 'GetList':
|
|
|
|
|
case 'GetPaged':
|
|
|
|
|
parameters.push(
|
|
|
|
|
{
|
|
|
|
|
name: 'SkipCount',
|
|
|
|
|
value: currentValues.SkipCount || '0',
|
|
|
|
|
type: 'query',
|
|
|
|
|
required: false,
|
|
|
|
|
description: 'Number of records to skip',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'MaxResultCount',
|
|
|
|
|
value: currentValues.MaxResultCount || '10',
|
|
|
|
|
type: 'query',
|
|
|
|
|
required: false,
|
|
|
|
|
description: 'Maximum number of records to return',
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parameters
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if endpoint needs request body
|
|
|
|
|
const needsRequestBody = (endpoint: EndpointType): boolean => {
|
|
|
|
|
return endpoint.operationType === 'Create' || endpoint.operationType === 'Update'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update parameter value
|
|
|
|
|
const updateParameterValue = (endpointId: string, paramName: string, value: string) => {
|
|
|
|
|
setParameterValues((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[endpointId]: {
|
|
|
|
|
...prev[endpointId],
|
|
|
|
|
[paramName]: value,
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update request body
|
|
|
|
|
const updateRequestBody = (endpointId: string, body: string) => {
|
|
|
|
|
setRequestBodies((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[endpointId]: body,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize default values for endpoint when first opened
|
|
|
|
|
const initializeEndpointDefaults = (endpoint: EndpointType) => {
|
|
|
|
|
if (!parameterValues[endpoint.id]) {
|
|
|
|
|
const parameters = getEndpointParameters(endpoint)
|
|
|
|
|
const defaultValues: Record<string, string> = {}
|
|
|
|
|
|
|
|
|
|
parameters.forEach((param) => {
|
|
|
|
|
defaultValues[param.name] = param.value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
setParameterValues((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[endpoint.id]: defaultValues,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!requestBodies[endpoint.id] && needsRequestBody(endpoint)) {
|
|
|
|
|
const example = getRequestExample(endpoint)
|
|
|
|
|
if (example) {
|
|
|
|
|
setRequestBodies((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[endpoint.id]: JSON.stringify(example, null, 2),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get current request body for endpoint
|
|
|
|
|
const getCurrentRequestBody = (endpoint: EndpointType): string => {
|
|
|
|
|
const stored = requestBodies[endpoint.id]
|
|
|
|
|
if (stored) return stored
|
|
|
|
|
|
|
|
|
|
const example = getRequestExample(endpoint)
|
|
|
|
|
return example ? JSON.stringify(example, null, 2) : ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const testEndpoint = async (endpoint: EndpointType) => {
|
|
|
|
|
const endpointId = endpoint.id
|
|
|
|
|
|
|
|
|
|
// Add to loading set
|
|
|
|
|
setLoadingEndpoints((prev) => new Set(prev).add(endpointId))
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let url = ''
|
|
|
|
|
const method = endpoint.method
|
|
|
|
|
let data = null
|
|
|
|
|
|
2025-11-05 09:02:16 +00:00
|
|
|
// For generated endpoints, use the Crud API
|
|
|
|
|
url = `${import.meta.env.VITE_API_URL}/api/app/crudendpoint/${endpoint.entityName}`
|
2025-08-11 06:34:44 +00:00
|
|
|
|
|
|
|
|
// Get parameters and modify URL based on operation type
|
|
|
|
|
const parameters = getEndpointParameters(endpoint)
|
|
|
|
|
const pathParams = parameters.filter((p) => p.type === 'path')
|
|
|
|
|
const queryParams = parameters.filter((p) => p.type === 'query')
|
|
|
|
|
|
|
|
|
|
// Handle path parameters
|
|
|
|
|
if (pathParams.length > 0) {
|
|
|
|
|
const idParam = pathParams.find((p) => p.name === 'id')
|
|
|
|
|
if (idParam) {
|
|
|
|
|
url += `/${idParam.value}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle query parameters
|
|
|
|
|
if (queryParams.length > 0) {
|
|
|
|
|
const queryString = queryParams
|
|
|
|
|
.filter((p) => p.value.trim() !== '')
|
|
|
|
|
.map((p) => `${p.name}=${encodeURIComponent(p.value)}`)
|
|
|
|
|
.join('&')
|
|
|
|
|
if (queryString) {
|
|
|
|
|
url += `?${queryString}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle request body
|
|
|
|
|
if (needsRequestBody(endpoint)) {
|
|
|
|
|
const requestBodyText = getCurrentRequestBody(endpoint)
|
|
|
|
|
try {
|
|
|
|
|
data = requestBodyText ? JSON.parse(requestBodyText) : getRequestExample(endpoint)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
throw new Error(`Invalid JSON in request body: ${e}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
method,
|
|
|
|
|
url,
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
data: data || undefined,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await axios(config)
|
|
|
|
|
|
|
|
|
|
setTestResults((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[endpointId]: {
|
|
|
|
|
success: true,
|
|
|
|
|
status: response.status,
|
|
|
|
|
data: response.data,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
const axiosError = error as {
|
|
|
|
|
response?: { status?: number; data?: unknown }
|
|
|
|
|
message?: string
|
|
|
|
|
}
|
|
|
|
|
setTestResults((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[endpointId]: {
|
|
|
|
|
success: false,
|
|
|
|
|
status: axiosError.response?.status || 0,
|
|
|
|
|
error: axiosError.response?.data || axiosError.message,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
} finally {
|
|
|
|
|
// Remove from loading set
|
|
|
|
|
setLoadingEndpoints((prev) => {
|
|
|
|
|
const newSet = new Set(prev)
|
|
|
|
|
newSet.delete(endpointId)
|
|
|
|
|
return newSet
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stats = {
|
|
|
|
|
total: allEndpoints.length,
|
|
|
|
|
custom: 0, // No more custom endpoints
|
|
|
|
|
generated: generatedEndpoints.filter((e) => e.isActive).length,
|
|
|
|
|
byMethod: {
|
|
|
|
|
GET: allEndpoints.filter((e) => e.method === 'GET').length,
|
|
|
|
|
POST: allEndpoints.filter((e) => e.method === 'POST').length,
|
|
|
|
|
PUT: allEndpoints.filter((e) => e.method === 'PUT').length,
|
|
|
|
|
DELETE: allEndpoints.filter((e) => e.method === 'DELETE').length,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-30 21:38:51 +00:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
2025-08-11 06:34:44 +00:00
|
|
|
<div>
|
2025-10-30 21:38:51 +00:00
|
|
|
<h1 className="text-2xl font-bold text-slate-900">
|
2025-11-05 09:02:16 +00:00
|
|
|
{translate('::App.DeveloperKit.CrudEndpoints')}
|
2025-08-11 06:34:44 +00:00
|
|
|
</h1>
|
2025-10-31 09:20:39 +00:00
|
|
|
<p className="text-slate-600">{translate('::App.DeveloperKit.Endpoint.Description')}</p>
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="flex items-center gap-2 bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm font-medium">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaGlobe className="w-4 h-4" />
|
2025-08-11 06:34:44 +00:00
|
|
|
{translate('::App.DeveloperKit.Endpoint.SwaggerCompatible')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Stats Cards */}
|
2025-10-31 09:20:39 +00:00
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-4">
|
2025-08-11 06:34:44 +00:00
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-slate-600">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.GeneratedCrud')}
|
|
|
|
|
</p>
|
2025-10-31 09:20:39 +00:00
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.generated}</p>
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="bg-emerald-100 text-emerald-600 p-3 rounded-lg">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaDatabase className="w-6 h-6" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-slate-600">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.GetCount')}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.GET}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-blue-100 text-blue-600 p-3 rounded-lg">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaBook className="w-6 h-6" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-slate-600">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.PostCount')}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.POST}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaPlusCircle className="w-6 h-6" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-slate-600">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.PutCount')}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.PUT}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaEdit className="w-6 h-6" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-slate-600">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.DeleteCount')}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.DELETE}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaTrash className="w-6 h-6" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Filters */}
|
2025-10-31 09:20:39 +00:00
|
|
|
<div className="flex flex-col lg:flex-row gap-4">
|
|
|
|
|
<div className="flex-1 relative">
|
|
|
|
|
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder={translate('::App.DeveloperKit.Endpoint.SearchPlaceholder')}
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<FaFilter className="w-5 h-5 text-slate-500" />
|
|
|
|
|
<select
|
|
|
|
|
value={filterMethod}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setFilterMethod(e.target.value as 'all' | 'GET' | 'POST' | 'PUT' | 'DELETE')
|
|
|
|
|
}
|
|
|
|
|
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<option value="all">{translate('::App.DeveloperKit.Endpoint.AllMethods')}</option>
|
|
|
|
|
<option value="GET">GET</option>
|
|
|
|
|
<option value="POST">POST</option>
|
|
|
|
|
<option value="PUT">PUT</option>
|
|
|
|
|
<option value="DELETE">DELETE</option>
|
|
|
|
|
</select>
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Endpoints List */}
|
|
|
|
|
{filteredEndpoints.length > 0 ? (
|
2025-10-31 09:20:39 +00:00
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
2025-08-11 06:34:44 +00:00
|
|
|
{filteredEndpoints.map((endpoint) => (
|
|
|
|
|
<div
|
|
|
|
|
key={endpoint.id}
|
|
|
|
|
className="bg-white rounded-lg border border-slate-200 shadow-sm"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="p-6 cursor-pointer hover:bg-slate-50 transition-colors"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const newSelectedEndpoint = selectedEndpoint === endpoint.id ? null : endpoint.id
|
|
|
|
|
setSelectedEndpoint(newSelectedEndpoint)
|
|
|
|
|
if (newSelectedEndpoint === endpoint.id) {
|
|
|
|
|
initializeEndpointDefaults(endpoint)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between">
|
2025-10-31 09:20:39 +00:00
|
|
|
{/* Sol taraf */}
|
2025-08-11 06:34:44 +00:00
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<span
|
|
|
|
|
className={`px-3 py-1 text-sm font-medium rounded-full border ${getMethodColor(
|
|
|
|
|
endpoint.method,
|
|
|
|
|
)}`}
|
|
|
|
|
>
|
|
|
|
|
{endpoint.method}
|
|
|
|
|
</span>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-slate-900">{endpoint.name}</h3>
|
|
|
|
|
<code className="text-sm bg-slate-100 text-slate-700 px-2 py-1 rounded">
|
|
|
|
|
{endpoint.path}
|
|
|
|
|
</code>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-31 09:20:39 +00:00
|
|
|
|
|
|
|
|
{/* Sağ taraf */}
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
{endpoint.type === 'generated' && (
|
|
|
|
|
<span className="bg-emerald-100 text-emerald-700 text-xs px-2 py-1 rounded-full">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.AutoGenerated')}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<FaCheckCircle className="w-5 h-5 text-green-500" />
|
|
|
|
|
<span className="text-sm text-slate-500">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.Active')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-31 09:20:39 +00:00
|
|
|
|
2025-08-11 06:34:44 +00:00
|
|
|
{endpoint.description && (
|
|
|
|
|
<p className="text-slate-600 text-sm mt-2">{endpoint.description}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Expanded Details */}
|
|
|
|
|
{selectedEndpoint === endpoint.id && (
|
|
|
|
|
<div className="border-t border-slate-200 p-6 bg-slate-50">
|
2025-10-31 09:20:39 +00:00
|
|
|
<div className="grid grid-cols-1 gap-6">
|
2025-08-11 06:34:44 +00:00
|
|
|
{/* Request Details */}
|
|
|
|
|
<div>
|
2025-10-31 09:20:39 +00:00
|
|
|
<h4 className="font-semibold text-slate-900 mb-3">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.RequestTitle')}
|
|
|
|
|
</h4>
|
2025-08-11 06:34:44 +00:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.UrlLabel')}
|
|
|
|
|
</label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<code className="flex-1 bg-white border border-slate-300 rounded px-3 py-2 text-sm">
|
|
|
|
|
{endpoint.method} {endpoint.path}
|
|
|
|
|
</code>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => copyToClipboard(`${endpoint.method} ${endpoint.path}`)}
|
|
|
|
|
className="p-2 text-slate-600 hover:text-slate-900 transition-colors"
|
|
|
|
|
title={translate('::App.DeveloperKit.Endpoint.CopyUrl')}
|
|
|
|
|
>
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaCopy className="w-4 h-4" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Parameters Section */}
|
|
|
|
|
{getEndpointParameters(endpoint).length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.ParametersLabel')}
|
|
|
|
|
</label>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{getEndpointParameters(endpoint).map((param) => (
|
|
|
|
|
<div
|
|
|
|
|
key={param.name}
|
|
|
|
|
className="bg-white border border-slate-300 rounded p-3"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<span className="text-sm font-medium text-slate-900">
|
|
|
|
|
{param.name}
|
|
|
|
|
</span>
|
|
|
|
|
<span
|
|
|
|
|
className={`text-xs px-2 py-1 rounded ${
|
|
|
|
|
param.type === 'path'
|
|
|
|
|
? 'bg-blue-100 text-blue-700'
|
|
|
|
|
: 'bg-green-100 text-green-700'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{param.type}
|
|
|
|
|
</span>
|
|
|
|
|
{param.required && (
|
|
|
|
|
<span className="text-xs px-2 py-1 rounded bg-red-100 text-red-700">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.RequiredLabel')}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{param.description && (
|
|
|
|
|
<p className="text-xs text-slate-600 mb-2">
|
|
|
|
|
{param.description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={param.value}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
updateParameterValue(endpoint.id, param.name, e.target.value)
|
|
|
|
|
}
|
|
|
|
|
placeholder={`Enter ${param.name}`}
|
|
|
|
|
className="w-full px-3 py-2 text-sm border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Request Body Section */}
|
|
|
|
|
{needsRequestBody(endpoint) && (
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.RequestBodyLabel')}
|
|
|
|
|
</label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<textarea
|
|
|
|
|
value={getCurrentRequestBody(endpoint)}
|
|
|
|
|
onChange={(e) => updateRequestBody(endpoint.id, e.target.value)}
|
2025-10-31 09:20:39 +00:00
|
|
|
placeholder={translate(
|
|
|
|
|
'::App.DeveloperKit.Endpoint.RequestBodyPlaceholder',
|
|
|
|
|
)}
|
2025-08-11 06:34:44 +00:00
|
|
|
rows={8}
|
|
|
|
|
className="w-full px-3 py-2 text-sm border border-slate-300 rounded font-mono focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => copyToClipboard(getCurrentRequestBody(endpoint))}
|
|
|
|
|
className="absolute top-2 right-2 p-1 text-slate-600 hover:text-slate-900 transition-colors"
|
|
|
|
|
title={translate('::App.DeveloperKit.Endpoint.CopyRequestBody')}
|
|
|
|
|
>
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaCopy className="w-3 h-3" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Response Details */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="font-semibold text-slate-900 mb-3">Response</h4>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.ResponseSuccessLabel')}
|
|
|
|
|
</label>
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<pre className="bg-white border border-slate-300 rounded p-3 text-sm overflow-x-auto">
|
|
|
|
|
{JSON.stringify(getResponseExample(endpoint), null, 2)}
|
|
|
|
|
</pre>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
copyToClipboard(
|
|
|
|
|
JSON.stringify(getResponseExample(endpoint), null, 2),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="absolute top-2 right-2 p-1 text-slate-600 hover:text-slate-900 transition-colors"
|
|
|
|
|
title={translate('::App.DeveloperKit.Endpoint.CopyResponse')}
|
|
|
|
|
>
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaCopy className="w-3 h-3" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Test Section */}
|
|
|
|
|
<div className="bg-white border border-slate-300 rounded p-4">
|
2025-10-31 09:20:39 +00:00
|
|
|
<h5 className="font-medium text-slate-900 mb-3">
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.TestSectionTitle')}
|
|
|
|
|
</h5>
|
2025-08-11 06:34:44 +00:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => testEndpoint(endpoint)}
|
|
|
|
|
disabled={loadingEndpoints.has(endpoint.id)}
|
|
|
|
|
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed transition-colors text-sm font-medium"
|
|
|
|
|
>
|
|
|
|
|
{loadingEndpoints.has(endpoint.id) ? (
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaSyncAlt className="w-4 h-4 animate-spin" />
|
2025-08-11 06:34:44 +00:00
|
|
|
) : (
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaPaperPlane className="w-4 h-4" />
|
2025-08-11 06:34:44 +00:00
|
|
|
)}
|
2025-10-31 09:20:39 +00:00
|
|
|
{loadingEndpoints.has(endpoint.id)
|
|
|
|
|
? translate('::App.DeveloperKit.Endpoint.SendLoading')
|
|
|
|
|
: translate('::App.DeveloperKit.Endpoint.SendRequest')}
|
2025-08-11 06:34:44 +00:00
|
|
|
</button>
|
|
|
|
|
{testResults[endpoint.id] && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setTestResults((prev) => {
|
|
|
|
|
const newResults = { ...prev }
|
|
|
|
|
delete newResults[endpoint.id]
|
|
|
|
|
return newResults
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
className="px-3 py-2 text-sm text-slate-600 hover:text-slate-900 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{translate('::App.DeveloperKit.Endpoint.ClearResult')}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Test Results */}
|
|
|
|
|
{testResults[endpoint.id] && (
|
|
|
|
|
<div className="mt-6 p-4 bg-slate-100 rounded-lg">
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
{testResults[endpoint.id].success ? (
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaCheckCircle className="w-5 h-5 text-green-500" />
|
2025-08-11 06:34:44 +00:00
|
|
|
) : (
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaExclamationCircle className="w-5 h-5 text-red-500" />
|
2025-08-11 06:34:44 +00:00
|
|
|
)}
|
|
|
|
|
<h5 className="font-semibold text-slate-900">
|
2025-10-31 09:20:39 +00:00
|
|
|
{translate('::App.DeveloperKit.Endpoint.TestResultLabel')} (
|
|
|
|
|
{testResults[endpoint.id].status})
|
2025-08-11 06:34:44 +00:00
|
|
|
</h5>
|
|
|
|
|
<span className="text-xs text-slate-500">
|
|
|
|
|
{new Date(testResults[endpoint.id].timestamp).toLocaleTimeString()}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<pre className="bg-white border border-slate-300 rounded p-3 text-sm overflow-x-auto max-h-96">
|
|
|
|
|
{JSON.stringify(
|
|
|
|
|
testResults[endpoint.id].success
|
|
|
|
|
? testResults[endpoint.id].data
|
|
|
|
|
: testResults[endpoint.id].error,
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
)}
|
|
|
|
|
</pre>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() =>
|
|
|
|
|
copyToClipboard(
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
testResults[endpoint.id].success
|
|
|
|
|
? testResults[endpoint.id].data
|
|
|
|
|
: testResults[endpoint.id].error,
|
|
|
|
|
null,
|
|
|
|
|
2,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className="absolute top-2 right-2 p-1 text-slate-600 hover:text-slate-900 transition-colors"
|
|
|
|
|
title={translate('::App.DeveloperKit.Endpoint.CopyResult')}
|
|
|
|
|
>
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaCopy className="w-3 h-3" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<div className="max-w-md mx-auto">
|
|
|
|
|
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
2025-08-16 19:47:24 +00:00
|
|
|
<FaBook className="w-8 h-8 text-slate-500" />
|
2025-08-11 06:34:44 +00:00
|
|
|
</div>
|
|
|
|
|
<h3 className="text-lg font-medium text-slate-900 mb-2">
|
|
|
|
|
{searchTerm || filterMethod !== 'all'
|
|
|
|
|
? translate('::App.DeveloperKit.Endpoint.EmptyFilteredTitle')
|
|
|
|
|
: translate('::App.DeveloperKit.Endpoint.EmptyInitialTitle')}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-slate-600">
|
|
|
|
|
{searchTerm || filterMethod !== 'all'
|
|
|
|
|
? translate('::App.DeveloperKit.Endpoint.EmptyFilteredDescription')
|
|
|
|
|
: translate('::App.DeveloperKit.Endpoint.EmptyInitialDescription')}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 09:02:16 +00:00
|
|
|
export default CrudEndpointManager
|