sozsoft-platform/ui/src/views/developerKit/CrudEndpointManager.tsx
2026-03-18 08:36:36 +03:00

817 lines
36 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react'
import axios from 'axios'
import {
FaSearch,
FaGlobe,
FaCopy,
FaCheckCircle,
FaExclamationCircle,
FaDatabase,
FaSyncAlt,
FaPaperPlane,
FaTrash,
FaBolt,
FaTable,
FaToggleOn,
FaToggleOff,
FaChevronRight,
FaChevronDown,
FaServer,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { getDataSources } from '@/services/data-source.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { developerKitService } from '@/services/developerKit.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
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
}
// Helper: tableName -> PascalCase entity name
function toPascalCase(tableName: string): string {
const name = tableName.replace(/^.*\./g, '')
return name
.replace(/[_-]([a-z])/gi, (_: string, c: string) => c.toUpperCase())
.replace(/^([a-z])/, (c: string) => c.toUpperCase())
}
const METHOD_COLOR: Record<string, string> = {
GET: 'bg-blue-100 text-blue-800 border-blue-200',
POST: 'bg-green-100 text-green-800 border-green-200',
PUT: 'bg-yellow-100 text-yellow-800 border-yellow-200',
DELETE: 'bg-red-100 text-red-800 border-red-200',
}
const CrudEndpointManager: React.FC = () => {
const { translate } = useLocalization()
// Endpoint state
const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([])
useEffect(() => {
developerKitService
.getGeneratedListEndpoints()
.then((res) => setGeneratedEndpoints(res.items || []))
.catch((err) => console.error('Failed to load endpoints', err))
}, [])
// Data source + tables
const [dataSources, setDataSources] = useState<DataSourceDto[]>([])
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(null)
const [dbTables, setDbTables] = useState<DatabaseTableDto[]>([])
const [loadingTables, setLoadingTables] = useState(false)
// Selection
const [selectedTable, setSelectedTable] = useState<DatabaseTableDto | null>(null)
const [tableSearch, setTableSearch] = useState('')
const [crudFilter, setCrudFilter] = useState<'all' | 'with' | 'without'>('all')
// Endpoint management state
const [generatingFor, setGeneratingFor] = useState<string | null>(null)
const [deletingAll, setDeletingAll] = useState<string | null>(null)
const [togglingId, setTogglingId] = useState<string | null>(null)
const [expandedEndpoint, setExpandedEndpoint] = useState<string | null>(null)
// Test state
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>>({})
// Load data sources on mount
useEffect(() => {
getDataSources()
.then((res) => {
const items = res.data.items || []
setDataSources(items)
if (items.length > 0) {
setSelectedDataSource(items[0].code ?? null)
}
})
.catch(console.error)
}, [])
// Load tables when datasource changes
useEffect(() => {
if (!selectedDataSource) return
setLoadingTables(true)
setDbTables([])
setSelectedTable(null)
sqlObjectManagerService
.getAllObjects(selectedDataSource)
.then((res) => {
setDbTables(res.data.tables || [])
})
.catch(console.error)
.finally(() => setLoadingTables(false))
}, [selectedDataSource])
// Helpers
const getEndpointsForTable = useCallback(
(tableName: string): CrudEndpoint[] => {
const entityName = toPascalCase(tableName)
return generatedEndpoints.filter((ep) => ep.entityName === entityName)
},
[generatedEndpoints],
)
const activeEndpointCount = (tableName: string) =>
getEndpointsForTable(tableName).filter((ep) => ep.isActive).length
const allEndpointCount = (tableName: string) => getEndpointsForTable(tableName).length
// Filtered table list
const filteredTables = dbTables.filter((t) => {
const matchesSearch =
t.fullName.toLowerCase().includes(tableSearch.toLowerCase()) ||
t.tableName.toLowerCase().includes(tableSearch.toLowerCase())
const hasCrud = allEndpointCount(t.tableName) > 0
const matchesCrudFilter =
crudFilter === 'all' ||
(crudFilter === 'with' && hasCrud) ||
(crudFilter === 'without' && !hasCrud)
return matchesSearch && matchesCrudFilter
})
// Group by schema
const tablesBySchema = filteredTables.reduce<Record<string, DatabaseTableDto[]>>((acc, t) => {
const schema = t.schemaName || 'dbo'
if (!acc[schema]) acc[schema] = []
acc[schema].push(t)
return acc
}, {})
// Generate CRUD endpoints for selected table
const handleGenerate = async (table: DatabaseTableDto) => {
const key = table.fullName
setGeneratingFor(key)
try {
const entityName = toPascalCase(table.tableName)
const result = await developerKitService.generateCrudEndpoints(entityName)
setGeneratedEndpoints((prev) => [
...prev.filter((ep) => ep.entityName !== entityName),
...(result.items || []),
])
} catch (err) {
console.error('Generate failed', err)
} finally {
setGeneratingFor(null)
}
}
// Delete all endpoints for a table
const handleDeleteAll = async (table: DatabaseTableDto) => {
const key = table.fullName
setDeletingAll(key)
try {
const endpoints = getEndpointsForTable(table.tableName)
await Promise.all(endpoints.map((ep) => developerKitService.deleteGeneratedEndpoint(ep.id)))
const deletedIds = new Set(endpoints.map((ep) => ep.id))
setGeneratedEndpoints((prev) => prev.filter((ep) => !deletedIds.has(ep.id)))
} catch (err) {
console.error('Delete failed', err)
} finally {
setDeletingAll(null)
}
}
// Toggle single endpoint
const handleToggle = async (endpointId: string) => {
setTogglingId(endpointId)
try {
const updated = await developerKitService.toggleGeneratedEndpoint(endpointId)
setGeneratedEndpoints((prev) => prev.map((ep) => (ep.id === endpointId ? updated : ep)))
} catch (err) {
console.error('Toggle failed', err)
} finally {
setTogglingId(null)
}
}
// Test endpoint helpers
const getEndpointParameters = (endpoint: CrudEndpoint): ParameterInput[] => {
const params: ParameterInput[] = []
const vals = parameterValues[endpoint.id] || {}
switch (endpoint.operationType) {
case 'GetById':
case 'Update':
case 'Delete':
params.push({
name: 'id',
value: vals.id || '3fa85f64-5717-4562-b3fc-2c963f66afa6',
type: 'path',
required: true,
description: 'Entity ID',
})
break
case 'GetList':
params.push(
{
name: 'SkipCount',
value: vals.SkipCount || '0',
type: 'query',
required: false,
description: 'Skip count',
},
{
name: 'MaxResultCount',
value: vals.MaxResultCount || '10',
type: 'query',
required: false,
description: 'Max records',
},
)
break
}
return params
}
const needsBody = (ep: CrudEndpoint) =>
ep.operationType === 'Create' || ep.operationType === 'Update'
const getRequestBody = (ep: CrudEndpoint) => {
if (requestBodies[ep.id]) return requestBodies[ep.id]
return JSON.stringify(
{ name: 'Sample Item', description: 'Description', isActive: true },
null,
2,
)
}
const testEndpoint = async (endpoint: CrudEndpoint) => {
setLoadingEndpoints((prev) => new Set(prev).add(endpoint.id))
try {
let url = `${import.meta.env.VITE_API_URL}/api/app/crudendpoint/${endpoint.entityName?.toLowerCase()}`
const params = getEndpointParameters(endpoint)
const pathParam = params.find((p) => p.type === 'path')
if (pathParam) url += `/${pathParam.value}`
const queryParams = params.filter((p) => p.type === 'query')
if (queryParams.length) {
url += '?' + queryParams.map((p) => `${p.name}=${encodeURIComponent(p.value)}`).join('&')
}
let data = undefined
if (needsBody(endpoint)) {
try {
data = JSON.parse(getRequestBody(endpoint))
} catch {
data = {}
}
}
const res = await axios({
method: endpoint.method,
url,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
data,
})
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: true,
status: res.status,
data: res.data,
timestamp: new Date().toISOString(),
},
}))
} catch (error: unknown) {
const axiosErr = error as { response?: { status?: number; data?: unknown }; message?: string }
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: false,
status: axiosErr.response?.status || 0,
error: axiosErr.response?.data || axiosErr.message,
timestamp: new Date().toISOString(),
},
}))
} finally {
setLoadingEndpoints((prev) => {
const s = new Set(prev)
s.delete(endpoint.id)
return s
})
}
}
// Derived stats
const tablesWithEndpoints = dbTables.filter((t) => allEndpointCount(t.tableName) > 0).length
const totalActiveEndpoints = generatedEndpoints.filter((ep) => ep.isActive).length
const selectedTableEndpoints = selectedTable ? getEndpointsForTable(selectedTable.tableName) : []
return (
<div className="flex flex-col h-full gap-4">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.DeveloperKit.CrudEndpoints')}
defaultTitle={APP_NAME}
></Helmet>
{/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-2">
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">{translate('::App.DeveloperKit.CrudEndpoints.TotalTables')}</p>
<p className="text-3xl font-bold text-slate-900">{dbTables.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-100">
<FaDatabase className="w-6 h-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">{translate('::App.DeveloperKit.CrudEndpoints.EndpointInstalled')}</p>
<p className="text-3xl font-bold text-slate-900">{tablesWithEndpoints}</p>
</div>
<div className="p-3 rounded-lg bg-green-100">
<FaCheckCircle className="w-6 h-6 text-green-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">{translate('::App.DeveloperKit.CrudEndpoints.ActiveEndpoint')}</p>
<p className="text-3xl font-bold text-slate-900">{totalActiveEndpoints}</p>
</div>
<div className="p-3 rounded-lg bg-emerald-100">
<FaBolt className="w-6 h-6 text-emerald-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">{translate('::App.DeveloperKit.CrudEndpoints.DataSource')}</p>
<p className="text-3xl font-bold text-slate-900">{dataSources.length}</p>
</div>
<div className="p-3 rounded-lg bg-purple-100">
<FaServer className="w-6 h-6 text-purple-600" />
</div>
</div>
</div>
</div>
{/* Main two-panel layout */}
<div
className="flex flex-col gap-4 lg:flex-row"
style={{ minHeight: 400, height: 'calc(100vh - 250px)' }}
>
{/* Left Panel: Table List */}
<div className="w-full lg:w-72 lg:flex-shrink-0 flex flex-col bg-white rounded-lg border border-slate-200 overflow-hidden h-[38vh] min-h-[260px] lg:h-auto lg:min-h-0">
{/* DataSource selector */}
<div className="p-3 border-b border-slate-200 bg-slate-50">
<select
value={selectedDataSource || ''}
onChange={(e) => setSelectedDataSource(e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
>
{dataSources.map((ds) => (
<option key={ds.id} value={ds.code ?? ''}>
{ds.code}
</option>
))}
{dataSources.length === 0 && <option value="">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</option>}
</select>
</div>
{/* Search + CRUD filter */}
<div className="p-3 border-b border-slate-200 space-y-2">
<div className="relative">
<FaSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-xs" />
<input
type="text"
placeholder={translate('::App.DeveloperKit.CrudEndpoints.SearchTable')}
value={tableSearch}
onChange={(e) => setTableSearch(e.target.value)}
className="w-full pl-7 pr-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex rounded-lg border border-slate-200 overflow-hidden text-xs font-medium">
{(['all', 'with', 'without'] as const).map((f) => {
const labels = {
all: `${translate('::App.DeveloperKit.CrudEndpoints.FilterAll')} (${dbTables.length})`,
with: `${translate('::App.DeveloperKit.CrudEndpoints.FilterWith')} (${dbTables.filter((t) => allEndpointCount(t.tableName) > 0).length})`,
without: `${translate('::App.DeveloperKit.CrudEndpoints.FilterWithout')} (${dbTables.filter((t) => allEndpointCount(t.tableName) === 0).length})`,
}
const active = crudFilter === f
return (
<button
key={f}
onClick={() => setCrudFilter(f)}
className={`flex-1 py-1.5 transition-colors ${
active
? f === 'with'
? 'bg-green-500 text-white'
: f === 'without'
? 'bg-slate-500 text-white'
: 'bg-blue-500 text-white'
: 'bg-white text-slate-500 hover:bg-slate-50'
}`}
>
{labels[f]}
</button>
)
})}
</div>
</div>
{/* Table list */}
<div className="flex-1 overflow-y-auto">
{loadingTables ? (
<div className="flex items-center justify-center p-8 text-slate-400">
<FaSyncAlt className="animate-spin mr-2" />
<span className="text-sm">{translate('::App.DeveloperKit.CrudEndpoints.Loading')}</span>
</div>
) : filteredTables.length === 0 ? (
<div className="p-6 text-center text-slate-400 text-sm">
{selectedDataSource ? translate('::App.DeveloperKit.CrudEndpoints.NoTablesFound') : translate('::App.DeveloperKit.CrudEndpoints.SelectDataSource')}
</div>
) : (
Object.entries(tablesBySchema).map(([schema, tables]) => (
<div key={schema}>
<div className="px-3 py-1.5 text-xs font-semibold text-slate-400 uppercase tracking-wide bg-slate-50 border-b border-slate-100 sticky top-0">
{schema}
</div>
{tables.map((table) => {
const active = activeEndpointCount(table.tableName)
const total = allEndpointCount(table.tableName)
const isSelected = selectedTable?.fullName === table.fullName
const hasEndpoints = total > 0
return (
<button
key={table.fullName}
onClick={() => setSelectedTable(table)}
className={`w-full flex items-center justify-between px-3 py-1 text-left hover:bg-slate-50 transition-colors border-b border-slate-50 ${
isSelected ? 'bg-blue-50 border-l-2 border-l-blue-500' : ''
}`}
>
<div className="flex items-center gap-2 min-w-0">
<FaTable
className={`flex-shrink-0 text-xs ${
hasEndpoints ? 'text-green-500' : 'text-slate-300'
}`}
/>
<span className="text-sm text-slate-700 truncate">{table.tableName}</span>
</div>
{hasEndpoints && (
<span
className={`flex-shrink-0 text-xs px-1.5 py-0.5 rounded-full font-medium ${
active > 0
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-500'
}`}
>
{active}/{total}
</span>
)}
{isSelected && (
<FaChevronRight className="flex-shrink-0 text-blue-400 text-xs ml-1" />
)}
</button>
)
})}
</div>
))
)}
</div>
</div>
{/* Right Panel: Endpoint Management */}
<div className="flex-1 min-w-0 flex flex-col bg-white rounded-lg border border-slate-200 overflow-hidden min-h-0">
{!selectedTable ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 p-8">
<FaDatabase className="text-4xl mb-3 text-slate-200" />
<p className="text-base font-medium">{translate('::App.DeveloperKit.CrudEndpoints.SelectTablePrompt')}</p>
<p className="text-sm mt-1">
{translate('::App.DeveloperKit.CrudEndpoints.SelectTableDescription')}
</p>
</div>
) : (
<div className="flex flex-col h-full overflow-hidden">
{/* Table header */}
<div className="flex flex-col gap-3 p-4 border-b border-slate-200 bg-slate-50 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<div className="bg-blue-100 text-blue-600 p-2 rounded-lg">
<FaTable />
</div>
<div>
<h4 className="text-slate-900">{selectedTable.schemaName}.{selectedTable.tableName}</h4>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
{selectedTableEndpoints.length > 0 && (
<button
onClick={() => handleDeleteAll(selectedTable)}
disabled={deletingAll === selectedTable.fullName}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50 disabled:opacity-50 transition-colors"
>
{deletingAll === selectedTable.fullName ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaTrash />
)}
{translate('::App.DeveloperKit.CrudEndpoints.DeleteAll')}
</button>
)}
<button
onClick={() => handleGenerate(selectedTable)}
disabled={generatingFor === selectedTable.fullName}
className="flex items-center gap-2 px-4 py-1.5 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{generatingFor === selectedTable.fullName ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaBolt />
)}
{selectedTableEndpoints.length > 0
? translate('::App.DeveloperKit.CrudEndpoints.Regenerate')
: translate('::App.DeveloperKit.CrudEndpoints.CreateCrudEndpoint')}
</button>
</div>
</div>
{/* Endpoints list */}
<div className="flex-1 overflow-y-auto p-4">
{selectedTableEndpoints.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-400">
<FaBolt className="text-3xl mb-3 text-slate-200" />
<p className="font-medium">{translate('::App.DeveloperKit.CrudEndpoints.NoEndpointsYet')}</p>
<p className="text-sm mt-1">
{translate('::App.DeveloperKit.CrudEndpoints.ClickToCreate')}
</p>
</div>
) : (
<div className="space-y-3">
{selectedTableEndpoints.map((ep) => {
const isExpanded = expandedEndpoint === ep.id
const testResult = testResults[ep.id]
return (
<div
key={ep.id}
className="border border-slate-200 rounded-lg overflow-hidden"
>
{/* Endpoint row */}
<div className="flex items-center gap-3 p-3 bg-white hover:bg-slate-50 transition-colors">
{/* Toggle */}
<button
onClick={() => handleToggle(ep.id)}
disabled={togglingId === ep.id}
title={ep.isActive ? translate('::App.DeveloperKit.CrudEndpoints.Disable') : translate('::App.DeveloperKit.CrudEndpoints.Enable')}
className={`flex-shrink-0 text-xl transition-colors ${
ep.isActive
? 'text-green-500 hover:text-green-700'
: 'text-slate-300 hover:text-slate-500'
} disabled:opacity-40`}
>
{togglingId === ep.id ? (
<FaSyncAlt className="animate-spin text-base" />
) : ep.isActive ? (
<FaToggleOn />
) : (
<FaToggleOff />
)}
</button>
{/* Method badge */}
<span
className={`flex-shrink-0 px-2 py-0.5 text-xs font-bold rounded border ${
METHOD_COLOR[ep.method] ||
'bg-slate-100 text-slate-700 border-slate-200'
}`}
>
{ep.method}
</span>
{/* Operation */}
<div className="flex-1 min-w-0">
<span className="font-medium text-slate-800 text-sm">
{ep.operationType}
</span>
<code className="ml-3 text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
{ep.path}
</code>
</div>
{/* Expand */}
<button
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}
className="p-1.5 text-slate-400 hover:text-slate-700 transition-colors"
title={translate('::App.DeveloperKit.CrudEndpoints.TestDetails')}
>
{isExpanded ? (
<FaChevronDown className="text-xs" />
) : (
<FaChevronRight className="text-xs" />
)}
</button>
</div>
{/* Expanded detail + test */}
{isExpanded && (
<div className="border-t border-slate-200 bg-slate-50 p-4 space-y-4">
{/* Parameters */}
{getEndpointParameters(ep).length > 0 && (
<div>
<p className="text-xs font-semibold text-slate-600 mb-2">
{translate('::App.DeveloperKit.CrudEndpoints.Parameters')}
</p>
<div className="space-y-2">
{getEndpointParameters(ep).map((param) => (
<div key={param.name} className="flex items-center gap-2">
<span
className={`text-xs px-1.5 py-0.5 rounded font-mono ${
param.type === 'path'
? 'bg-blue-100 text-blue-700'
: 'bg-green-100 text-green-700'
}`}
>
{param.name}
</span>
<input
type="text"
value={
parameterValues[ep.id]?.[param.name] ?? param.value
}
onChange={(e) =>
setParameterValues((prev) => ({
...prev,
[ep.id]: {
...prev[ep.id],
[param.name]: e.target.value,
},
}))
}
className="flex-1 px-2 py-1 text-xs border border-slate-300 rounded focus:ring-1 focus:ring-blue-500"
/>
</div>
))}
</div>
</div>
)}
{/* Request body */}
{needsBody(ep) && (
<div>
<p className="text-xs font-semibold text-slate-600 mb-2">
{translate('::App.DeveloperKit.CrudEndpoints.RequestBody')}
</p>
<textarea
value={getRequestBody(ep)}
onChange={(e) =>
setRequestBodies((prev) => ({
...prev,
[ep.id]: e.target.value,
}))
}
rows={5}
className="w-full px-2 py-1.5 text-xs border border-slate-300 rounded font-mono focus:ring-1 focus:ring-blue-500 bg-white"
/>
</div>
)}
{/* Test button */}
<div className="flex items-center gap-2">
<button
onClick={() => testEndpoint(ep)}
disabled={loadingEndpoints.has(ep.id)}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-1.5 text-sm rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{loadingEndpoints.has(ep.id) ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaPaperPlane />
)}
{loadingEndpoints.has(ep.id) ? translate('::App.DeveloperKit.CrudEndpoints.Sending') : translate('::App.DeveloperKit.CrudEndpoints.Test')}
</button>
{testResult && (
<button
onClick={() =>
setTestResults((prev) => {
const n = { ...prev }
delete n[ep.id]
return n
})
}
className="text-xs text-slate-500 hover:text-slate-700 px-2 py-1.5"
>
{translate('::App.DeveloperKit.CrudEndpoints.Clear')}
</button>
)}
</div>
{/* Test result */}
{testResult && (
<div
className={`rounded-lg border p-3 ${
testResult.success
? 'border-green-200 bg-green-50'
: 'border-red-200 bg-red-50'
}`}
>
<div className="flex items-center gap-2 mb-2">
{testResult.success ? (
<FaCheckCircle className="text-green-500" />
) : (
<FaExclamationCircle className="text-red-500" />
)}
<span className="text-sm font-medium">
HTTP {testResult.status}
</span>
<span className="text-xs text-slate-500 ml-auto">
{new Date(testResult.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="relative">
<pre className="text-xs bg-white border border-slate-200 rounded p-2 overflow-x-auto max-h-48">
{JSON.stringify(
testResult.success ? testResult.data : testResult.error,
null,
2,
)}
</pre>
<button
onClick={() =>
navigator.clipboard.writeText(
JSON.stringify(
testResult.success ? testResult.data : testResult.error,
null,
2,
),
)
}
className="absolute top-1.5 right-1.5 p-1 text-slate-400 hover:text-slate-700"
>
<FaCopy className="text-xs" />
</button>
</div>
</div>
)}
{/* C# Code Preview */}
{ep.csharpCode && (
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-xs font-semibold text-slate-600">{translate('::App.DeveloperKit.CrudEndpoints.CsharpCode')}</p>
<button
onClick={() => navigator.clipboard.writeText(ep.csharpCode)}
className="text-xs text-slate-400 hover:text-slate-700 flex items-center gap-1"
>
<FaCopy /> {translate('::App.DeveloperKit.CrudEndpoints.Copy')}
</button>
</div>
<pre className="text-xs bg-slate-800 text-green-300 rounded-lg p-3 overflow-x-auto max-h-48 font-mono">
{ep.csharpCode}
</pre>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{/* Footer info */}
{selectedTableEndpoints.length > 0 && (
<div className="border-t border-slate-200 px-2 py-1 bg-slate-50 flex items-center gap-4 text-xs text-slate-500">
<span className="flex items-center gap-1">
<FaCheckCircle className="text-green-400" />
{selectedTableEndpoints.filter((e) => e.isActive).length} {translate('::App.DeveloperKit.CrudEndpoints.ActiveCount')}
</span>
<span>{selectedTableEndpoints.filter((e) => !e.isActive).length} {translate('::App.DeveloperKit.CrudEndpoints.InactiveCount')}</span>
<span className="ml-auto">
{translate('::App.DeveloperKit.CrudEndpoints.EndpointSummary')}
</span>
</div>
)}
</div>
)}
</div>
</div>
</div>
)
}
export default CrudEndpointManager