erp-platform/ui/src/components/developerKit/CrudEndpointManager.tsx

795 lines
31 KiB
TypeScript
Raw Normal View History

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