sozsoft-platform/ui/src/views/developerKit/CrudEndpointDialog.tsx

856 lines
33 KiB
TypeScript
Raw Normal View History

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'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<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',
}
interface OperationMeta {
icon: ReactNode
/** Aktif endpoint rengi; pasif olanlar gri gosterilir. */
color: string
}
const OPERATION_META: Record<string, OperationMeta> = {
GetList: { icon: <FaList />, color: 'text-blue-500' },
GetById: { icon: <FaSearch />, color: 'text-sky-500' },
Create: { icon: <FaPlus />, color: 'text-green-500' },
Update: { icon: <FaPen />, color: 'text-amber-500' },
Delete: { icon: <FaTrash />, color: 'text-red-500' },
}
export const getOperationMeta = (operationType: string): OperationMeta =>
OPERATION_META[operationType] ?? { icon: <FaList />, color: 'text-gray-400' }
/** Endpoint'leri CRUD_OPERATION_TYPES sirasina gore siralar; bilinmeyenler sona gider. */
export const sortByCrudOperation = <T extends { operationType: string }>(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<string, CrudEndpoint[]> => {
const map = new Map<string, CrudEndpoint[]>()
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 (
<span
className="flex flex-shrink-0 items-center gap-1"
onClick={
onClick
? (e) => {
e.stopPropagation()
onClick()
}
: undefined
}
>
{endpoints.map((endpoint) => {
const meta = getOperationMeta(endpoint.operationType)
return (
<span
key={endpoint.id}
title={buildTitle(endpoint)}
className={`text-[10px] ${endpoint.isActive ? meta.color : 'text-gray-300 dark:text-gray-600'}`}
>
{meta.icon}
</span>
)
})}
</span>
)
}
// ─── Dialog ───────────────────────────────────────────────────────────────────
interface CrudEndpointDialogProps {
isOpen: boolean
/** Endpoint'leri yonetilecek tablo; entity adi tablo adindan turetilir. */
tableName: string
schemaName?: string
/** 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,
dataSourceCode,
onClose,
onChanged,
}: CrudEndpointDialogProps) => {
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION)
const entityName = toEntityName(tableName)
const [endpoints, setEndpoints] = useState<CrudEndpoint[]>([])
const [columns, setColumns] = useState<DatabaseColumnDto[]>([])
const [loading, setLoading] = useState(false)
const [generating, setGenerating] = useState(false)
const [deletingAll, setDeletingAll] = useState(false)
const [busyId, setBusyId] = useState<string | null>(null)
const [expandedId, setExpandedId] = useState<string | null>(null)
const [confirmDeleteAll, setConfirmDeleteAll] = useState(false)
const [testingId, setTestingId] = useState<string | null>(null)
const [testResults, setTestResults] = useState<Record<string, TestResult>>({})
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
const pushError = useCallback((message: string) => {
toast.push(
<Notification type="danger" duration={3000}>
{message}
</Notification>,
{ placement: 'bottom-end' },
)
}, [])
const pushSuccess = useCallback((message: string) => {
toast.push(
<Notification type="success" duration={2000}>
{message}
</Notification>,
{ 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) {
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])
const sampleBodies = useMemo(() => {
if (columns.length === 0) return { create: GENERIC_BODY, update: GENERIC_BODY }
const build = (includeId: boolean) => {
const body: Record<string, unknown> = {}
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)
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 (
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={900}>
<Dialog.Body className="flex flex-col gap-3 mt-2">
<div>
<h5 className="mb-0">{translate('::App.SqlQueryManager.CrudEndpoints')}</h5>
<p className="mb-0 text-sm text-gray-500 dark:text-gray-400">
{schemaName ? `${schemaName}.${tableName}` : tableName}
{' — '}
{entityName}
</p>
</div>
<div className="min-h-[180px] max-h-[55vh] overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center gap-2 py-12 text-gray-400">
<FaSyncAlt className="animate-spin" />
<span className="text-sm">{translate('::App.Platform.LoadingWithThreeDot')}</span>
</div>
) : endpoints.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-400 dark:text-gray-500">
<FaBolt className="mb-3 text-3xl text-gray-200 dark:text-gray-700" />
<p className="mb-0 font-medium">
{translate('::App.DeveloperKitCrudEndpoints.NoEndpointsYet')}
</p>
<p className="mt-1 mb-0 text-sm">
{translate('::App.DeveloperKitCrudEndpoints.ClickToCreate')}
</p>
</div>
) : (
<div className="space-y-2">
{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 (
<div
key={ep.id}
className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"
>
<div className="flex items-center gap-3 bg-white p-2 dark:bg-gray-800">
<Button
size="xs"
variant="plain"
shape="circle"
disabled={busyId === ep.id}
title={
ep.isActive
? translate('::App.DeveloperKitCrudEndpoints.Disable')
: translate('::App.DeveloperKitCrudEndpoints.Enable')
}
onClick={() => handleToggle(ep)}
icon={
busyId === ep.id ? (
<FaSyncAlt className="animate-spin" />
) : ep.isActive ? (
<FaToggleOn className="text-lg text-green-500" />
) : (
<FaToggleOff className="text-lg text-gray-300" />
)
}
/>
<span
className={`flex-shrink-0 rounded border px-2 py-0.5 text-xs font-bold ${
METHOD_COLOR[ep.method] ||
'border-gray-200 bg-gray-100 text-gray-700 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-200'
}`}
>
{ep.method}
</span>
<span className={`flex-shrink-0 ${meta.color}`}>{meta.icon}</span>
<div className="min-w-0 flex-1">
<span className="text-sm font-medium text-gray-800 dark:text-gray-100">
{ep.operationType}
</span>
<code className="ml-2 rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-300">
{ep.path}
</code>
</div>
{testResult && !isExpanded && (
<span
className={`flex-shrink-0 text-xs font-semibold ${
testResult.success ? 'text-green-600' : 'text-red-600'
}`}
>
{testResult.status}
</span>
)}
<Button
size="xs"
variant="plain"
shape="circle"
title={translate('::App.Platform.Delete')}
disabled={busyId === ep.id}
onClick={() => handleDelete(ep)}
icon={<FaTrash className="text-red-500" />}
/>
<Button
size="xs"
variant="plain"
shape="circle"
aria-expanded={isExpanded}
title={translate('::App.DeveloperKitCrudEndpoints.TestDetails')}
onClick={() => setExpandedId(isExpanded ? null : ep.id)}
icon={isExpanded ? <FaChevronDown /> : <FaChevronRight />}
/>
</div>
{isExpanded && (
<div className="space-y-3 border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900">
{parameters.length > 0 && (
<div>
<p className="mb-2 text-xs font-semibold text-gray-600 dark:text-gray-300">
{translate('::App.DeveloperKitCrudEndpoints.Parameters')}
</p>
<div className="space-y-2">
{parameters.map((param) => (
<div key={param.name} className="flex items-center gap-2">
<span
className={`w-32 flex-shrink-0 rounded px-1.5 py-0.5 font-mono text-xs ${
param.type === 'path'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
}`}
>
{param.name}
</span>
<input
type="text"
value={param.value}
aria-label={param.name}
onChange={(e) =>
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"
/>
</div>
))}
</div>
</div>
)}
{needsBody(ep) && (
<div>
<div className="mb-1 flex items-center justify-between">
<span className="text-xs font-semibold text-gray-600 dark:text-gray-300">
{translate('::App.DeveloperKitCrudEndpoints.RequestBody')}
</span>
<Button
size="xs"
variant="plain"
onClick={() =>
setRequestBodies((prev) => ({
...prev,
[ep.id]: getDefaultBody(ep),
}))
}
>
{translate('::App.Platform.Reset')}
</Button>
</div>
<textarea
rows={8}
spellCheck={false}
value={requestBodies[ep.id] ?? getDefaultBody(ep)}
aria-label={translate('::App.DeveloperKitCrudEndpoints.RequestBody')}
onChange={(e) =>
setRequestBodies((prev) => ({ ...prev, [ep.id]: e.target.value }))
}
className="w-full rounded border border-gray-300 bg-white px-2 py-1.5 font-mono text-xs text-gray-900 focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
</div>
)}
<div className="flex items-center gap-2">
<Button
size="sm"
variant="solid"
disabled={isTesting}
onClick={() => handleTest(ep)}
icon={
isTesting ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaPaperPlane />
)
}
>
{isTesting
? translate('::App.DeveloperKitEndpoint.SendLoading')
: translate('::App.DeveloperKitCrudEndpoints.Test')}
</Button>
{testResult && (
<Button
size="sm"
variant="plain"
onClick={() =>
setTestResults((prev) => {
const next = { ...prev }
delete next[ep.id]
return next
})
}
>
{translate('::App.Platform.Clear')}
</Button>
)}
{!ep.isActive && (
<span className="text-xs text-amber-600 dark:text-amber-400">
{translate('::App.DeveloperKitCrudEndpoints.InactiveCount')}
</span>
)}
</div>
{testResult && (
<div
className={`rounded-lg border p-2 ${
testResult.success
? 'border-green-200 bg-green-50 dark:border-green-700 dark:bg-green-900/20'
: 'border-red-200 bg-red-50 dark:border-red-700 dark:bg-red-900/20'
}`}
>
<div className="mb-2 flex items-center gap-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="ml-auto text-xs text-gray-500 dark:text-gray-400">
{testResult.durationMs} ms
</span>
<Button
size="xs"
variant="plain"
shape="circle"
title={translate('::App.Platform.CopyToClipboard')}
onClick={() => copyToClipboard(testResult.payload)}
icon={<FaCopy className="text-xs" />}
/>
</div>
<pre className="max-h-56 overflow-auto rounded border border-gray-200 bg-white p-2 text-xs text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100">
{testResult.payload}
</pre>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{endpoints.length > 0 && (
<div className="flex items-center gap-4 border-t pt-2 text-xs text-gray-500 dark:text-gray-400">
<span>
{activeCount} {translate('::App.Listform.ListformField.IsActive')}
</span>
<span>
{endpoints.length - activeCount}{' '}
{translate('::App.DeveloperKitCrudEndpoints.InactiveCount')}
</span>
</div>
)}
</Dialog.Body>
<Dialog.Footer className="mt-1 flex flex-wrap items-center gap-2 border-t pt-3">
{endpoints.length > 0 && (
<Button
size="sm"
variant="default"
icon={deletingAll ? <FaSyncAlt className="animate-spin" /> : <FaTrash />}
disabled={busy}
onClick={() => setConfirmDeleteAll(true)}
className="text-red-600"
>
{translate('::App.DeveloperKitCrudEndpoints.DeleteAll')}
</Button>
)}
<Button
size="sm"
variant="solid"
icon={generating ? <FaSyncAlt className="animate-spin" /> : <FaBolt />}
disabled={busy}
onClick={handleGenerate}
>
{endpoints.length > 0
? translate('::App.DeveloperKitCrudEndpoints.Regenerate')
: translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpoint')}
</Button>
<Button className="ml-auto" variant="solid" onClick={onClose}>
{translate('::App.Platform.Close')}
</Button>
</Dialog.Footer>
<ConfirmDialog
isOpen={confirmDeleteAll}
type="danger"
title={translate('::App.Platform.DeleteConfirmation')}
confirmText={translate('::App.DeveloperKitCrudEndpoints.DeleteAll')}
cancelText={translate('::App.Platform.Cancel')}
confirmButtonColor="red-600"
onCancel={() => setConfirmDeleteAll(false)}
onConfirm={() => {
setConfirmDeleteAll(false)
handleDeleteAll()
}}
>
<p>{translate('::App.Platform.DeleteConfirmationMessage')}</p>
</ConfirmDialog>
</Dialog>
)
}
export default CrudEndpointDialog