Audit bilgileri yapılan değişiklikler kaydediliyor.

This commit is contained in:
Sedat Öztürk 2025-10-18 19:02:58 +03:00
parent 0f9fe71e6b
commit a299ae099d
11 changed files with 451 additions and 196 deletions

View file

@ -3,6 +3,7 @@ using Kurs.Notifications.Application;
using Kurs.Settings; using Kurs.Settings;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account; using Volo.Abp.Account;
using Volo.Abp.Auditing;
using Volo.Abp.AutoMapper; using Volo.Abp.AutoMapper;
using Volo.Abp.FeatureManagement; using Volo.Abp.FeatureManagement;
using Volo.Abp.Identity; using Volo.Abp.Identity;
@ -39,5 +40,11 @@ public class PlatformApplicationModule : AbpModule
options.IsDynamicPermissionStoreEnabled = true; options.IsDynamicPermissionStoreEnabled = true;
options.SaveStaticPermissionsToDatabase = true; options.SaveStaticPermissionsToDatabase = true;
}); });
// ListFormCustomization için audit kaydı kapatılıyor
Configure<AbpAuditingOptions>(options =>
{
options.IgnoredTypes.Add(typeof(Entities.ListFormCustomization));
});
} }
} }

View file

@ -9,7 +9,6 @@ public class Sector : FullAuditedEntity<Guid>, IMultiTenant
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; set; }
Guid? IMultiTenant.TenantId => TenantId; Guid? IMultiTenant.TenantId => TenantId;
} }

View file

@ -750,7 +750,6 @@ public class PlatformDbContext :
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(128); b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.FullName).HasMaxLength(256);
}); });
builder.Entity<ContactTag>(b => builder.Entity<ContactTag>(b =>

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations namespace Kurs.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20251016215302_Initial")] [Migration("20251018150230_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -6070,10 +6070,6 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("DeletionTime"); .HasColumnName("DeletionTime");
b.Property<string>("FullName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")

View file

@ -1068,7 +1068,6 @@ namespace Kurs.Platform.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false), Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
FullName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false), CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true), LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),

View file

@ -6067,10 +6067,6 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2") .HasColumnType("datetime2")
.HasColumnName("DeletionTime"); .HasColumnName("DeletionTime");
b.Property<string>("FullName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("IsDeleted") b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("bit") .HasColumnType("bit")

View file

@ -100,7 +100,9 @@
"props": null, "props": null,
"description": null, "description": null,
"isActive": true, "isActive": true,
"dependencies": ["AxiosListComponent"] "dependencies": [
"AxiosListComponent"
]
} }
], ],
"ReportCategories": [ "ReportCategories": [
@ -120,7 +122,6 @@
"icon": "📜" "icon": "📜"
} }
], ],
"Abouts": [ "Abouts": [
{ {
"stats": [ "stats": [
@ -623,65 +624,97 @@
], ],
"Sectors": [ "Sectors": [
{ {
"Name": "Ambalaj", "Name": "Adalet ve Güvenlik"
"FullName": ""
}, },
{ {
"Name": "Demir Çelik", "Name": "Ağaç İşleri, Kağıt ve Kağıt Ürünleri"
"FullName": "" },
{
"Name": "Ambalaj"
},
{
"Name": "Bilişim Teknolojileri"
},
{
"Name": "Cam, Çimento ve Toprak"
},
{
"Name": "Çevre"
},
{
"Name": "Demir Çelik"
}, },
{ {
"Name": "Diğer", "Name": "Diğer",
"FullName": "" "FullName": ""
}, },
{
"Name": "Eğitim"
},
{ {
"Name": "Elektrik ve Elektronik", "Name": "Elektrik ve Elektronik",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Giyim", "Name": "Enerji"
},
{
"Name": "Finans",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Güvenlik", "Name": "Gıda"
"FullName": ""
}, },
{ {
"Name": "Gıda", "Name": "Giyim"
"FullName": "" },
{
"Name": "Güvenlik"
}, },
{ {
"Name": "Hizmet-servis", "Name": "Hizmet-servis",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Hırdavat ve Nalburiye", "Name": "Hırdavat ve Nalburiye"
"FullName": ""
}, },
{ {
"Name": "Isıtma, Soğutma ve Havalandırma", "Name": "Isıtma, Soğutma ve Havalandırma",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "İnşaat", "Name": "İnşaat"
"FullName": "" },
{
"Name": "İş ve Yönetim"
}, },
{ {
"Name": "Kantar", "Name": "Kantar",
"FullName": "" "FullName": ""
}, },
{
"Name": "Kimya, Petrol, Lastik ve Plastik"
},
{ {
"Name": "Kimyasal", "Name": "Kimyasal",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Kırtasiye", "Name": "Kırtasiye"
"FullName": "" },
{
"Name": "Kültür, Sanat ve Tasarım"
}, },
{ {
"Name": "Laboratuar ve Test Ürünleri", "Name": "Laboratuar ve Test Ürünleri",
"FullName": "" "FullName": ""
}, },
{
"Name": "Maden"
},
{
"Name": "Makine"
},
{ {
"Name": "Makina", "Name": "Makina",
"FullName": "" "FullName": ""
@ -690,37 +723,67 @@
"Name": "Matbaa", "Name": "Matbaa",
"FullName": "" "FullName": ""
}, },
{
"Name": "Medya, İletişim ve Yayıncılık"
},
{
"Name": "Metal"
},
{ {
"Name": "Ofis", "Name": "Ofis",
"FullName": "" "FullName": ""
}, },
{
"Name": "Otomotiv"
},
{ {
"Name": "Oto Tamir-Servis", "Name": "Oto Tamir-Servis",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Pnomatik", "Name": "Pnomatik"
"FullName": ""
},
{
"Name": "Sarf",
"FullName": ""
}, },
{ {
"Name": "Sağlık", "Name": "Sağlık",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Tartı", "Name": "Sağlık ve Sosyal Hizmetler"
},
{
"Name": "Sarf"
},
{
"Name": "Spor ve Rekreasyon",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Transpalet", "Name": "Tarım, Avcılık ve Balıılık"
},
{
"Name": "Tartı"
},
{
"Name": "Tekstil, Hazır Giyim, Deri",
"FullName": "" "FullName": ""
}, },
{ {
"Name": "Yedek Parça", "Name": "Ticaret (Satış ve Pazarlama)"
"FullName": "" },
{
"Name": "Toplumsal ve Kişisel Hizmetler"
},
{
"Name": "Transpalet"
},
{
"Name": "Turizm, Konaklama, Yiyecek-İçecek Hizmetleri"
},
{
"Name": "Ulaştırma, Lojistik ve Haberleşme"
},
{
"Name": "Yedek Parça"
} }
], ],
"SkillTypes": [ "SkillTypes": [

View file

@ -192,7 +192,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
await _sectorRepository.InsertAsync(new Sector await _sectorRepository.InsertAsync(new Sector
{ {
Name = item.Name, Name = item.Name,
FullName = item.FullName
}); });
} }
} }

View file

@ -62,7 +62,6 @@ public class GlobalSearchSeedDto
public class SectorSeedDto public class SectorSeedDto
{ {
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; set; }
} }
public class UomCategorySeedDto public class UomCategorySeedDto

View file

@ -27,6 +27,7 @@ using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore; using OpenIddict.Validation.AspNetCore;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Account.Web; using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Auditing;
using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc;
@ -35,6 +36,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Auditing;
using Volo.Abp.Autofac; using Volo.Abp.Autofac;
using Volo.Abp.BackgroundWorkers.Hangfire; using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.BlobStoring; using Volo.Abp.BlobStoring;
@ -112,6 +114,7 @@ public class PlatformHttpApiHostModule : AbpModule
ConfigureCache(); ConfigureCache();
ConfigureHangfire(context, configuration); ConfigureHangfire(context, configuration);
ConfigureBlobStoring(configuration); ConfigureBlobStoring(configuration);
ConfigureAuditing();
context.Services.AddSignalR(); context.Services.AddSignalR();
@ -351,6 +354,13 @@ public class PlatformHttpApiHostModule : AbpModule
}); });
} }
private void ConfigureAuditing()
{
Configure<AbpAspNetCoreAuditingOptions>(options =>
{
options.IgnoredUrls.Add("/api/app/list-form-customization");
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)
{ {

View file

@ -3,10 +3,19 @@ import Dialog from '@/components/ui/Dialog'
import TabList from '@/components/ui/Tabs/TabList' import TabList from '@/components/ui/Tabs/TabList'
import TabNav from '@/components/ui/Tabs/TabNav' import TabNav from '@/components/ui/Tabs/TabNav'
import TabContent from '@/components/ui/Tabs/TabContent' import TabContent from '@/components/ui/Tabs/TabContent'
import { Tabs } from '@/components/ui' import { Tabs, Badge, Spinner } from '@/components/ui'
import { AdaptableCard } from '@/components/shared' import { AdaptableCard } from '@/components/shared'
import { AuditLogDto } from '@/proxy/auditLog/audit-log' import { AuditLogDto } from '@/proxy/auditLog/audit-log'
import { getAuditLogs } from '@/services/identity.service' import { getAuditLogs } from '@/services/identity.service'
import {
HiOutlineClock,
HiOutlineGlobe,
HiOutlineUser,
HiOutlineCode,
HiOutlineDocumentText,
HiOutlineExclamationCircle,
HiOutlineCheckCircle,
} from 'react-icons/hi'
function AuditLogs({ function AuditLogs({
open, open,
@ -18,6 +27,7 @@ function AuditLogs({
id: string id: string
}) { }) {
const [selectedLog, setSelectedLog] = useState<AuditLogDto>() const [selectedLog, setSelectedLog] = useState<AuditLogDto>()
const [loading, setLoading] = useState(false)
useEffect(() => { useEffect(() => {
if (open && id) { if (open && id) {
@ -26,184 +36,362 @@ function AuditLogs({
}, [open, id]) }, [open, id])
const fetchAuditLog = async (logId: string) => { const fetchAuditLog = async (logId: string) => {
setLoading(true)
try {
const response = await getAuditLogs(logId) const response = await getAuditLogs(logId)
setSelectedLog(response.data) setSelectedLog(response.data)
} catch (error) {
console.error('Failed to fetch audit log:', error)
} finally {
setLoading(false)
}
} }
return ( const getStatusBadge = (statusCode?: number) => {
<Dialog width="80%" isOpen={open} onClose={onDialogClose} onRequestClose={onDialogClose}> if (!statusCode) return <Badge className="bg-gray-500">Unknown</Badge>
<h5 className="text-lg font-semibold mb-4"> if (statusCode >= 200 && statusCode < 300)
Audit Log Detail return <Badge className="bg-green-500" content={statusCode}></Badge>
{selectedLog?.id && ( if (statusCode >= 400 && statusCode < 500)
<span className="text-xs font-normal text-slate-500 ml-2">({selectedLog.id})</span> return <Badge className="bg-yellow-500" content={statusCode}></Badge>
)} if (statusCode >= 500) return <Badge className="bg-red-500" content={statusCode}></Badge>
</h5> return <Badge className="bg-blue-500" content={statusCode}></Badge>
}
{!selectedLog ? ( const getChangeTypeBadge = (changeType: number) => {
<div className="text-center py-6 text-gray-500">Loading...</div> const types = ['Created', 'Updated', 'Deleted']
const colors = ['bg-green-600', 'bg-blue-600', 'bg-red-600']
return (
<Badge
className={colors[changeType] || 'bg-gray-600'}
content={types[changeType] || 'Unknown'}
></Badge>
)
}
const formatDuration = (ms: number) => {
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(2)}s`
}
const InfoRow = ({ icon: Icon, label, value, valueClassName = '' }: any) => (
<div className="flex items-start gap-3 py-2 border-b border-gray-100 last:border-0">
<Icon className="w-5 h-5 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-gray-500 mb-0.5">{label}</div>
<div className={`text-sm ${valueClassName} break-words`}>{value}</div>
</div>
</div>
)
return (
<Dialog width="90%" isOpen={open} onClose={onDialogClose} onRequestClose={onDialogClose}>
{/* Header */}
<div className="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
<div>
<h4 className="text-xl font-bold text-gray-800">Audit Log Details</h4>
{selectedLog?.id && <p className="text-sm text-gray-500 mt-1">ID: {selectedLog.id}</p>}
</div>
{selectedLog?.httpStatusCode && (
<div className="flex items-center gap-2">
{getStatusBadge(selectedLog.httpStatusCode)}
<Badge
className="border border-gray-300 bg-white text-gray-700"
content={selectedLog.applicationName}
></Badge>
</div>
)}
</div>
{loading ? (
<div className="flex items-center justify-center py-16">
<Spinner size={40} />
</div>
) : !selectedLog ? (
<div className="text-center py-16">
<HiOutlineExclamationCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No audit log found</p>
</div>
) : ( ) : (
<Tabs defaultValue="log" variant="pill"> <Tabs defaultValue="log" variant="pill">
<TabList className="flex-wrap border-b mb-4 bg-slate-50 rounded-t"> <TabList className="mb-6 bg-gray-50 p-1 rounded-lg">
<TabNav value="log">Log</TabNav> <TabNav value="log">
<HiOutlineDocumentText className="w-4 h-4 mr-2" />
Overview
</TabNav>
<TabNav value="actions"> <TabNav value="actions">
Actions {selectedLog.actions?.length > 0 && `(${selectedLog.actions.length})`} <HiOutlineCode className="w-4 h-4 mr-2" />
Actions
{selectedLog.actions?.length > 0 && (
<Badge
className="ml-2 bg-blue-500"
content={`${selectedLog.actions.length}`}
></Badge>
)}
</TabNav> </TabNav>
<TabNav value="changes"> <TabNav value="changes">
Entity Changes{' '} <HiOutlineCheckCircle className="w-4 h-4 mr-2" />
{selectedLog.entityChanges?.length > 0 && `(${selectedLog.entityChanges.length})`} Entity Changes
{selectedLog.entityChanges?.length > 0 && (
<Badge
className="ml-2 bg-purple-500"
content={`${selectedLog.entityChanges.length}`}
></Badge>
)}
</TabNav> </TabNav>
</TabList> </TabList>
{/* LOG DETAILS */} {/* OVERVIEW TAB */}
<TabContent value="log"> <TabContent value="log">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<AdaptableCard> {/* Request Information */}
<h6 className="font-medium mb-3 text-slate-600 text-sm">Request Info</h6> <AdaptableCard className="shadow-sm">
<table className="w-full text-xs"> <h6 className="font-semibold text-gray-700 mb-4 flex items-center gap-2">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all"> <HiOutlineGlobe className="w-5 h-5 text-blue-500" />
<tr> Request Information
<td>User</td> </h6>
<td>{selectedLog.userName}</td> <div className="space-y-1">
</tr> <InfoRow
<tr> icon={HiOutlineUser}
<td>Time</td> label="User"
<td>{new Date(selectedLog.executionTime).toLocaleString()}</td> value={selectedLog.userName || 'Anonymous'}
</tr> valueClassName="font-medium text-gray-800"
<tr> />
<td>Duration</td> <InfoRow
<td>{selectedLog.executionDuration} ms</td> icon={HiOutlineClock}
</tr> label="Execution Time"
<tr> value={new Date(selectedLog.executionTime).toLocaleString('tr-TR', {
<td>IP</td> day: '2-digit',
<td>{selectedLog.clientIpAddress}</td> month: '2-digit',
</tr> year: 'numeric',
<tr> hour: '2-digit',
<td>Method</td> minute: '2-digit',
<td>{selectedLog.httpMethod}</td> second: '2-digit',
</tr> })}
<tr> />
<td>Status Code</td> <InfoRow
<td>{selectedLog.httpStatusCode}</td> icon={HiOutlineClock}
</tr> label="Duration"
<tr> value={formatDuration(selectedLog.executionDuration)}
<td>URL</td> valueClassName={
<td>{selectedLog.url}</td> selectedLog.executionDuration > 1000
</tr> ? 'text-orange-600 font-semibold'
</tbody> : 'text-green-600'
</table> }
/>
<InfoRow
icon={HiOutlineGlobe}
label="Client IP"
value={selectedLog.clientIpAddress || 'Unknown'}
/>
</div>
</AdaptableCard> </AdaptableCard>
<AdaptableCard> {/* HTTP Details */}
<h6 className="font-medium mb-3 text-slate-600 text-sm">Client Info</h6> <AdaptableCard className="shadow-sm">
<table className="w-full text-xs"> <h6 className="font-semibold text-gray-700 mb-4 flex items-center gap-2">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all"> <HiOutlineCode className="w-5 h-5 text-green-500" />
<tr> HTTP Details
<td>Browser</td> </h6>
<td>{selectedLog.browserInfo}</td> <div className="space-y-1">
</tr> <InfoRow
<tr> icon={HiOutlineCode}
<td>Exceptions</td> label="Method"
<td> value={
<pre className="bg-slate-100 text-red-500 p-2 rounded text-[11px] whitespace-pre-wrap leading-snug"> <Badge
{selectedLog.exceptions || 'None'} className={
</pre> selectedLog.httpMethod === 'POST'
</td> ? 'bg-blue-500'
</tr> : selectedLog.httpMethod === 'GET'
</tbody> ? 'bg-green-500'
</table> : selectedLog.httpMethod === 'PUT'
? 'bg-yellow-500'
: selectedLog.httpMethod === 'DELETE'
? 'bg-red-500'
: 'bg-gray-500'
}
content={selectedLog.httpMethod}
></Badge>
}
/>
<InfoRow
icon={HiOutlineCheckCircle}
label="Status Code"
value={getStatusBadge(selectedLog.httpStatusCode)}
/>
<InfoRow
icon={HiOutlineGlobe}
label="URL"
value={selectedLog.url}
valueClassName="text-blue-600 font-mono text-xs"
/>
<InfoRow
icon={HiOutlineDocumentText}
label="Browser"
value={
<span className="text-xs text-gray-600 line-clamp-2">
{selectedLog.browserInfo || 'Unknown'}
</span>
}
/>
</div>
</AdaptableCard> </AdaptableCard>
{/* Exceptions (Full Width) */}
{selectedLog.exceptions && (
<AdaptableCard className="lg:col-span-2 shadow-sm border-l-4 border-red-500">
<h6 className="font-semibold text-red-600 mb-3 flex items-center gap-2">
<HiOutlineExclamationCircle className="w-5 h-5" />
Exceptions
</h6>
<pre className="bg-red-50 text-red-700 p-4 rounded-lg text-xs whitespace-pre-wrap leading-relaxed font-mono overflow-x-auto">
{selectedLog.exceptions}
</pre>
</AdaptableCard>
)}
{/* Comments */}
{selectedLog.comments && (
<AdaptableCard className="lg:col-span-2 shadow-sm bg-blue-50">
<h6 className="font-semibold text-blue-700 mb-2 flex items-center gap-2">
<HiOutlineDocumentText className="w-5 h-5" />
Comments
</h6>
<p className="text-sm text-blue-800">{selectedLog.comments}</p>
</AdaptableCard>
)}
</div> </div>
</TabContent> </TabContent>
{/* ACTIONS */} {/* ACTIONS TAB */}
<TabContent value="actions"> <TabContent value="actions">
{selectedLog.actions?.map((action, index) => ( {!selectedLog.actions || selectedLog.actions.length === 0 ? (
<AdaptableCard key={index} className="mb-4"> <div className="text-center py-16">
<h6 className="font-medium text-slate-600 mb-3 text-sm">Action #{index + 1}</h6> <HiOutlineCode className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<table className="w-full text-xs"> <p className="text-gray-500">No actions recorded</p>
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all"> </div>
<tr> ) : (
<td>Service</td> <div className="space-y-4">
<td>{action.serviceName}</td> {selectedLog.actions.map((action, index) => (
</tr> <AdaptableCard
<tr> key={index}
<td>Method</td> className="shadow-sm hover:shadow-md transition-shadow"
<td>{action.methodName}</td> >
</tr> <div className="flex items-start justify-between mb-4">
<tr> <h6 className="font-semibold text-gray-700 flex items-center gap-2">
<td>Time</td> <Badge className="bg-indigo-500" content={`#${index + 1}`}></Badge>
<td>{action.executionTime}</td> <span className="text-sm">{action.methodName}</span>
</tr> </h6>
<tr> <Badge
<td>Duration</td> className="border border-gray-300 bg-white text-gray-700 text-xs"
<td>{action.executionDuration} ms</td> content={formatDuration(action.executionDuration)}
</tr> ></Badge>
<tr> </div>
<td>Parameters</td>
<td> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<pre className="bg-slate-100 p-2 rounded text-[11px] whitespace-pre-wrap leading-snug"> <div>
{action.parameters} <p className="text-xs text-gray-500 mb-1">Service Name</p>
<p className="text-sm font-mono text-gray-700 break-all">
{action.serviceName}
</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-1">Execution Time</p>
<p className="text-sm text-gray-700">
{new Date(action.executionTime).toLocaleString('tr-TR')}
</p>
</div>
</div>
{action.parameters && (
<div>
<p className="text-xs text-gray-500 mb-2">Parameters</p>
<pre className="bg-gray-50 border border-gray-200 p-3 rounded-lg text-xs overflow-x-auto">
{JSON.stringify(JSON.parse(action.parameters), null, 2)}
</pre> </pre>
</td> </div>
</tr> )}
</tbody>
</table>
</AdaptableCard> </AdaptableCard>
))} ))}
</div>
)}
</TabContent> </TabContent>
{/* ENTITY CHANGES */} {/* ENTITY CHANGES TAB */}
<TabContent value="changes"> <TabContent value="changes">
{selectedLog.entityChanges?.map((change, index) => ( {!selectedLog.entityChanges || selectedLog.entityChanges.length === 0 ? (
<AdaptableCard key={index} className="mb-4"> <div className="text-center py-16">
<h6 className="font-medium text-slate-600 mb-3 text-sm">Change #{index + 1}</h6> <HiOutlineCheckCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<table className="w-full text-xs"> <p className="text-gray-500">No entity changes recorded</p>
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all"> </div>
<tr> ) : (
<td>Time</td> <div className="space-y-4">
<td>{new Date(change.changeTime).toLocaleString()}</td> {selectedLog.entityChanges.map((change, index) => (
</tr> <AdaptableCard
<tr> key={index}
<td>Type</td> className="shadow-sm hover:shadow-md transition-shadow"
<td>{change.changeType}</td> >
</tr> <div className="flex items-start justify-between mb-4">
<tr> <div className="flex items-center gap-3">
<td>Entity ID</td> <Badge className="bg-purple-500" content={`#${index + 1}`}></Badge>
<td>{change.entityId}</td> <div>
</tr> <h6 className="font-semibold text-gray-700">
<tr> {change.entityTypeFullName}
<td>Entity Type</td> </h6>
<td>{change.entityTypeFullName}</td> <p className="text-xs text-gray-500">ID: {change.entityId}</p>
</tr> </div>
<tr> </div>
<td>Property Changes</td> <div className="flex items-center gap-2">
<td> {getChangeTypeBadge(change.changeType)}
<ul className="list-disc pl-4 space-y-1"> <span className="text-xs text-gray-500">
{change.propertyChanges.map((p, i) => ( {new Date(change.changeTime).toLocaleTimeString('tr-TR')}
<li key={i}> </span>
<code>{p.propertyName}</code> ({p.propertyTypeFullName}):{' '} </div>
{!p.originalValue || p.originalValue === 'null' ? ( </div>
<span className="text-green-600 font-semibold ml-1">
{p.newValue} {change.propertyChanges && change.propertyChanges.length > 0 && (
<div>
<p className="text-xs font-medium text-gray-600 mb-3">Property Changes</p>
<div className="space-y-2">
{change.propertyChanges.map((prop, i) => (
<div key={i} className="bg-gray-50 p-3 rounded-lg">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<code className="text-sm font-semibold text-indigo-600">
{prop.propertyName}
</code>
<span className="text-xs text-gray-500 ml-2">
({prop.propertyTypeFullName?.split('.').pop()})
</span>
</div>
<div className="flex items-center gap-2 text-sm">
{!prop.originalValue ||
prop.originalValue === 'null' ||
prop.originalValue === '[Not Tracked]' ? (
<span className="px-3 py-1 bg-green-100 text-green-700 rounded-md font-medium">
{prop.newValue || 'null'}
</span> </span>
) : ( ) : (
<> <>
<span className="text-gray-500">{p.originalValue}</span>{' '} <span className="px-3 py-1 bg-gray-100 text-gray-600 rounded-md">
<span className="mx-1"></span> {prop.originalValue}
<span className="text-yellow-600 font-semibold"> </span>
{p.newValue} <span className="text-gray-400"></span>
<span className="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-md font-medium">
{prop.newValue || 'null'}
</span> </span>
</> </>
)} )}
</li> </div>
</div>
</div>
))} ))}
</ul> </div>
</td> </div>
</tr> )}
</tbody>
</table>
</AdaptableCard> </AdaptableCard>
))} ))}
</div>
)}
</TabContent> </TabContent>
</Tabs> </Tabs>
)} )}