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 = { 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([]) useEffect(() => { developerKitService .getGeneratedListEndpoints() .then((res) => setGeneratedEndpoints(res.items || [])) .catch((err) => console.error('Failed to load endpoints', err)) }, []) // Data source + tables const [dataSources, setDataSources] = useState([]) const [selectedDataSource, setSelectedDataSource] = useState(null) const [dbTables, setDbTables] = useState([]) const [loadingTables, setLoadingTables] = useState(false) // Selection const [selectedTable, setSelectedTable] = useState(null) const [tableSearch, setTableSearch] = useState('') const [crudFilter, setCrudFilter] = useState<'all' | 'with' | 'without'>('all') // Endpoint management state const [generatingFor, setGeneratingFor] = useState(null) const [deletingAll, setDeletingAll] = useState(null) const [togglingId, setTogglingId] = useState(null) const [expandedEndpoint, setExpandedEndpoint] = useState(null) // Test state const [testResults, setTestResults] = useState>({}) const [loadingEndpoints, setLoadingEndpoints] = useState>(new Set()) const [parameterValues, setParameterValues] = useState>>({}) const [requestBodies, setRequestBodies] = useState>({}) // 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>((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 (
{/* Stats Row */}

{translate('::App.DeveloperKit.CrudEndpoints.TotalTables')}

{dbTables.length}

{translate('::App.DeveloperKit.CrudEndpoints.EndpointInstalled')}

{tablesWithEndpoints}

{translate('::App.DeveloperKit.CrudEndpoints.ActiveEndpoint')}

{totalActiveEndpoints}

{translate('::App.DeveloperKit.CrudEndpoints.DataSource')}

{dataSources.length}

{/* Main two-panel layout */}
{/* Left Panel: Table List */}
{/* DataSource selector */}
{/* Search + CRUD filter */}
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" />
{(['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 ( ) })}
{/* Table list */}
{loadingTables ? (
{translate('::App.DeveloperKit.CrudEndpoints.Loading')}
) : filteredTables.length === 0 ? (
{selectedDataSource ? translate('::App.DeveloperKit.CrudEndpoints.NoTablesFound') : translate('::App.DeveloperKit.CrudEndpoints.SelectDataSource')}
) : ( Object.entries(tablesBySchema).map(([schema, tables]) => (
{schema}
{tables.map((table) => { const active = activeEndpointCount(table.tableName) const total = allEndpointCount(table.tableName) const isSelected = selectedTable?.fullName === table.fullName const hasEndpoints = total > 0 return ( ) })}
)) )}
{/* Right Panel: Endpoint Management */}
{!selectedTable ? (

{translate('::App.DeveloperKit.CrudEndpoints.SelectTablePrompt')}

{translate('::App.DeveloperKit.CrudEndpoints.SelectTableDescription')}

) : (
{/* Table header */}

{selectedTable.schemaName}.{selectedTable.tableName}

{selectedTableEndpoints.length > 0 && ( )}
{/* Endpoints list */}
{selectedTableEndpoints.length === 0 ? (

{translate('::App.DeveloperKit.CrudEndpoints.NoEndpointsYet')}

{translate('::App.DeveloperKit.CrudEndpoints.ClickToCreate')}

) : (
{selectedTableEndpoints.map((ep) => { const isExpanded = expandedEndpoint === ep.id const testResult = testResults[ep.id] return (
{/* Endpoint row */}
{/* Toggle */} {/* Method badge */} {ep.method} {/* Operation */}
{ep.operationType} {ep.path}
{/* Expand */}
{/* Expanded detail + test */} {isExpanded && (
{/* Parameters */} {getEndpointParameters(ep).length > 0 && (

{translate('::App.DeveloperKit.CrudEndpoints.Parameters')}

{getEndpointParameters(ep).map((param) => (
{param.name} 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" />
))}
)} {/* Request body */} {needsBody(ep) && (

{translate('::App.DeveloperKit.CrudEndpoints.RequestBody')}