AuditLog
SubForms kısmında ListFormCode dahil edildi
This commit is contained in:
parent
119c3650f0
commit
1c472a7d9a
7 changed files with 233 additions and 65 deletions
|
|
@ -0,0 +1,9 @@
|
|||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Sozsoft.Platform.AuditLogs;
|
||||
|
||||
public class AuditLogListRequestDto : PagedAndSortedResultRequestDto
|
||||
{
|
||||
public string ListFormCode { get; set; }
|
||||
public string EntityId { get; set; }
|
||||
}
|
||||
|
|
@ -1,30 +1,43 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Sozsoft.Platform.ListForms;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.AuditLogging;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.Uow;
|
||||
using static Sozsoft.Platform.Data.Seeds.SeedConsts;
|
||||
|
||||
namespace Sozsoft.Platform.AuditLogs;
|
||||
|
||||
public interface IAuditLogAppService
|
||||
: ICrudAppService<AuditLogDto, Guid>
|
||||
: ICrudAppService<AuditLogDto, Guid, AuditLogListRequestDto>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Authorize(AppCodes.IdentityManagement.AuditLogs)]
|
||||
public class AuditLogAppService
|
||||
: CrudAppService<AuditLog, AuditLogDto, Guid>
|
||||
, IAuditLogAppService
|
||||
public class AuditLogAppService : CrudAppService<
|
||||
AuditLog,
|
||||
AuditLogDto,
|
||||
Guid,
|
||||
AuditLogListRequestDto>, IAuditLogAppService
|
||||
{
|
||||
public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository)
|
||||
private readonly IRepository<ListForm, Guid> _listFormRepository;
|
||||
|
||||
public AuditLogAppService(
|
||||
IAuditLogRepository auditLogRepository,
|
||||
IRepository<ListForm, Guid> listFormRepository
|
||||
) : base(auditLogRepository)
|
||||
{
|
||||
_listFormRepository = listFormRepository;
|
||||
}
|
||||
|
||||
public override async Task<AuditLogDto> GetAsync(Guid id)
|
||||
|
|
@ -35,23 +48,26 @@ public class AuditLogAppService
|
|||
}
|
||||
|
||||
[UnitOfWork]
|
||||
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(PagedAndSortedResultRequestDto input)
|
||||
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(AuditLogListRequestDto input)
|
||||
{
|
||||
var query = await CreateFilteredQueryAsync(input);
|
||||
var query = await Repository.WithDetailsAsync();
|
||||
|
||||
if (!input.ListFormCode.IsNullOrWhiteSpace())
|
||||
{
|
||||
var filterRules = await GetListFormFilterRulesAsync(input.ListFormCode);
|
||||
query = ApplyAuditLogActionParametersFilter(query, filterRules, input.EntityId);
|
||||
}
|
||||
else if (!input.EntityId.IsNullOrWhiteSpace())
|
||||
{
|
||||
query = query.Where(a => a.Actions.Any(action => action.Parameters.Contains(input.EntityId)));
|
||||
}
|
||||
|
||||
var totalCount = await AsyncExecuter.CountAsync(query);
|
||||
|
||||
query = ApplySorting(query, input);
|
||||
query = ApplyPaging(query, input);
|
||||
|
||||
// EntityChanges ile birlikte getir (N+1 query önlenir)
|
||||
var auditLogRepository = (IAuditLogRepository)Repository;
|
||||
var auditLogsWithDetails = await auditLogRepository.GetListAsync(
|
||||
sorting: input.Sorting,
|
||||
maxResultCount: input.MaxResultCount,
|
||||
skipCount: input.SkipCount,
|
||||
includeDetails: true
|
||||
);
|
||||
var auditLogsWithDetails = await AsyncExecuter.ToListAsync(query);
|
||||
|
||||
// Mapping tek seferde yap
|
||||
var entityDtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(auditLogsWithDetails);
|
||||
|
|
@ -69,6 +85,102 @@ public class AuditLogAppService
|
|||
);
|
||||
}
|
||||
|
||||
private async Task<List<AuditLogListFormFilterRule>> GetListFormFilterRulesAsync(string listFormCode)
|
||||
{
|
||||
var rules = new List<AuditLogListFormFilterRule>
|
||||
{
|
||||
new(listFormCode, null)
|
||||
};
|
||||
|
||||
var listForm = await _listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||
if (listForm?.SubFormsJson.IsNullOrWhiteSpace() != false)
|
||||
{
|
||||
return rules;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var subForms = JsonSerializer.Deserialize<List<SubFormDto>>(listForm.SubFormsJson) ?? [];
|
||||
foreach (var subForm in subForms.Where(a => !a.Code.IsNullOrWhiteSpace()))
|
||||
{
|
||||
var childFieldNames = subForm.Relation?
|
||||
.Select(a => a.ChildFieldName)
|
||||
.Where(a => !a.IsNullOrWhiteSpace())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList() ?? [];
|
||||
|
||||
rules.AddRange(childFieldNames.Select(childFieldName => new AuditLogListFormFilterRule(subForm.Code, childFieldName)));
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Invalid subform JSON should not block audit log listing for the main form.
|
||||
}
|
||||
|
||||
return rules
|
||||
.DistinctBy(a => $"{a.ListFormCode}|{a.ChildFieldName}".ToLowerInvariant())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IQueryable<AuditLog> ApplyAuditLogActionParametersFilter(
|
||||
IQueryable<AuditLog> query,
|
||||
List<AuditLogListFormFilterRule> rules,
|
||||
string entityId)
|
||||
{
|
||||
var validRules = rules
|
||||
.Where(rule => !rule.ListFormCode.IsNullOrWhiteSpace())
|
||||
.ToList();
|
||||
|
||||
if (validRules.Count == 0)
|
||||
{
|
||||
return query;
|
||||
}
|
||||
|
||||
var auditLog = Expression.Parameter(typeof(AuditLog), "auditLog");
|
||||
var action = Expression.Parameter(typeof(AuditLogAction), "action");
|
||||
var parameters = Expression.Property(action, nameof(AuditLogAction.Parameters));
|
||||
|
||||
Expression actionBody = Expression.Constant(false);
|
||||
foreach (var rule in validRules)
|
||||
{
|
||||
Expression ruleBody = Contains(parameters, rule.ListFormCode);
|
||||
if (!entityId.IsNullOrWhiteSpace())
|
||||
{
|
||||
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, entityId));
|
||||
}
|
||||
|
||||
if (!rule.ChildFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, rule.ChildFieldName));
|
||||
}
|
||||
|
||||
actionBody = Expression.OrElse(actionBody, ruleBody);
|
||||
}
|
||||
|
||||
var actions = Expression.Property(auditLog, nameof(AuditLog.Actions));
|
||||
var actionPredicate = Expression.Lambda<Func<AuditLogAction, bool>>(actionBody, action);
|
||||
var anyCall = Expression.Call(
|
||||
typeof(Enumerable),
|
||||
nameof(Enumerable.Any),
|
||||
[typeof(AuditLogAction)],
|
||||
actions,
|
||||
actionPredicate);
|
||||
|
||||
var auditLogPredicate = Expression.Lambda<Func<AuditLog, bool>>(anyCall, auditLog);
|
||||
return query.Where(auditLogPredicate);
|
||||
}
|
||||
|
||||
private static MethodCallExpression Contains(MemberExpression source, string value)
|
||||
{
|
||||
return Expression.Call(
|
||||
source,
|
||||
nameof(string.Contains),
|
||||
Type.EmptyTypes,
|
||||
Expression.Constant(value));
|
||||
}
|
||||
|
||||
private sealed record AuditLogListFormFilterRule(string ListFormCode, string? ChildFieldName);
|
||||
|
||||
// Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz
|
||||
[RemoteService(IsEnabled = false)]
|
||||
public override Task<AuditLogDto> CreateAsync(AuditLogDto input)
|
||||
|
|
|
|||
|
|
@ -2302,7 +2302,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
|
||||
PagerOptionJson = DefaultPagerOptionJson,
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false),
|
||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, false, false),
|
||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
|
||||
new() {
|
||||
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[
|
||||
|
|
|
|||
24
ui/src/services/auditLog.service.ts
Normal file
24
ui/src/services/auditLog.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { PagedResultDto } from '@/proxy'
|
||||
import { AuditLogDto } from '@/proxy/auditLog/audit-log'
|
||||
import apiService from '@/services/api.service'
|
||||
|
||||
export interface AuditLogListRequestDto {
|
||||
skipCount?: number
|
||||
maxResultCount?: number
|
||||
sorting?: string
|
||||
listFormCode?: string
|
||||
entityId?: string
|
||||
}
|
||||
|
||||
class AuditLogService {
|
||||
async getList(params?: AuditLogListRequestDto): Promise<PagedResultDto<AuditLogDto>> {
|
||||
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
|
||||
url: '/api/app/audit-log',
|
||||
method: 'GET',
|
||||
params,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
export const auditLogService = new AuditLogService()
|
||||
|
|
@ -17,16 +17,17 @@ import TabNav from '@/components/ui/Tabs/TabNav'
|
|||
import { NoteDto } from '@/proxy/note/models'
|
||||
import { AVATAR_URL } from '@/constants/app.constant'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import apiService from '@/services/api.service'
|
||||
import { PagedResultDto } from '@/proxy'
|
||||
import { AuditLogActionDto, AuditLogDto } from '@/proxy/auditLog/audit-log'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { auditLogService } from '@/services/auditLog.service'
|
||||
|
||||
interface NoteListProps {
|
||||
notes: NoteDto[]
|
||||
entityName: string
|
||||
entityId: string
|
||||
isVisible?: boolean
|
||||
onAddNote?: () => void
|
||||
onRefreshNotes?: () => void
|
||||
onDeleteNote?: (noteId: string) => void
|
||||
onDownloadFile?: (fileData: any) => void
|
||||
}
|
||||
|
|
@ -35,7 +36,9 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
notes,
|
||||
entityName,
|
||||
entityId,
|
||||
isVisible = true,
|
||||
onAddNote,
|
||||
onRefreshNotes,
|
||||
onDeleteNote,
|
||||
onDownloadFile,
|
||||
}) => {
|
||||
|
|
@ -215,7 +218,18 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
const getRowLabelIfMatches = (input: any): string | null => {
|
||||
if (!entityIdNormalized) return null
|
||||
const inputFormCode = normalize(input?.listFormCode)
|
||||
if (!inputFormCode || inputFormCode !== listFormCodeNormalized) return null
|
||||
if (!inputFormCode) return null
|
||||
const data = input?.data
|
||||
const isMainListForm = inputFormCode === listFormCodeNormalized
|
||||
|
||||
if (!isMainListForm) {
|
||||
if (!data || typeof data !== 'object') return null
|
||||
const hit = findMatchingValueInData(data, entityIdNormalized)
|
||||
if (!hit) return null
|
||||
|
||||
const nameValue = (data as any)?.Name ?? (data as any)?.name
|
||||
return nameValue ? String(nameValue) : String(hit.value)
|
||||
}
|
||||
|
||||
const keys = getKeysFromInput(input)
|
||||
.map((k) => normalize(k))
|
||||
|
|
@ -227,7 +241,6 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
}
|
||||
|
||||
// Some entities may use a different PK than the visible row key; allow strict match via input.data too.
|
||||
const data = input?.data
|
||||
if (data && typeof data === 'object') {
|
||||
const hit = findMatchingValueInData(data, entityIdNormalized)
|
||||
if (hit) {
|
||||
|
|
@ -240,7 +253,6 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
}
|
||||
|
||||
// insert: keys is null, match by scanning input.data for entity id/name/code/etc.
|
||||
const data = input?.data
|
||||
if (data && typeof data === 'object') {
|
||||
const hit = findMatchingValueInData(data, entityIdNormalized)
|
||||
if (!hit) return null
|
||||
|
|
@ -277,17 +289,15 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
setAuditLoading(true)
|
||||
setAuditError(null)
|
||||
try {
|
||||
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
|
||||
method: 'GET',
|
||||
url: '/api/app/audit-log',
|
||||
params: {
|
||||
const response = await auditLogService.getList({
|
||||
skipCount: 0,
|
||||
maxResultCount: 200,
|
||||
sorting: 'ExecutionTime DESC',
|
||||
},
|
||||
listFormCode: entityName,
|
||||
entityId,
|
||||
})
|
||||
|
||||
const items = response.data?.items ?? []
|
||||
const items = response?.items ?? []
|
||||
const filtered = items
|
||||
.map((log) => ({ log, matchedActions: buildMatchedActions(log) }))
|
||||
.filter((x) => x.matchedActions.length > 0)
|
||||
|
|
@ -305,13 +315,13 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTab !== 'audit') return
|
||||
if (!isVisible) return
|
||||
if (!listFormCodeNormalized && !entityIdNormalized) return
|
||||
const key = `${listFormCodeNormalized}|${entityIdNormalized}`
|
||||
if (auditLoadedKey === key) return
|
||||
loadAuditLogs()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentTab, listFormCodeNormalized, entityIdNormalized])
|
||||
}, [isVisible, listFormCodeNormalized, entityIdNormalized])
|
||||
|
||||
const getStatusBadge = (statusCode?: number) => {
|
||||
if (!statusCode) return <Badge className="bg-gray-500" content="?" />
|
||||
|
|
@ -330,7 +340,7 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
onChange={(val) => setCurrentTab(val as 'notes' | 'audit')}
|
||||
variant="underline"
|
||||
>
|
||||
<TabList className="mb-4 border-0 dark:bg-gray-800">
|
||||
<TabList className="mb-2 border-0 dark:bg-gray-800">
|
||||
<TabNav value="notes">
|
||||
{translate('::ListForms.ListForm.Notes')}
|
||||
<Badge className="ml-2 bg-blue-500" content={`${notes?.length ?? 0}`} />
|
||||
|
|
@ -341,12 +351,13 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
</TabNav>
|
||||
</TabList>
|
||||
|
||||
<TabContent value="notes">
|
||||
<div className="flex items-center justify-end mb-2">
|
||||
<div className="mb-2 flex min-h-10 items-center justify-end border-y border-gray-200 bg-gray-50 px-1 py-1 dark:border-gray-700 dark:bg-gray-900">
|
||||
{currentTab === 'notes' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
icon={<FaPlus className="mr-1" />}
|
||||
size="xs"
|
||||
icon={<FaPlus />}
|
||||
type="button"
|
||||
onClick={onAddNote}
|
||||
disabled={!onAddNote}
|
||||
|
|
@ -354,8 +365,34 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
>
|
||||
{translate('::ListForms.ListForm.AddNote')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
type="button"
|
||||
icon={<FaSyncAlt />}
|
||||
onClick={onRefreshNotes}
|
||||
disabled={!onRefreshNotes}
|
||||
className="flex items-center"
|
||||
>
|
||||
{translate('::ListForms.ListForm.Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
type="button"
|
||||
icon={<FaSyncAlt />}
|
||||
onClick={loadAuditLogs}
|
||||
disabled={auditLoading}
|
||||
className="flex items-center"
|
||||
>
|
||||
{translate('::ListForms.ListForm.Refresh')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TabContent value="notes">
|
||||
{(notes?.length ?? 0) === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
|
||||
<FaStickyNote className="text-4xl mb-2 opacity-50" />
|
||||
|
|
@ -449,20 +486,6 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
</TabContent>
|
||||
|
||||
<TabContent value="audit">
|
||||
<div className="flex items-center justify-end mb-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
type="button"
|
||||
icon={<FaSyncAlt className="mr-1" />}
|
||||
onClick={loadAuditLogs}
|
||||
disabled={auditLoading}
|
||||
className="flex items-center"
|
||||
>
|
||||
{translate('::ListForms.ListForm.Refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{auditLoading ? (
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Spinner size={32} />
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ function NoteModalContent({
|
|||
{/* Başlık */}
|
||||
<div className="flex items-center justify-between mb-5 flex-shrink-0">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 rounded-full">
|
||||
<div className="p-1 bg-purple-100 rounded-full">
|
||||
<FaPlus className="text-purple-600 text-lg" />
|
||||
</div>
|
||||
{translate('::ListForms.ListForm.AddNote')}
|
||||
|
|
@ -116,7 +116,7 @@ function NoteModalContent({
|
|||
{types.map((t) => (
|
||||
<label
|
||||
key={t.value}
|
||||
className="flex items-center gap-2 px-2 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
|
||||
className="flex items-center gap-2 px-1 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
|
||||
>
|
||||
<Radio
|
||||
value={t.value}
|
||||
|
|
@ -139,8 +139,9 @@ function NoteModalContent({
|
|||
<Field
|
||||
type="text"
|
||||
name="subject"
|
||||
as={Input}
|
||||
placeholder={translate('::ListForms.ListForm.NoteModal.Subject')}
|
||||
component={Input}
|
||||
autoFocus
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
|
|
@ -198,7 +199,7 @@ function NoteModalContent({
|
|||
|
||||
{/* DOSYA YÜKLEME */}
|
||||
<FormItem>
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-2 text-center hover:border-purple-400 transition-colors duration-200">
|
||||
<Upload
|
||||
className="cursor-pointer"
|
||||
showList={false}
|
||||
|
|
@ -255,7 +256,7 @@ function NoteModalContent({
|
|||
</FormContainer>
|
||||
|
||||
{/* ALT BUTONLAR */}
|
||||
<Dialog.Footer className="mt-5 flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<Dialog.Footer className="mt-2 flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const NotePanel: React.FC<NotePanelProps> = ({
|
|||
|
||||
useEffect(() => {
|
||||
if (isVisible) fetchActivities()
|
||||
}, [isVisible])
|
||||
}, [isVisible, entityName, entityId])
|
||||
|
||||
const handleDownloadFile = async (fileData: any) => {
|
||||
if (!fileData?.SavedFileName) return
|
||||
|
|
@ -62,8 +62,6 @@ export const NotePanel: React.FC<NotePanelProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const getTotalCount = () => activities.length
|
||||
|
||||
// Draggable button handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!buttonRef.current) return
|
||||
|
|
@ -134,7 +132,6 @@ export const NotePanel: React.FC<NotePanelProps> = ({
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
|
||||
{getTotalCount() > 0 && <Badge content={getTotalCount()} />}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -187,7 +184,9 @@ export const NotePanel: React.FC<NotePanelProps> = ({
|
|||
notes={activities}
|
||||
entityName={entityName}
|
||||
entityId={entityId}
|
||||
isVisible={isVisible}
|
||||
onAddNote={() => setShowAddModal(true)}
|
||||
onRefreshNotes={fetchActivities}
|
||||
onDeleteNote={handleDeleteActivity}
|
||||
onDownloadFile={handleDownloadFile}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue