Improve the frontend 5

This commit is contained in:
Urtzi Alfaro
2025-11-02 20:24:44 +01:00
parent 0220da1725
commit 5adb0e39c0
90 changed files with 10658 additions and 2548 deletions

View File

@@ -0,0 +1,115 @@
/**
* Audit Logs React Query hooks
*
* Provides React Query hooks for fetching and managing audit logs
* across all microservices with caching and real-time updates.
*
* Last Updated: 2025-11-02
* Status: ✅ Complete
*/
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { auditLogsService } from '../services/auditLogs';
import {
AuditLogResponse,
AuditLogFilters,
AuditLogListResponse,
AuditLogStatsResponse,
AggregatedAuditLog,
AuditLogServiceName,
} from '../types/auditLogs';
import { ApiError } from '../client';
// Query Keys
export const auditLogKeys = {
all: ['audit-logs'] as const,
lists: () => [...auditLogKeys.all, 'list'] as const,
list: (tenantId: string, filters?: AuditLogFilters) =>
[...auditLogKeys.lists(), tenantId, filters] as const,
serviceList: (tenantId: string, service: AuditLogServiceName, filters?: AuditLogFilters) =>
[...auditLogKeys.lists(), 'service', tenantId, service, filters] as const,
stats: () => [...auditLogKeys.all, 'stats'] as const,
stat: (tenantId: string, filters?: { start_date?: string; end_date?: string }) =>
[...auditLogKeys.stats(), tenantId, filters] as const,
serviceStat: (
tenantId: string,
service: AuditLogServiceName,
filters?: { start_date?: string; end_date?: string }
) => [...auditLogKeys.stats(), 'service', tenantId, service, filters] as const,
} as const;
/**
* Hook to fetch audit logs from a single service
*/
export function useServiceAuditLogs(
tenantId: string,
serviceName: AuditLogServiceName,
filters?: AuditLogFilters,
options?: Omit<UseQueryOptions<AuditLogListResponse, ApiError>, 'queryKey' | 'queryFn'>
) {
return useQuery<AuditLogListResponse, ApiError>({
queryKey: auditLogKeys.serviceList(tenantId, serviceName, filters),
queryFn: () => auditLogsService.getServiceAuditLogs(tenantId, serviceName, filters),
enabled: !!tenantId,
staleTime: 30000, // 30 seconds
...options,
});
}
/**
* Hook to fetch aggregated audit logs from ALL services
*/
export function useAllAuditLogs(
tenantId: string,
filters?: AuditLogFilters,
options?: Omit<UseQueryOptions<AggregatedAuditLog[], ApiError>, 'queryKey' | 'queryFn'>
) {
return useQuery<AggregatedAuditLog[], ApiError>({
queryKey: auditLogKeys.list(tenantId, filters),
queryFn: () => auditLogsService.getAllAuditLogs(tenantId, filters),
enabled: !!tenantId,
staleTime: 30000, // 30 seconds
...options,
});
}
/**
* Hook to fetch audit log statistics from a single service
*/
export function useServiceAuditLogStats(
tenantId: string,
serviceName: AuditLogServiceName,
filters?: {
start_date?: string;
end_date?: string;
},
options?: Omit<UseQueryOptions<AuditLogStatsResponse, ApiError>, 'queryKey' | 'queryFn'>
) {
return useQuery<AuditLogStatsResponse, ApiError>({
queryKey: auditLogKeys.serviceStat(tenantId, serviceName, filters),
queryFn: () => auditLogsService.getServiceAuditLogStats(tenantId, serviceName, filters),
enabled: !!tenantId,
staleTime: 60000, // 1 minute
...options,
});
}
/**
* Hook to fetch aggregated audit log statistics from ALL services
*/
export function useAllAuditLogStats(
tenantId: string,
filters?: {
start_date?: string;
end_date?: string;
},
options?: Omit<UseQueryOptions<AuditLogStatsResponse, ApiError>, 'queryKey' | 'queryFn'>
) {
return useQuery<AuditLogStatsResponse, ApiError>({
queryKey: auditLogKeys.stat(tenantId, filters),
queryFn: () => auditLogsService.getAllAuditLogStats(tenantId, filters),
enabled: !!tenantId,
staleTime: 60000, // 1 minute
...options,
});
}

View File

@@ -0,0 +1,267 @@
// ================================================================
// frontend/src/api/services/auditLogs.ts
// ================================================================
/**
* Audit Logs Aggregation Service
*
* Aggregates audit logs from all microservices and provides
* unified access to system event history.
*
* Backend endpoints:
* - GET /tenants/{tenant_id}/{service}/audit-logs
* - GET /tenants/{tenant_id}/{service}/audit-logs/stats
*
* Last Updated: 2025-11-02
* Status: ✅ Complete - Multi-service aggregation
*/
import { apiClient } from '../client';
import {
AuditLogResponse,
AuditLogFilters,
AuditLogListResponse,
AuditLogStatsResponse,
AggregatedAuditLog,
AUDIT_LOG_SERVICES,
AuditLogServiceName,
} from '../types/auditLogs';
export class AuditLogsService {
private readonly baseUrl = '/tenants';
/**
* Get audit logs from a single service
*/
async getServiceAuditLogs(
tenantId: string,
serviceName: AuditLogServiceName,
filters?: AuditLogFilters
): Promise<AuditLogListResponse> {
const queryParams = new URLSearchParams();
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
if (filters?.user_id) queryParams.append('user_id', filters.user_id);
if (filters?.action) queryParams.append('action', filters.action);
if (filters?.resource_type) queryParams.append('resource_type', filters.resource_type);
if (filters?.severity) queryParams.append('severity', filters.severity);
if (filters?.search) queryParams.append('search', filters.search);
if (filters?.limit) queryParams.append('limit', filters.limit.toString());
if (filters?.offset) queryParams.append('offset', filters.offset.toString());
const url = `${this.baseUrl}/${tenantId}/${serviceName}/audit-logs${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiClient.get<AuditLogListResponse>(url);
}
/**
* Get audit log statistics from a single service
*/
async getServiceAuditLogStats(
tenantId: string,
serviceName: AuditLogServiceName,
filters?: {
start_date?: string;
end_date?: string;
}
): Promise<AuditLogStatsResponse> {
const queryParams = new URLSearchParams();
if (filters?.start_date) queryParams.append('start_date', filters.start_date);
if (filters?.end_date) queryParams.append('end_date', filters.end_date);
const url = `${this.baseUrl}/${tenantId}/${serviceName}/audit-logs/stats${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return apiClient.get<AuditLogStatsResponse>(url);
}
/**
* Get aggregated audit logs from ALL services
* Makes parallel requests to all services and combines results
*/
async getAllAuditLogs(
tenantId: string,
filters?: AuditLogFilters
): Promise<AggregatedAuditLog[]> {
// Make parallel requests to all services
const promises = AUDIT_LOG_SERVICES.map(service =>
this.getServiceAuditLogs(tenantId, service, {
...filters,
limit: filters?.limit || 100,
}).catch(error => {
// If a service fails, log the error but don't fail the entire request
console.warn(`Failed to fetch audit logs from ${service}:`, error);
return { items: [], total: 0, limit: 0, offset: 0, has_more: false };
})
);
const results = await Promise.all(promises);
// Combine all results
const allLogs: AggregatedAuditLog[] = results.flatMap(result => result.items);
// Sort by created_at descending (most recent first)
allLogs.sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return dateB - dateA;
});
// Apply limit if specified
const limit = filters?.limit || 100;
const offset = filters?.offset || 0;
return allLogs.slice(offset, offset + limit);
}
/**
* Get aggregated statistics from ALL services
*/
async getAllAuditLogStats(
tenantId: string,
filters?: {
start_date?: string;
end_date?: string;
}
): Promise<AuditLogStatsResponse> {
// Make parallel requests to all services
const promises = AUDIT_LOG_SERVICES.map(service =>
this.getServiceAuditLogStats(tenantId, service, filters).catch(error => {
console.warn(`Failed to fetch audit log stats from ${service}:`, error);
return {
total_events: 0,
events_by_action: {},
events_by_severity: {},
events_by_resource_type: {},
date_range: { min: null, max: null },
};
})
);
const results = await Promise.all(promises);
// Aggregate statistics
const aggregated: AuditLogStatsResponse = {
total_events: 0,
events_by_action: {},
events_by_severity: {},
events_by_resource_type: {},
date_range: { min: null, max: null },
};
for (const result of results) {
aggregated.total_events += result.total_events;
// Merge events_by_action
for (const [action, count] of Object.entries(result.events_by_action)) {
aggregated.events_by_action[action] = (aggregated.events_by_action[action] || 0) + count;
}
// Merge events_by_severity
for (const [severity, count] of Object.entries(result.events_by_severity)) {
aggregated.events_by_severity[severity] = (aggregated.events_by_severity[severity] || 0) + count;
}
// Merge events_by_resource_type
for (const [resource, count] of Object.entries(result.events_by_resource_type)) {
aggregated.events_by_resource_type[resource] = (aggregated.events_by_resource_type[resource] || 0) + count;
}
// Update date range
if (result.date_range.min) {
if (!aggregated.date_range.min || result.date_range.min < aggregated.date_range.min) {
aggregated.date_range.min = result.date_range.min;
}
}
if (result.date_range.max) {
if (!aggregated.date_range.max || result.date_range.max > aggregated.date_range.max) {
aggregated.date_range.max = result.date_range.max;
}
}
}
return aggregated;
}
/**
* Export audit logs to CSV format
*/
exportToCSV(logs: AggregatedAuditLog[]): string {
if (logs.length === 0) return '';
const headers = [
'Timestamp',
'Service',
'User ID',
'Action',
'Resource Type',
'Resource ID',
'Severity',
'Description',
'IP Address',
'Endpoint',
'Method',
];
const rows = logs.map(log => [
log.created_at,
log.service_name,
log.user_id || '',
log.action,
log.resource_type,
log.resource_id || '',
log.severity,
log.description,
log.ip_address || '',
log.endpoint || '',
log.method || '',
]);
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(',')),
].join('\n');
return csvContent;
}
/**
* Export audit logs to JSON format
*/
exportToJSON(logs: AggregatedAuditLog[]): string {
return JSON.stringify(logs, null, 2);
}
/**
* Download audit logs as a file
*/
downloadAuditLogs(
logs: AggregatedAuditLog[],
format: 'csv' | 'json',
filename?: string
): void {
const content = format === 'csv' ? this.exportToCSV(logs) : this.exportToJSON(logs);
const blob = new Blob([content], {
type: format === 'csv' ? 'text/csv;charset=utf-8;' : 'application/json',
});
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute(
'download',
filename || `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`
);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
// Export singleton instance
export const auditLogsService = new AuditLogsService();

View File

@@ -0,0 +1,84 @@
// ================================================================
// frontend/src/api/types/auditLogs.ts
// ================================================================
/**
* Audit Log Types - TypeScript interfaces for audit log data
*
* Aligned with backend schema:
* - shared/models/audit_log_schemas.py
*
* Last Updated: 2025-11-02
* Status: ✅ Complete - Aligned with backend
*/
export interface AuditLogResponse {
id: string;
tenant_id: string;
user_id: string | null;
service_name: string;
action: string;
resource_type: string;
resource_id: string | null;
severity: 'low' | 'medium' | 'high' | 'critical';
description: string;
changes: Record<string, any> | null;
audit_metadata: Record<string, any> | null;
endpoint: string | null;
method: string | null;
ip_address: string | null;
user_agent: string | null;
created_at: string;
}
export interface AuditLogFilters {
start_date?: string;
end_date?: string;
user_id?: string;
action?: string;
resource_type?: string;
severity?: 'low' | 'medium' | 'high' | 'critical';
search?: string;
limit?: number;
offset?: number;
}
export interface AuditLogListResponse {
items: AuditLogResponse[];
total: number;
limit: number;
offset: number;
has_more: boolean;
}
export interface AuditLogStatsResponse {
total_events: number;
events_by_action: Record<string, number>;
events_by_severity: Record<string, number>;
events_by_resource_type: Record<string, number>;
date_range: {
min: string | null;
max: string | null;
};
}
// Aggregated audit log (combines logs from all services)
export interface AggregatedAuditLog extends AuditLogResponse {
// All fields from AuditLogResponse, service_name distinguishes the source
}
// Service list for audit log aggregation
export const AUDIT_LOG_SERVICES = [
'sales',
'inventory',
'orders',
'production',
'recipes',
'suppliers',
'pos',
'training',
'notification',
'external',
'forecasting',
] as const;
export type AuditLogServiceName = typeof AUDIT_LOG_SERVICES[number];