Claude güncellemesi Crudendpoint değişiklikleri
This commit is contained in:
parent
c2f1b203ab
commit
abece2b33e
1 changed files with 460 additions and 286 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
import {
|
||||||
FaSearch,
|
FaSearch,
|
||||||
FaCopy,
|
FaCopy,
|
||||||
|
|
@ -21,11 +20,14 @@ import Widget from '@/components/common/Widget'
|
||||||
import { getDataSources } from '@/services/data-source.service'
|
import { getDataSources } from '@/services/data-source.service'
|
||||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import { developerKitService } from '@/services/developerKit.service'
|
import { developerKitService } from '@/services/developerKit.service'
|
||||||
|
import apiService from '@/services/api.service'
|
||||||
import type { DataSourceDto } from '@/proxy/data-source'
|
import type { DataSourceDto } from '@/proxy/data-source'
|
||||||
import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
|
import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
|
||||||
import type { CrudEndpoint } from '@/proxy/developerKit/models'
|
import type { CrudEndpoint } from '@/proxy/developerKit/models'
|
||||||
import PageTitle from '@/components/shared/PageTitle'
|
import PageTitle from '@/components/shared/PageTitle'
|
||||||
|
import { ConfirmDialog } from '@/components/shared'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
|
import { Notification, toast } from '@/components/ui'
|
||||||
|
|
||||||
interface TestResult {
|
interface TestResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
|
|
@ -38,9 +40,41 @@ interface TestResult {
|
||||||
interface ParameterInput {
|
interface ParameterInput {
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
type: 'path' | 'query' | 'body'
|
type: 'path' | 'query'
|
||||||
required: boolean
|
}
|
||||||
description?: string
|
|
||||||
|
type CrudFilter = 'all' | 'with' | 'without'
|
||||||
|
|
||||||
|
interface TableStat {
|
||||||
|
total: number
|
||||||
|
active: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const CRUD_FILTERS: readonly CrudFilter[] = ['all', 'with', 'without'] as const
|
||||||
|
|
||||||
|
const EMPTY_ENDPOINTS: CrudEndpoint[] = []
|
||||||
|
const EMPTY_PARAMS: ParameterInput[] = []
|
||||||
|
|
||||||
|
const DEFAULT_SAMPLE_BODY = JSON.stringify(
|
||||||
|
{ name: 'Sample Item', description: 'Description', isActive: true },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
const DEFAULT_ID_PARAM = '3fa85f64-5717-4562-b3fc-2c963f66afa6'
|
||||||
|
|
||||||
|
const METHOD_COLOR: Record<string, string> = {
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILTER_ACTIVE_CLASS: Record<CrudFilter, string> = {
|
||||||
|
all: '!bg-blue-500 text-white',
|
||||||
|
with: '!bg-green-500 text-white',
|
||||||
|
without: '!bg-slate-500 text-white',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: tableName -> PascalCase entity name
|
// Helper: tableName -> PascalCase entity name
|
||||||
|
|
@ -51,26 +85,27 @@ function toPascalCase(tableName: string): string {
|
||||||
.replace(/^([a-z])/, (c: string) => c.toUpperCase())
|
.replace(/^([a-z])/, (c: string) => c.toUpperCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
const METHOD_COLOR: Record<string, string> = {
|
const getErrorMessage = (error: unknown, fallback: string): string => {
|
||||||
GET: 'bg-blue-100 text-blue-800 border-blue-200',
|
const response = (
|
||||||
POST: 'bg-green-100 text-green-800 border-green-200',
|
error as { response?: { data?: { error?: { message?: string }; message?: string } } }
|
||||||
PUT: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
)?.response
|
||||||
DELETE: 'bg-red-100 text-red-800 border-red-200',
|
return (
|
||||||
|
response?.data?.error?.message ||
|
||||||
|
response?.data?.message ||
|
||||||
|
(error instanceof Error ? error.message : undefined) ||
|
||||||
|
fallback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const needsBody = (ep: CrudEndpoint) =>
|
||||||
|
ep.operationType === 'Create' || ep.operationType === 'Update'
|
||||||
|
|
||||||
const CrudEndpointManager: React.FC = () => {
|
const CrudEndpointManager: React.FC = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
// Endpoint state
|
// Endpoints
|
||||||
const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([])
|
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
|
// Data source + tables
|
||||||
const [dataSources, setDataSources] = useState<DataSourceDto[]>([])
|
const [dataSources, setDataSources] = useState<DataSourceDto[]>([])
|
||||||
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(null)
|
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(null)
|
||||||
|
|
@ -80,11 +115,12 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
// Selection
|
// Selection
|
||||||
const [selectedTable, setSelectedTable] = useState<DatabaseTableDto | null>(null)
|
const [selectedTable, setSelectedTable] = useState<DatabaseTableDto | null>(null)
|
||||||
const [tableSearch, setTableSearch] = useState('')
|
const [tableSearch, setTableSearch] = useState('')
|
||||||
const [crudFilter, setCrudFilter] = useState<'all' | 'with' | 'without'>('all')
|
const [crudFilter, setCrudFilter] = useState<CrudFilter>('all')
|
||||||
|
|
||||||
// Endpoint management state
|
// Endpoint management state
|
||||||
const [generatingFor, setGeneratingFor] = useState<string | null>(null)
|
const [generatingFor, setGeneratingFor] = useState<string | null>(null)
|
||||||
const [deletingAll, setDeletingAll] = useState<string | null>(null)
|
const [deletingAll, setDeletingAll] = useState<string | null>(null)
|
||||||
|
const [confirmDeleteTable, setConfirmDeleteTable] = useState<DatabaseTableDto | null>(null)
|
||||||
const [togglingId, setTogglingId] = useState<string | null>(null)
|
const [togglingId, setTogglingId] = useState<string | null>(null)
|
||||||
const [expandedEndpoint, setExpandedEndpoint] = useState<string | null>(null)
|
const [expandedEndpoint, setExpandedEndpoint] = useState<string | null>(null)
|
||||||
|
|
||||||
|
|
@ -94,229 +130,354 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
|
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
|
||||||
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
|
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
// Load data sources on mount
|
const pushError = useCallback((message: string) => {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="danger" duration={3000}>
|
||||||
|
{message}
|
||||||
|
</Notification>,
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const pushSuccess = useCallback((message: string) => {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="success" duration={2000}>
|
||||||
|
{message}
|
||||||
|
</Notification>,
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load endpoints + data sources on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
developerKitService
|
||||||
|
.getGeneratedListEndpoints()
|
||||||
|
.then((res) => {
|
||||||
|
if (!cancelled) setGeneratedEndpoints(res.items || [])
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled) pushError(getErrorMessage(err, translate('::App.Platform.ErrorLoading')))
|
||||||
|
})
|
||||||
|
|
||||||
getDataSources()
|
getDataSources()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (cancelled) return
|
||||||
const items = res.data.items || []
|
const items = res.data.items || []
|
||||||
setDataSources(items)
|
setDataSources(items)
|
||||||
if (items.length > 0) {
|
setSelectedDataSource((current) => current ?? items[0]?.code ?? null)
|
||||||
setSelectedDataSource(items[0].code ?? null)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.catch((err) => {
|
||||||
}, [])
|
if (!cancelled)
|
||||||
|
pushError(getErrorMessage(err, translate('::App.Platform.FailedtoloadDatasources')))
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [pushError, translate])
|
||||||
|
|
||||||
// Load tables when datasource changes
|
// Load tables when datasource changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedDataSource) return
|
if (!selectedDataSource) return
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
setLoadingTables(true)
|
setLoadingTables(true)
|
||||||
setDbTables([])
|
setDbTables([])
|
||||||
setSelectedTable(null)
|
setSelectedTable(null)
|
||||||
|
|
||||||
sqlObjectManagerService
|
sqlObjectManagerService
|
||||||
.getAllObjects(selectedDataSource)
|
.getAllObjects(selectedDataSource)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setDbTables(res.data.tables || [])
|
if (!cancelled) setDbTables(res.data.tables || [])
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled)
|
||||||
|
pushError(getErrorMessage(err, translate('::App.Platform.FailedToLoadObjects')))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoadingTables(false)
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
|
||||||
.finally(() => setLoadingTables(false))
|
|
||||||
}, [selectedDataSource])
|
|
||||||
|
|
||||||
// Helpers
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [selectedDataSource, pushError, translate])
|
||||||
|
|
||||||
|
// entityName -> endpoints (tek geçiş, tablo başına filtrelemeyi önler)
|
||||||
|
const endpointsByEntity = useMemo(() => {
|
||||||
|
const map = new Map<string, CrudEndpoint[]>()
|
||||||
|
for (const ep of generatedEndpoints) {
|
||||||
|
const list = map.get(ep.entityName)
|
||||||
|
if (list) list.push(ep)
|
||||||
|
else map.set(ep.entityName, [ep])
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [generatedEndpoints])
|
||||||
|
|
||||||
const getEndpointsForTable = useCallback(
|
const getEndpointsForTable = useCallback(
|
||||||
(tableName: string): CrudEndpoint[] => {
|
(tableName: string): CrudEndpoint[] =>
|
||||||
const entityName = toPascalCase(tableName)
|
endpointsByEntity.get(toPascalCase(tableName)) ?? EMPTY_ENDPOINTS,
|
||||||
return generatedEndpoints.filter((ep) => ep.entityName === entityName)
|
[endpointsByEntity],
|
||||||
|
)
|
||||||
|
|
||||||
|
// fullName -> { total, active }
|
||||||
|
const tableStats = useMemo(() => {
|
||||||
|
const map = new Map<string, TableStat>()
|
||||||
|
for (const table of dbTables) {
|
||||||
|
const endpoints = endpointsByEntity.get(toPascalCase(table.tableName)) ?? EMPTY_ENDPOINTS
|
||||||
|
map.set(table.fullName, {
|
||||||
|
total: endpoints.length,
|
||||||
|
active: endpoints.reduce((sum, ep) => sum + (ep.isActive ? 1 : 0), 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}, [dbTables, endpointsByEntity])
|
||||||
|
|
||||||
|
const withCrudCount = useMemo(
|
||||||
|
() => dbTables.reduce((sum, t) => sum + ((tableStats.get(t.fullName)?.total ?? 0) > 0 ? 1 : 0), 0),
|
||||||
|
[dbTables, tableStats],
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredTables = useMemo(() => {
|
||||||
|
const search = tableSearch.trim().toLowerCase()
|
||||||
|
return dbTables.filter((t) => {
|
||||||
|
const matchesSearch =
|
||||||
|
!search ||
|
||||||
|
t.fullName.toLowerCase().includes(search) ||
|
||||||
|
t.tableName.toLowerCase().includes(search)
|
||||||
|
if (!matchesSearch) return false
|
||||||
|
if (crudFilter === 'all') return true
|
||||||
|
const hasCrud = (tableStats.get(t.fullName)?.total ?? 0) > 0
|
||||||
|
return crudFilter === 'with' ? hasCrud : !hasCrud
|
||||||
|
})
|
||||||
|
}, [dbTables, tableSearch, crudFilter, tableStats])
|
||||||
|
|
||||||
|
const tablesBySchema = useMemo(
|
||||||
|
() =>
|
||||||
|
filteredTables.reduce<Record<string, DatabaseTableDto[]>>((acc, t) => {
|
||||||
|
const schema = t.schemaName || 'dbo'
|
||||||
|
;(acc[schema] ??= []).push(t)
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
[filteredTables],
|
||||||
|
)
|
||||||
|
|
||||||
|
const filterLabels = useMemo<Record<CrudFilter, string>>(
|
||||||
|
() => ({
|
||||||
|
all: `${translate('::App.StaticLookup.All')} (${dbTables.length})`,
|
||||||
|
with: `${translate('::App.DeveloperKitCrudEndpoints.FilterWith')} (${withCrudCount})`,
|
||||||
|
without: `${translate('::App.DeveloperKitCrudEndpoints.FilterWithout')} (${dbTables.length - withCrudCount})`,
|
||||||
|
}),
|
||||||
|
[dbTables.length, withCrudCount, translate],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generate CRUD endpoints for a table
|
||||||
|
const handleGenerate = useCallback(
|
||||||
|
async (table: DatabaseTableDto) => {
|
||||||
|
setGeneratingFor(table.fullName)
|
||||||
|
try {
|
||||||
|
const entityName = toPascalCase(table.tableName)
|
||||||
|
const result = await developerKitService.generateCrudEndpoints(entityName)
|
||||||
|
setGeneratedEndpoints((prev) => [
|
||||||
|
...prev.filter((ep) => ep.entityName !== entityName),
|
||||||
|
...(result.items || []),
|
||||||
|
])
|
||||||
|
pushSuccess(translate('::App.Platform.OperationCompleted'))
|
||||||
|
} catch (err) {
|
||||||
|
pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz')))
|
||||||
|
} finally {
|
||||||
|
setGeneratingFor(null)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
[pushError, pushSuccess, translate],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete all endpoints for a table
|
||||||
|
const handleDeleteAll = useCallback(
|
||||||
|
async (table: DatabaseTableDto) => {
|
||||||
|
setDeletingAll(table.fullName)
|
||||||
|
try {
|
||||||
|
const endpoints = getEndpointsForTable(table.tableName)
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
endpoints.map((ep) => developerKitService.deleteGeneratedEndpoint(ep.id)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Kısmi başarıda yalnızca gerçekten silinenleri state'ten düş.
|
||||||
|
const deletedIds = new Set(
|
||||||
|
endpoints.filter((_, i) => results[i].status === 'fulfilled').map((ep) => ep.id),
|
||||||
|
)
|
||||||
|
if (deletedIds.size) {
|
||||||
|
setGeneratedEndpoints((prev) => prev.filter((ep) => !deletedIds.has(ep.id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const failed = results.find((r) => r.status === 'rejected')
|
||||||
|
if (failed) {
|
||||||
|
pushError(
|
||||||
|
getErrorMessage(
|
||||||
|
(failed as PromiseRejectedResult).reason,
|
||||||
|
translate('::App.Platform.ErrorDeleting'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pushSuccess(translate('::App.Platform.ObjectDeletedSuccessfully'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting')))
|
||||||
|
} finally {
|
||||||
|
setDeletingAll(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getEndpointsForTable, pushError, pushSuccess, translate],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Toggle single endpoint
|
||||||
|
const handleToggle = useCallback(
|
||||||
|
async (endpointId: string) => {
|
||||||
|
setTogglingId(endpointId)
|
||||||
|
try {
|
||||||
|
const updated = await developerKitService.toggleGeneratedEndpoint(endpointId)
|
||||||
|
setGeneratedEndpoints((prev) => prev.map((ep) => (ep.id === endpointId ? updated : ep)))
|
||||||
|
} catch (err) {
|
||||||
|
pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz')))
|
||||||
|
} finally {
|
||||||
|
setTogglingId(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pushError, translate],
|
||||||
|
)
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(
|
||||||
|
async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
pushSuccess(translate('::App.Platform.Copied'))
|
||||||
|
} catch (err) {
|
||||||
|
pushError(getErrorMessage(err, translate('::App.Platform.CopyFailed')))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pushError, pushSuccess, translate],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test endpoint helpers
|
||||||
|
const getEndpointParameters = useCallback(
|
||||||
|
(endpoint: CrudEndpoint): ParameterInput[] => {
|
||||||
|
const vals = parameterValues[endpoint.id]
|
||||||
|
const valueOf = (name: string, fallback: string) => vals?.[name] ?? fallback
|
||||||
|
|
||||||
|
switch (endpoint.operationType) {
|
||||||
|
case 'GetById':
|
||||||
|
case 'Update':
|
||||||
|
case 'Delete':
|
||||||
|
return [{ name: 'id', value: valueOf('id', DEFAULT_ID_PARAM), type: 'path' }]
|
||||||
|
case 'GetList':
|
||||||
|
return [
|
||||||
|
{ name: 'SkipCount', value: valueOf('SkipCount', '0'), type: 'query' },
|
||||||
|
{ name: 'MaxResultCount', value: valueOf('MaxResultCount', '10'), type: 'query' },
|
||||||
|
]
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[parameterValues],
|
||||||
|
)
|
||||||
|
|
||||||
|
const getRequestBody = useCallback(
|
||||||
|
(ep: CrudEndpoint) => requestBodies[ep.id] ?? DEFAULT_SAMPLE_BODY,
|
||||||
|
[requestBodies],
|
||||||
|
)
|
||||||
|
|
||||||
|
const testEndpoint = useCallback(
|
||||||
|
async (endpoint: CrudEndpoint) => {
|
||||||
|
setLoadingEndpoints((prev) => new Set(prev).add(endpoint.id))
|
||||||
|
try {
|
||||||
|
const params = getEndpointParameters(endpoint)
|
||||||
|
const pathValues = new Map(
|
||||||
|
params.filter((p) => p.type === 'path').map((p) => [p.name, p.value]),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path backend tarafından üretilir ({id} gibi yer tutucular içerir).
|
||||||
|
const basePath = endpoint.path || `/api/app/crudendpoint/${endpoint.entityName}`
|
||||||
|
const url = basePath.replace(/\{(\w+)\}/g, (match, name: string) => {
|
||||||
|
const value = pathValues.get(name)
|
||||||
|
return value === undefined ? match : encodeURIComponent(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryParams = Object.fromEntries(
|
||||||
|
params.filter((p) => p.type === 'query').map((p) => [p.name, p.value]),
|
||||||
|
)
|
||||||
|
|
||||||
|
let data: unknown
|
||||||
|
if (needsBody(endpoint)) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(getRequestBody(endpoint))
|
||||||
|
} catch {
|
||||||
|
data = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiService üzerinden gider: Authorization, __tenant ve Accept-Language
|
||||||
|
// interceptor'ları uygulanır.
|
||||||
|
const res = await apiService.fetchData({
|
||||||
|
method: endpoint.method,
|
||||||
|
url,
|
||||||
|
params: Object.keys(queryParams).length ? queryParams : undefined,
|
||||||
|
timeout: 10000,
|
||||||
|
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 next = new Set(prev)
|
||||||
|
next.delete(endpoint.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[getEndpointParameters, getRequestBody],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Derived stats
|
||||||
|
const totalActiveEndpoints = useMemo(
|
||||||
|
() => generatedEndpoints.reduce((sum, ep) => sum + (ep.isActive ? 1 : 0), 0),
|
||||||
[generatedEndpoints],
|
[generatedEndpoints],
|
||||||
)
|
)
|
||||||
|
|
||||||
const activeEndpointCount = (tableName: string) =>
|
const selectedTableEndpoints = useMemo(
|
||||||
getEndpointsForTable(tableName).filter((ep) => ep.isActive).length
|
() => (selectedTable ? getEndpointsForTable(selectedTable.tableName) : EMPTY_ENDPOINTS),
|
||||||
|
[selectedTable, getEndpointsForTable],
|
||||||
|
)
|
||||||
|
|
||||||
const allEndpointCount = (tableName: string) => getEndpointsForTable(tableName).length
|
const selectedActiveCount = useMemo(
|
||||||
|
() => selectedTableEndpoints.reduce((sum, ep) => sum + (ep.isActive ? 1 : 0), 0),
|
||||||
// Filtered table list
|
[selectedTableEndpoints],
|
||||||
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 (
|
return (
|
||||||
<div className="flex flex-col h-full gap-4 bg-gray-50 dark:bg-gray-950">
|
<div className="flex flex-col h-full gap-4 bg-gray-50 dark:bg-gray-950">
|
||||||
<PageTitle title={translate('::' + 'App.DeveloperKit.CrudEndpoints')} />
|
<PageTitle title={translate('::App.DeveloperKit.CrudEndpoints')} />
|
||||||
|
|
||||||
{/* Stats Row */}
|
{/* Stats Row */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-2">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-2">
|
||||||
|
|
@ -330,7 +491,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<Widget
|
<Widget
|
||||||
title={translate('::App.DeveloperKitCrudEndpoints.EndpointInstalled')}
|
title={translate('::App.DeveloperKitCrudEndpoints.EndpointInstalled')}
|
||||||
value={tablesWithEndpoints}
|
value={withCrudCount}
|
||||||
color="green"
|
color="green"
|
||||||
icon="FaCheckCircle"
|
icon="FaCheckCircle"
|
||||||
valueClassName="text-3xl"
|
valueClassName="text-3xl"
|
||||||
|
|
@ -366,14 +527,17 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<select
|
<select
|
||||||
value={selectedDataSource || ''}
|
value={selectedDataSource || ''}
|
||||||
onChange={(e) => setSelectedDataSource(e.target.value)}
|
onChange={(e) => setSelectedDataSource(e.target.value)}
|
||||||
|
aria-label={translate('::App.Platform.DataSource')}
|
||||||
className="w-full px-2 py-1.5 text-sm border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
className="w-full px-2 py-1.5 text-sm border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
||||||
>
|
>
|
||||||
|
{dataSources.length === 0 && (
|
||||||
|
<option value="">{translate('::App.Platform.LoadingWithThreeDot')}</option>
|
||||||
|
)}
|
||||||
{dataSources.map((ds) => (
|
{dataSources.map((ds) => (
|
||||||
<option key={ds.id} value={ds.code ?? ''}>
|
<option key={ds.id} value={ds.code ?? ''}>
|
||||||
{ds.code}
|
{ds.code}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
{dataSources.length === 0 && <option value="">{translate('::App.Platform.LoadingWithThreeDot')}</option>}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -382,42 +546,32 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<FaSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-xs" />
|
<FaSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-xs" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="search"
|
||||||
placeholder={translate('::App.DeveloperKitCrudEndpoints.SearchTable')}
|
placeholder={translate('::App.DeveloperKitCrudEndpoints.SearchTable')}
|
||||||
value={tableSearch}
|
value={tableSearch}
|
||||||
onChange={(e) => setTableSearch(e.target.value)}
|
onChange={(e) => setTableSearch(e.target.value)}
|
||||||
|
aria-label={translate('::App.DeveloperKitCrudEndpoints.SearchTable')}
|
||||||
className="w-full pl-7 pr-3 py-1.5 text-sm border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
className="w-full pl-7 pr-3 py-1.5 text-sm border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex rounded-lg border border-slate-200 dark:border-gray-700 overflow-hidden text-xs font-medium">
|
<div className="flex rounded-lg border border-slate-200 dark:border-gray-700 overflow-hidden text-xs font-medium">
|
||||||
{(['all', 'with', 'without'] as const).map((f) => {
|
{CRUD_FILTERS.map((f) => (
|
||||||
const labels = {
|
<Button
|
||||||
all: `${translate('::App.StaticLookup.All')} (${dbTables.length})`,
|
type="button"
|
||||||
with: `${translate('::App.DeveloperKitCrudEndpoints.FilterWith')} (${dbTables.filter((t) => allEndpointCount(t.tableName) > 0).length})`,
|
key={f}
|
||||||
without: `${translate('::App.DeveloperKitCrudEndpoints.FilterWithout')} (${dbTables.filter((t) => allEndpointCount(t.tableName) === 0).length})`,
|
onClick={() => setCrudFilter(f)}
|
||||||
}
|
variant="plain"
|
||||||
const active = crudFilter === f
|
shape="none"
|
||||||
return (
|
aria-pressed={crudFilter === f}
|
||||||
<Button
|
className={`flex-1 !h-auto !rounded-none !px-0 !py-1.5 transition-colors ${
|
||||||
type="button"
|
crudFilter === f
|
||||||
key={f}
|
? FILTER_ACTIVE_CLASS[f]
|
||||||
onClick={() => setCrudFilter(f)}
|
: '!bg-white text-slate-500 hover:!bg-slate-50 dark:!bg-gray-900 dark:text-gray-400 dark:hover:!bg-gray-800'
|
||||||
variant="plain"
|
}`}
|
||||||
shape="none"
|
>
|
||||||
className={`flex-1 !h-auto !rounded-none !px-0 !py-1.5 transition-colors ${
|
{filterLabels[f]}
|
||||||
active
|
</Button>
|
||||||
? 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 dark:!bg-gray-900 dark:text-gray-400 dark:hover:!bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{labels[f]}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -441,8 +595,9 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{schema}
|
{schema}
|
||||||
</div>
|
</div>
|
||||||
{tables.map((table) => {
|
{tables.map((table) => {
|
||||||
const active = activeEndpointCount(table.tableName)
|
const stat = tableStats.get(table.fullName)
|
||||||
const total = allEndpointCount(table.tableName)
|
const active = stat?.active ?? 0
|
||||||
|
const total = stat?.total ?? 0
|
||||||
const isSelected = selectedTable?.fullName === table.fullName
|
const isSelected = selectedTable?.fullName === table.fullName
|
||||||
const hasEndpoints = total > 0
|
const hasEndpoints = total > 0
|
||||||
return (
|
return (
|
||||||
|
|
@ -515,17 +670,15 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<div className="bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 p-2 rounded-lg">
|
<div className="bg-blue-100 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 p-2 rounded-lg">
|
||||||
<FaTable />
|
<FaTable />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<h4 className="text-slate-900 dark:text-gray-100">
|
||||||
<h4 className="text-slate-900 dark:text-gray-100">
|
{selectedTable.schemaName}.{selectedTable.tableName}
|
||||||
{selectedTable.schemaName}.{selectedTable.tableName}
|
</h4>
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{selectedTableEndpoints.length > 0 && (
|
{selectedTableEndpoints.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDeleteAll(selectedTable)}
|
onClick={() => setConfirmDeleteTable(selectedTable)}
|
||||||
disabled={deletingAll === selectedTable.fullName}
|
disabled={deletingAll === selectedTable.fullName}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
|
|
@ -544,7 +697,10 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleGenerate(selectedTable)}
|
onClick={() => handleGenerate(selectedTable)}
|
||||||
disabled={generatingFor === selectedTable.fullName}
|
disabled={
|
||||||
|
generatingFor === selectedTable.fullName ||
|
||||||
|
deletingAll === selectedTable.fullName
|
||||||
|
}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
icon={
|
icon={
|
||||||
generatingFor === selectedTable.fullName ? (
|
generatingFor === selectedTable.fullName ? (
|
||||||
|
|
@ -579,6 +735,15 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{selectedTableEndpoints.map((ep) => {
|
{selectedTableEndpoints.map((ep) => {
|
||||||
const isExpanded = expandedEndpoint === ep.id
|
const isExpanded = expandedEndpoint === ep.id
|
||||||
const testResult = testResults[ep.id]
|
const testResult = testResults[ep.id]
|
||||||
|
const isTesting = loadingEndpoints.has(ep.id)
|
||||||
|
const parameters = isExpanded ? getEndpointParameters(ep) : EMPTY_PARAMS
|
||||||
|
const resultPayload = testResult
|
||||||
|
? JSON.stringify(
|
||||||
|
testResult.success ? testResult.data : testResult.error,
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
: ''
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={ep.id}
|
key={ep.id}
|
||||||
|
|
@ -639,6 +804,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}
|
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
|
aria-expanded={isExpanded}
|
||||||
className="!h-7 !w-7 !rounded !p-0 text-slate-400 transition-colors hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
className="!h-7 !w-7 !rounded !p-0 text-slate-400 transition-colors hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
title={translate('::App.DeveloperKitCrudEndpoints.TestDetails')}
|
title={translate('::App.DeveloperKitCrudEndpoints.TestDetails')}
|
||||||
>
|
>
|
||||||
|
|
@ -654,28 +820,27 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="border-t border-slate-200 dark:border-gray-700 bg-slate-50 dark:bg-gray-800 p-4 space-y-4">
|
<div className="border-t border-slate-200 dark:border-gray-700 bg-slate-50 dark:bg-gray-800 p-4 space-y-4">
|
||||||
{/* Parameters */}
|
{/* Parameters */}
|
||||||
{getEndpointParameters(ep).length > 0 && (
|
{parameters.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-slate-600 dark:text-gray-300 mb-2">
|
<p className="text-xs font-semibold text-slate-600 dark:text-gray-300 mb-2">
|
||||||
{translate('::App.DeveloperKitCrudEndpoints.Parameters')}
|
{translate('::App.DeveloperKitCrudEndpoints.Parameters')}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{getEndpointParameters(ep).map((param) => (
|
{parameters.map((param) => (
|
||||||
<div key={param.name} className="flex items-center gap-2">
|
<div key={param.name} className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`text-xs px-1.5 py-0.5 rounded font-mono ${
|
className={`text-xs px-1.5 py-0.5 rounded font-mono ${
|
||||||
param.type === 'path'
|
param.type === 'path'
|
||||||
? 'bg-blue-100 text-blue-700'
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||||
: 'bg-green-100 text-green-700'
|
: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{param.name}
|
{param.name}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={
|
value={param.value}
|
||||||
parameterValues[ep.id]?.[param.name] ?? param.value
|
aria-label={param.name}
|
||||||
}
|
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setParameterValues((prev) => ({
|
setParameterValues((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -701,6 +866,9 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
value={getRequestBody(ep)}
|
value={getRequestBody(ep)}
|
||||||
|
aria-label={translate(
|
||||||
|
'::App.DeveloperKitCrudEndpoints.RequestBody',
|
||||||
|
)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRequestBodies((prev) => ({
|
setRequestBodies((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -708,6 +876,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
rows={5}
|
rows={5}
|
||||||
|
spellCheck={false}
|
||||||
className="w-full px-2 py-1.5 text-xs border border-slate-300 dark:border-gray-700 rounded font-mono focus:ring-1 focus:ring-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
className="w-full px-2 py-1.5 text-xs border border-slate-300 dark:border-gray-700 rounded font-mono focus:ring-1 focus:ring-blue-500 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -718,10 +887,10 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => testEndpoint(ep)}
|
onClick={() => testEndpoint(ep)}
|
||||||
disabled={loadingEndpoints.has(ep.id)}
|
disabled={isTesting}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
icon={
|
icon={
|
||||||
loadingEndpoints.has(ep.id) ? (
|
isTesting ? (
|
||||||
<FaSyncAlt className="animate-spin" />
|
<FaSyncAlt className="animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<FaPaperPlane />
|
<FaPaperPlane />
|
||||||
|
|
@ -729,7 +898,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
}
|
}
|
||||||
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg !px-4 !py-1.5 text-sm text-white transition-colors hover:!bg-blue-700 disabled:opacity-50"
|
className="!inline-flex !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg !px-4 !py-1.5 text-sm text-white transition-colors hover:!bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{loadingEndpoints.has(ep.id)
|
{isTesting
|
||||||
? translate('::App.DeveloperKitEndpoint.SendLoading')
|
? translate('::App.DeveloperKitEndpoint.SendLoading')
|
||||||
: translate('::App.DeveloperKitCrudEndpoints.Test')}
|
: translate('::App.DeveloperKitCrudEndpoints.Test')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -738,9 +907,9 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setTestResults((prev) => {
|
setTestResults((prev) => {
|
||||||
const n = { ...prev }
|
const next = { ...prev }
|
||||||
delete n[ep.id]
|
delete next[ep.id]
|
||||||
return n
|
return next
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
@ -776,25 +945,14 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<pre className="text-xs bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded p-2 overflow-x-auto max-h-48 text-gray-900 dark:text-gray-100">
|
<pre className="text-xs bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-700 rounded p-2 overflow-x-auto max-h-48 text-gray-900 dark:text-gray-100">
|
||||||
{JSON.stringify(
|
{resultPayload}
|
||||||
testResult.success ? testResult.data : testResult.error,
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)}
|
|
||||||
</pre>
|
</pre>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => copyToClipboard(resultPayload)}
|
||||||
navigator.clipboard.writeText(
|
|
||||||
JSON.stringify(
|
|
||||||
testResult.success ? testResult.data : testResult.error,
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
|
title={translate('::App.Platform.CopyToClipboard')}
|
||||||
icon={<FaCopy className="text-xs" />}
|
icon={<FaCopy className="text-xs" />}
|
||||||
className="absolute right-1.5 top-1.5 !h-6 !w-6 !rounded !p-0 text-slate-400 hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
className="absolute right-1.5 top-1.5 !h-6 !w-6 !rounded !p-0 text-slate-400 hover:!bg-transparent hover:text-slate-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
/>
|
/>
|
||||||
|
|
@ -811,7 +969,7 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigator.clipboard.writeText(ep.csharpCode)}
|
onClick={() => copyToClipboard(ep.csharpCode)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="none"
|
shape="none"
|
||||||
icon={<FaCopy />}
|
icon={<FaCopy />}
|
||||||
|
|
@ -839,11 +997,10 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
<div className="border-t border-slate-200 dark:border-gray-700 px-2 py-1 bg-slate-50 dark:bg-gray-800 flex items-center gap-4 text-xs text-slate-500 dark:text-gray-400">
|
<div className="border-t border-slate-200 dark:border-gray-700 px-2 py-1 bg-slate-50 dark:bg-gray-800 flex items-center gap-4 text-xs text-slate-500 dark:text-gray-400">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<FaCheckCircle className="text-green-400" />
|
<FaCheckCircle className="text-green-400" />
|
||||||
{selectedTableEndpoints.filter((e) => e.isActive).length}{' '}
|
{selectedActiveCount} {translate('::App.Listform.ListformField.IsActive')}
|
||||||
{translate('::App.Listform.ListformField.IsActive')}
|
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{selectedTableEndpoints.filter((e) => !e.isActive).length}{' '}
|
{selectedTableEndpoints.length - selectedActiveCount}{' '}
|
||||||
{translate('::App.DeveloperKitCrudEndpoints.InactiveCount')}
|
{translate('::App.DeveloperKitCrudEndpoints.InactiveCount')}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto">
|
<span className="ml-auto">
|
||||||
|
|
@ -855,6 +1012,23 @@ const CrudEndpointManager: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={!!confirmDeleteTable}
|
||||||
|
type="danger"
|
||||||
|
title={translate('::App.Platform.DeleteConfirmation')}
|
||||||
|
confirmText={translate('::App.DeveloperKitCrudEndpoints.DeleteAll')}
|
||||||
|
cancelText={translate('::App.Platform.Cancel')}
|
||||||
|
confirmButtonColor="red-600"
|
||||||
|
onCancel={() => setConfirmDeleteTable(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
const table = confirmDeleteTable
|
||||||
|
setConfirmDeleteTable(null)
|
||||||
|
if (table) handleDeleteAll(table)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p>{translate('::App.Platform.DeleteConfirmationMessage')}</p>
|
||||||
|
</ConfirmDialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue