import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react' import { FaBolt, FaSyncAlt, FaToggleOn, FaToggleOff, FaTrash, FaCopy, FaChevronDown, FaChevronRight, FaPaperPlane, FaCheckCircle, FaExclamationCircle, FaList, FaSearch, FaPlus, FaPen, } from 'react-icons/fa' import { Button, Dialog, Notification, toast } from '@/components/ui' import { ConfirmDialog } from '@/components/shared' import { developerKitService } from '@/services/developerKit.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import apiService from '@/services/api.service' import type { CrudEndpoint } from '@/proxy/developerKit/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import { useLocalization } from '@/utils/hooks/useLocalization' import { usePermission } from '@/utils/hooks/usePermission' // ─── Paylasilan yardimcilar ─────────────────────────────────────────────────── // Endpoint'leri ureten/gosteren tum ekranlar (SqlQueryManager, SqlObjectExplorer, // SqlTableDesignerDialog, ListForm Wizard) bu dosyadaki export'lari kullanir. /** * CRUD endpoint uretimi/yonetimi icin gereken izin. * Backend tarafinda PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints ile ayni. */ export const CRUD_ENDPOINT_PERMISSION = 'App.SqlQueryManager.CrudEndpoints' /** Uretilen endpoint operasyonlari; backend ile ayni sirada ve adlarla. */ export const CRUD_OPERATION_TYPES = [ 'GetList', 'GetById', 'Create', 'Update', 'Delete', ] as const export type CrudOperationType = (typeof CRUD_OPERATION_TYPES)[number] /** Endpoint uretilebilen SQL nesne turleri. */ export type CrudObjectType = 'table' | 'view' /** * View yazma islemlerini desteklemez; bu nesneler icin yalnizca okuma * endpoint'leri uretilir. Backend de ayni kisiti objectType parametresiyle uygular. */ export const READONLY_OPERATION_TYPES: CrudOperationType[] = ['GetList', 'GetById'] /** * Endpoint'lerde kullanilan entity adini turetir. * * Entity adi calisma zamaninda `DynamicEntityManager` tarafindan dogrudan tablo adi * olarak SQL'e girer (`SELECT * FROM {entityName}`), bu yuzden tablo adi aynen korunur; * yalnizca `dbo.` gibi sema oneki ayiklanir. Ad normalize edilirse (`Adm_D_Approval` → * `AdmDApproval`) uretilen endpoint calisma aninda olmayan bir tabloyu sorgular. */ export const toEntityName = (tableName: string): string => (tableName || '').replace(/^.*\./g, '').trim() export const METHOD_COLOR: Record = { GET: 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800', POST: 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800', PUT: 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800', DELETE: 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800', } interface OperationMeta { icon: ReactNode /** Aktif endpoint rengi; pasif olanlar gri gosterilir. */ color: string } const OPERATION_META: Record = { GetList: { icon: , color: 'text-blue-500' }, GetById: { icon: , color: 'text-sky-500' }, Create: { icon: , color: 'text-green-500' }, Update: { icon: , color: 'text-amber-500' }, Delete: { icon: , color: 'text-red-500' }, } export const getOperationMeta = (operationType: string): OperationMeta => OPERATION_META[operationType] ?? { icon: , color: 'text-gray-400' } /** Endpoint'leri CRUD_OPERATION_TYPES sirasina gore siralar; bilinmeyenler sona gider. */ export const sortByCrudOperation = (endpoints: T[]): T[] => { const order = (operationType: string) => { const index = CRUD_OPERATION_TYPES.indexOf(operationType as CrudOperationType) return index === -1 ? CRUD_OPERATION_TYPES.length : index } return [...endpoints].sort((a, b) => order(a.operationType) - order(b.operationType)) } /** Endpoint listesini entity adina gore gruplar. */ export const groupEndpointsByEntity = (endpoints: CrudEndpoint[]): Map => { const map = new Map() for (const endpoint of endpoints) { const list = map.get(endpoint.entityName) if (list) list.push(endpoint) else map.set(endpoint.entityName, [endpoint]) } // Explorer rozetleri de diyalogla ayni sirada cizilir. for (const [entityName, list] of map) { map.set(entityName, sortByCrudOperation(list)) } return map } export const getErrorMessage = (error: unknown, fallback: string): string => { const response = ( error as { response?: { data?: { error?: { message?: string }; message?: string } } } )?.response return ( response?.data?.error?.message || response?.data?.message || (error instanceof Error ? error.message : undefined) || fallback ) } interface CrudEndpointBadgesProps { endpoints: CrudEndpoint[] /** Ikon uzerindeki tooltip metnini uretir: (operationType, method, path, isActive) */ buildTitle: (endpoint: CrudEndpoint) => string onClick?: () => void } /** * Bir tablonun acilmis CRUD endpoint'lerini kucuk ikonlarla gosterir. * Aktif olanlar renkli, pasif olanlar soluk gri cizilir; tooltip operasyonu aciklar. */ export const CrudEndpointBadges = ({ endpoints, buildTitle, onClick }: CrudEndpointBadgesProps) => { if (endpoints.length === 0) return null return ( { e.stopPropagation() onClick() } : undefined } > {endpoints.map((endpoint) => { const meta = getOperationMeta(endpoint.operationType) return ( {meta.icon} ) })} ) } // ─── Dialog ─────────────────────────────────────────────────────────────────── interface CrudEndpointDialogProps { isOpen: boolean /** Endpoint'leri yonetilecek tablo/view; entity adi nesne adindan turetilir. */ tableName: string schemaName?: string /** Nesne turu; 'view' verildiginde yalnizca GetList/GetById uretilir. */ objectType?: CrudObjectType /** Verilirse istek govdesi tablonun gercek kolonlarindan uretilir. */ dataSourceCode?: string | null onClose: () => void /** Endpoint listesi degistiginde cagrilir; cagiran taraf kendi state'ini tazeler. */ onChanged?: (entityName: string, endpoints: CrudEndpoint[]) => void } interface TestResult { success: boolean status: number durationMs: number payload: string } interface EndpointParameter { name: string type: 'path' | 'query' value: string } /** Create istegine gonderilmemesi gereken ABP audit/sistem kolonlari. */ const SYSTEM_COLUMNS = new Set( [ 'Id', 'TenantId', 'CreationTime', 'CreatorId', 'LastModificationTime', 'LastModifierId', 'IsDeleted', 'DeleterId', 'DeletionTime', 'ConcurrencyStamp', 'ExtraProperties', ].map((name) => name.toLowerCase()), ) const EMPTY_GUID = '00000000-0000-0000-0000-000000000000' /** SQL veri tipine gore ornek govde degeri uretir. */ const getSampleValue = (column: DatabaseColumnDto): unknown => { const type = (column.dataType || '').toLowerCase() if (column.isNullable) return null if (type.includes('guid') || type === 'uuid') return EMPTY_GUID if (type.includes('bit') || type.includes('bool')) return false if ( type.includes('int') || type.includes('decimal') || type.includes('numeric') || type.includes('money') || type.includes('float') || type.includes('real') || type.includes('double') ) { return 0 } if (type.includes('date') || type.includes('time')) return new Date().toISOString() return '' } const GENERIC_BODY = JSON.stringify({ name: 'Sample', isActive: true }, null, 2) /** Endpoint'in path'indeki {placeholder} adlarini cikarir. */ const getPathParameterNames = (path: string): string[] => [...(path || '').matchAll(/\{(\w+)\}/g)].map((match) => match[1]) const needsBody = (endpoint: CrudEndpoint) => endpoint.operationType === 'Create' || endpoint.operationType === 'Update' /** * Tek bir tablonun CRUD endpoint'lerini yonetir: uretme/yeniden uretme, tekil * aktif/pasif, tekil silme, tumunu silme ve endpoint'i parametreleriyle calistirip * sonucunu gorme. SqlQueryManager ve ListForm Wizard ayni diyalogu kullanir. */ const CrudEndpointDialog = ({ isOpen, tableName, schemaName, objectType = 'table', dataSourceCode, onClose, onChanged, }: CrudEndpointDialogProps) => { const { translate } = useLocalization() const { checkPermission } = usePermission() const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION) const entityName = toEntityName(tableName) const isReadOnlyObject = objectType === 'view' const [endpoints, setEndpoints] = useState([]) const [columns, setColumns] = useState([]) const [loading, setLoading] = useState(false) const [generating, setGenerating] = useState(false) const [deletingAll, setDeletingAll] = useState(false) const [busyId, setBusyId] = useState(null) const [expandedId, setExpandedId] = useState(null) const [confirmDeleteAll, setConfirmDeleteAll] = useState(false) const [testingId, setTestingId] = useState(null) const [testResults, setTestResults] = useState>({}) const [parameterValues, setParameterValues] = useState>>({}) const [requestBodies, setRequestBodies] = useState>({}) const pushError = useCallback((message: string) => { toast.push( {message} , { placement: 'bottom-end' }, ) }, []) const pushSuccess = useCallback((message: string) => { toast.push( {message} , { placement: 'bottom-end' }, ) }, []) const publish = useCallback( (next: CrudEndpoint[]) => { // Liste veritabani sirasiyla gelir; kullaniciya her zaman CRUD sirasinda gosterilir. const sorted = sortByCrudOperation(next) setEndpoints(sorted) onChanged?.(entityName, sorted) }, [entityName, onChanged], ) useEffect(() => { if (!isOpen || !entityName) return let cancelled = false setLoading(true) setExpandedId(null) setTestResults({}) developerKitService .getEndpointsByEntity(entityName) .then((items) => { if (!cancelled) setEndpoints(sortByCrudOperation(items)) }) .catch((err) => { if (!cancelled) pushError(getErrorMessage(err, translate('::App.Platform.ErrorLoading'))) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [isOpen, entityName, pushError, translate]) // Govde sablonu icin tablo kolonlari; datasource bilinmiyorsa genel sablon kullanilir. useEffect(() => { if (!isOpen || !dataSourceCode || isReadOnlyObject) { setColumns([]) return } let cancelled = false sqlObjectManagerService .getTableColumns(dataSourceCode, schemaName || 'dbo', tableName) .then((res) => { if (!cancelled) setColumns(res.data || []) }) .catch(() => { // Kolonlar okunamazsa tester genel govde sablonuyla calismaya devam eder. if (!cancelled) setColumns([]) }) return () => { cancelled = true } }, [isOpen, dataSourceCode, schemaName, tableName, isReadOnlyObject]) const sampleBodies = useMemo(() => { if (columns.length === 0) return { create: GENERIC_BODY, update: GENERIC_BODY } const build = (includeId: boolean) => { const body: Record = {} for (const column of columns) { const isSystem = SYSTEM_COLUMNS.has(column.columnName.toLowerCase()) const isId = column.columnName.toLowerCase() === 'id' if (isSystem && !(includeId && isId)) continue body[column.columnName] = isId ? EMPTY_GUID : getSampleValue(column) } return JSON.stringify(body, null, 2) } return { create: build(false), update: build(true) } }, [columns]) const getDefaultBody = useCallback( (endpoint: CrudEndpoint) => endpoint.operationType === 'Update' ? sampleBodies.update : sampleBodies.create, [sampleBodies], ) const getParameters = useCallback( (endpoint: CrudEndpoint): EndpointParameter[] => { const values = parameterValues[endpoint.id] const valueOf = (name: string, fallback: string) => values?.[name] ?? fallback const pathParams: EndpointParameter[] = getPathParameterNames(endpoint.path).map((name) => ({ name, type: 'path', value: valueOf(name, name.toLowerCase() === 'id' ? EMPTY_GUID : ''), })) if (endpoint.operationType !== 'GetList') return pathParams return [ ...pathParams, { name: 'SkipCount', type: 'query', value: valueOf('SkipCount', '0') }, { name: 'MaxResultCount', type: 'query', value: valueOf('MaxResultCount', '10') }, { name: 'Sorting', type: 'query', value: valueOf('Sorting', '') }, ] }, [parameterValues], ) const handleGenerate = async () => { setGenerating(true) try { const result = await developerKitService.generateCrudEndpoints(entityName, undefined, objectType) publish(result.items || []) pushSuccess(translate('::App.Platform.OperationCompleted')) } catch (err) { pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))) } finally { setGenerating(false) } } const handleDeleteAll = async () => { setDeletingAll(true) try { await developerKitService.deleteGeneratedEndpointsByEntity(entityName) publish([]) pushSuccess(translate('::App.Platform.ObjectDeletedSuccessfully')) } catch (err) { pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting'))) } finally { setDeletingAll(false) } } const handleToggle = async (endpoint: CrudEndpoint) => { setBusyId(endpoint.id) try { const updated = await developerKitService.toggleGeneratedEndpoint(endpoint.id) publish(endpoints.map((ep) => (ep.id === endpoint.id ? updated : ep))) } catch (err) { pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))) } finally { setBusyId(null) } } const handleDelete = async (endpoint: CrudEndpoint) => { setBusyId(endpoint.id) try { await developerKitService.deleteGeneratedEndpoint(endpoint.id) publish(endpoints.filter((ep) => ep.id !== endpoint.id)) } catch (err) { pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting'))) } finally { setBusyId(null) } } /** * Endpoint'i doldurulan parametre ve govde ile cagirir. Istek apiService uzerinden * gider; Authorization, __tenant ve Accept-Language interceptor'lari uygulanir. */ const handleTest = async (endpoint: CrudEndpoint) => { setTestingId(endpoint.id) const startedAt = performance.now() try { const parameters = getParameters(endpoint) const pathValues = new Map( parameters.filter((p) => p.type === 'path').map((p) => [p.name, p.value]), ) const url = endpoint.path.replace(/\{(\w+)\}/g, (match, name: string) => { const value = pathValues.get(name) return value ? encodeURIComponent(value) : match }) const params = Object.fromEntries( parameters .filter((p) => p.type === 'query' && p.value !== '') .map((p) => [p.name, p.value]), ) let data: unknown if (needsBody(endpoint)) { const raw = requestBodies[endpoint.id] ?? getDefaultBody(endpoint) try { data = JSON.parse(raw) } catch { setTestResults((prev) => ({ ...prev, [endpoint.id]: { success: false, status: 0, durationMs: 0, payload: translate('::App.Platform.InvalidJson'), }, })) return } } const response = await apiService.fetchData({ method: endpoint.method, url, params: Object.keys(params).length > 0 ? params : undefined, data, timeout: 15000, }) setTestResults((prev) => ({ ...prev, [endpoint.id]: { success: true, status: response.status, durationMs: Math.round(performance.now() - startedAt), payload: JSON.stringify(response.data, null, 2), }, })) } catch (error: unknown) { const axiosError = error as { response?: { status?: number; data?: unknown }; message?: string } setTestResults((prev) => ({ ...prev, [endpoint.id]: { success: false, status: axiosError.response?.status ?? 0, durationMs: Math.round(performance.now() - startedAt), payload: JSON.stringify(axiosError.response?.data ?? axiosError.message, null, 2), }, })) } finally { setTestingId(null) } } const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text) pushSuccess(translate('::App.Platform.Copied')) } catch (err) { pushError(getErrorMessage(err, translate('::App.Platform.CopyFailed'))) } } const activeCount = endpoints.reduce((sum, ep) => sum + (ep.isActive ? 1 : 0), 0) const busy = loading || generating || deletingAll // Izin yoksa dialog hic acilmaz; cagiran ekranlar da butonu gizler. if (!canManageCrudEndpoints) return null return (
{translate('::App.SqlQueryManager.CrudEndpoints')}

{schemaName ? `${schemaName}.${tableName}` : tableName} {' — '} {entityName} {isReadOnlyObject && ( view )}

{isReadOnlyObject && (

{READONLY_OPERATION_TYPES.join(' / ')}

)}
{loading ? (
{translate('::App.Platform.LoadingWithThreeDot')}
) : endpoints.length === 0 ? (

{translate('::App.DeveloperKitCrudEndpoints.NoEndpointsYet')}

{translate('::App.DeveloperKitCrudEndpoints.ClickToCreate')}

) : (
{endpoints.map((ep) => { const meta = getOperationMeta(ep.operationType) const isExpanded = expandedId === ep.id const parameters = isExpanded ? getParameters(ep) : [] const testResult = testResults[ep.id] const isTesting = testingId === ep.id return (
{isExpanded && (
{parameters.length > 0 && (

{translate('::App.DeveloperKitCrudEndpoints.Parameters')}

{parameters.map((param) => (
{param.name} setParameterValues((prev) => ({ ...prev, [ep.id]: { ...prev[ep.id], [param.name]: e.target.value }, })) } className="flex-1 rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-900 focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100" />
))}
)} {needsBody(ep) && (
{translate('::App.DeveloperKitCrudEndpoints.RequestBody')}