Fix Admin Interface
This commit is contained in:
20
.env.development
Normal file
20
.env.development
Normal file
@@ -0,0 +1,20 @@
|
||||
# Development Environment Variables
|
||||
# This file is for local development only - NOT for production k8s deployment
|
||||
# In k8s, these values come from ConfigMaps and Secrets
|
||||
|
||||
# Frontend Vite Configuration (build-time only)
|
||||
VITE_AUTH0_DOMAIN=motovaultpro.us.auth0.com
|
||||
VITE_AUTH0_CLIENT_ID=yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3
|
||||
VITE_AUTH0_AUDIENCE=https://api.motovaultpro.com
|
||||
VITE_API_BASE_URL=/api
|
||||
|
||||
# Docker Compose Development Configuration
|
||||
# These variables are used by docker-compose for container build args only
|
||||
AUTH0_DOMAIN=motovaultpro.us.auth0.com
|
||||
AUTH0_CLIENT_ID=yspR8zdnSxmV8wFIghHynQ08iXAPoQJ3
|
||||
AUTH0_AUDIENCE=https://api.motovaultpro.com
|
||||
|
||||
# NOTE: Backend services no longer use this file
|
||||
# Backend configuration comes from:
|
||||
# - /app/config/production.yml (non-sensitive config)
|
||||
# - /run/secrets/ (sensitive secrets)
|
||||
@@ -10,24 +10,32 @@ import { PlatformCacheService } from '../../platform/domain/platform-cache.servi
|
||||
export interface CatalogMake {
|
||||
id: number;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CatalogModel {
|
||||
id: number;
|
||||
makeId: number;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CatalogYear {
|
||||
id: number;
|
||||
modelId: number;
|
||||
year: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CatalogTrim {
|
||||
id: number;
|
||||
yearId: number;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CatalogEngine {
|
||||
@@ -35,6 +43,8 @@ export interface CatalogEngine {
|
||||
trimId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PlatformChangeLog {
|
||||
@@ -58,7 +68,7 @@ export class VehicleCatalogService {
|
||||
|
||||
async getAllMakes(): Promise<CatalogMake[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
SELECT cache_key, data, created_at, updated_at
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:makes:%'
|
||||
ORDER BY (data->>'name')
|
||||
@@ -66,10 +76,18 @@ export class VehicleCatalogService {
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(query);
|
||||
return result.rows.map(row => ({
|
||||
return result.rows.map(row => {
|
||||
const createdAt =
|
||||
row.data?.createdAt ?? this.toIsoDate(row.created_at);
|
||||
const updatedAt =
|
||||
row.data?.updatedAt ?? this.toIsoDate(row.updated_at);
|
||||
return {
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
name: row.data.name
|
||||
}));
|
||||
name: row.data.name,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting all makes', { error });
|
||||
throw error;
|
||||
@@ -91,7 +109,8 @@ export class VehicleCatalogService {
|
||||
const makeId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert make
|
||||
const make: CatalogMake = { id: makeId, name };
|
||||
const now = new Date().toISOString();
|
||||
const make: CatalogMake = { id: makeId, name, createdAt: now, updatedAt: now };
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
@@ -124,15 +143,23 @@ export class VehicleCatalogService {
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
SELECT data, created_at FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:makes:${makeId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Make ${makeId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogMake = { id: makeId, name };
|
||||
const oldRow = oldResult.rows[0];
|
||||
const oldValue = oldRow.data;
|
||||
const createdAt =
|
||||
oldValue?.createdAt ?? this.toIsoDate(oldRow.created_at);
|
||||
const newValue: CatalogMake = {
|
||||
id: makeId,
|
||||
name,
|
||||
createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update make
|
||||
await client.query(`
|
||||
@@ -216,7 +243,7 @@ export class VehicleCatalogService {
|
||||
|
||||
async getModelsByMake(makeId: number): Promise<CatalogModel[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
SELECT cache_key, data, created_at, updated_at
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:models:%'
|
||||
AND (data->>'makeId')::int = $1
|
||||
@@ -228,7 +255,11 @@ export class VehicleCatalogService {
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
makeId: row.data.makeId,
|
||||
name: row.data.name
|
||||
name: row.data.name,
|
||||
createdAt:
|
||||
row.data?.createdAt ?? this.toIsoDate(row.created_at),
|
||||
updatedAt:
|
||||
row.data?.updatedAt ?? this.toIsoDate(row.updated_at),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting models by make', { error, makeId });
|
||||
@@ -260,7 +291,14 @@ export class VehicleCatalogService {
|
||||
const modelId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert model
|
||||
const model: CatalogModel = { id: modelId, makeId, name };
|
||||
const now = new Date().toISOString();
|
||||
const model: CatalogModel = {
|
||||
id: modelId,
|
||||
makeId,
|
||||
name,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
@@ -302,15 +340,24 @@ export class VehicleCatalogService {
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
SELECT data, created_at FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:models:${modelId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Model ${modelId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogModel = { id: modelId, makeId, name };
|
||||
const oldRow = oldResult.rows[0];
|
||||
const oldValue = oldRow.data;
|
||||
const createdAt =
|
||||
oldValue?.createdAt ?? this.toIsoDate(oldRow.created_at);
|
||||
const newValue: CatalogModel = {
|
||||
id: modelId,
|
||||
makeId,
|
||||
name,
|
||||
createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update model
|
||||
await client.query(`
|
||||
@@ -394,7 +441,7 @@ export class VehicleCatalogService {
|
||||
|
||||
async getYearsByModel(modelId: number): Promise<CatalogYear[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
SELECT cache_key, data, created_at, updated_at
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:years:%'
|
||||
AND (data->>'modelId')::int = $1
|
||||
@@ -406,7 +453,11 @@ export class VehicleCatalogService {
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
modelId: row.data.modelId,
|
||||
year: row.data.year
|
||||
year: row.data.year,
|
||||
createdAt:
|
||||
row.data?.createdAt ?? this.toIsoDate(row.created_at),
|
||||
updatedAt:
|
||||
row.data?.updatedAt ?? this.toIsoDate(row.updated_at),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting years by model', { error, modelId });
|
||||
@@ -438,7 +489,14 @@ export class VehicleCatalogService {
|
||||
const yearId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert year
|
||||
const yearData: CatalogYear = { id: yearId, modelId, year };
|
||||
const now = new Date().toISOString();
|
||||
const yearData: CatalogYear = {
|
||||
id: yearId,
|
||||
modelId,
|
||||
year,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
@@ -480,15 +538,24 @@ export class VehicleCatalogService {
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
SELECT data, created_at FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:years:${yearId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Year ${yearId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogYear = { id: yearId, modelId, year };
|
||||
const oldRow = oldResult.rows[0];
|
||||
const oldValue = oldRow.data;
|
||||
const createdAt =
|
||||
oldValue?.createdAt ?? this.toIsoDate(oldRow.created_at);
|
||||
const newValue: CatalogYear = {
|
||||
id: yearId,
|
||||
modelId,
|
||||
year,
|
||||
createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update year
|
||||
await client.query(`
|
||||
@@ -572,7 +639,7 @@ export class VehicleCatalogService {
|
||||
|
||||
async getTrimsByYear(yearId: number): Promise<CatalogTrim[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
SELECT cache_key, data, created_at, updated_at
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:trims:%'
|
||||
AND (data->>'yearId')::int = $1
|
||||
@@ -584,7 +651,11 @@ export class VehicleCatalogService {
|
||||
return result.rows.map(row => ({
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
yearId: row.data.yearId,
|
||||
name: row.data.name
|
||||
name: row.data.name,
|
||||
createdAt:
|
||||
row.data?.createdAt ?? this.toIsoDate(row.created_at),
|
||||
updatedAt:
|
||||
row.data?.updatedAt ?? this.toIsoDate(row.updated_at),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting trims by year', { error, yearId });
|
||||
@@ -616,7 +687,14 @@ export class VehicleCatalogService {
|
||||
const trimId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert trim
|
||||
const trim: CatalogTrim = { id: trimId, yearId, name };
|
||||
const now = new Date().toISOString();
|
||||
const trim: CatalogTrim = {
|
||||
id: trimId,
|
||||
yearId,
|
||||
name,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
@@ -658,15 +736,24 @@ export class VehicleCatalogService {
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
SELECT data, created_at FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:trims:${trimId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Trim ${trimId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogTrim = { id: trimId, yearId, name };
|
||||
const oldRow = oldResult.rows[0];
|
||||
const oldValue = oldRow.data;
|
||||
const createdAt =
|
||||
oldValue?.createdAt ?? this.toIsoDate(oldRow.created_at);
|
||||
const newValue: CatalogTrim = {
|
||||
id: trimId,
|
||||
yearId,
|
||||
name,
|
||||
createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update trim
|
||||
await client.query(`
|
||||
@@ -750,7 +837,7 @@ export class VehicleCatalogService {
|
||||
|
||||
async getEnginesByTrim(trimId: number): Promise<CatalogEngine[]> {
|
||||
const query = `
|
||||
SELECT cache_key, data
|
||||
SELECT cache_key, data, created_at, updated_at
|
||||
FROM vehicle_dropdown_cache
|
||||
WHERE cache_key LIKE 'catalog:engines:%'
|
||||
AND (data->>'trimId')::int = $1
|
||||
@@ -763,7 +850,11 @@ export class VehicleCatalogService {
|
||||
id: parseInt(row.cache_key.split(':')[2]),
|
||||
trimId: row.data.trimId,
|
||||
name: row.data.name,
|
||||
description: row.data.description
|
||||
description: row.data.description,
|
||||
createdAt:
|
||||
row.data?.createdAt ?? this.toIsoDate(row.created_at),
|
||||
updatedAt:
|
||||
row.data?.updatedAt ?? this.toIsoDate(row.updated_at),
|
||||
}));
|
||||
} catch (error) {
|
||||
logger.error('Error getting engines by trim', { error, trimId });
|
||||
@@ -795,7 +886,15 @@ export class VehicleCatalogService {
|
||||
const engineId = idResult.rows[0].next_id;
|
||||
|
||||
// Insert engine
|
||||
const engine: CatalogEngine = { id: engineId, trimId, name, description };
|
||||
const now = new Date().toISOString();
|
||||
const engine: CatalogEngine = {
|
||||
id: engineId,
|
||||
trimId,
|
||||
name,
|
||||
description,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await client.query(`
|
||||
INSERT INTO vehicle_dropdown_cache (cache_key, data, expires_at)
|
||||
VALUES ($1, $2, NOW() + INTERVAL '10 years')
|
||||
@@ -837,15 +936,25 @@ export class VehicleCatalogService {
|
||||
|
||||
// Get old value
|
||||
const oldResult = await client.query(`
|
||||
SELECT data FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
SELECT data, created_at FROM vehicle_dropdown_cache WHERE cache_key = $1
|
||||
`, [`catalog:engines:${engineId}`]);
|
||||
|
||||
if (oldResult.rows.length === 0) {
|
||||
throw new Error(`Engine ${engineId} not found`);
|
||||
}
|
||||
|
||||
const oldValue = oldResult.rows[0].data;
|
||||
const newValue: CatalogEngine = { id: engineId, trimId, name, description };
|
||||
const oldRow = oldResult.rows[0];
|
||||
const oldValue = oldRow.data;
|
||||
const createdAt =
|
||||
oldValue?.createdAt ?? this.toIsoDate(oldRow.created_at);
|
||||
const newValue: CatalogEngine = {
|
||||
id: engineId,
|
||||
trimId,
|
||||
name,
|
||||
description,
|
||||
createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Update engine
|
||||
await client.query(`
|
||||
@@ -915,6 +1024,17 @@ export class VehicleCatalogService {
|
||||
|
||||
// HELPER METHODS
|
||||
|
||||
private toIsoDate(value: Date | string | null | undefined): string {
|
||||
if (!value) {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.valueOf()) ? new Date().toISOString() : parsed.toISOString();
|
||||
}
|
||||
|
||||
private async logChange(
|
||||
client: any,
|
||||
changeType: 'CREATE' | 'UPDATE' | 'DELETE',
|
||||
|
||||
@@ -28,6 +28,11 @@ import {
|
||||
UpdateStationRequest,
|
||||
} from '../types/admin.types';
|
||||
|
||||
export interface AuditLogsResponse {
|
||||
logs: AdminAuditLog[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// Admin access verification
|
||||
export const adminApi = {
|
||||
// Verify admin access
|
||||
@@ -56,8 +61,8 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
// Audit logs
|
||||
listAuditLogs: async (): Promise<AdminAuditLog[]> => {
|
||||
const response = await apiClient.get<AdminAuditLog[]>('/admin/audit-logs');
|
||||
listAuditLogs: async (): Promise<AuditLogsResponse> => {
|
||||
const response = await apiClient.get<AuditLogsResponse>('/admin/audit-logs');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ export const makeSchema = z.object({
|
||||
|
||||
export const modelSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
makeId: z.string().min(1, 'Select a make'),
|
||||
makeId: z.coerce.string().min(1, 'Select a make'),
|
||||
});
|
||||
|
||||
export const yearSchema = z.object({
|
||||
modelId: z.string().min(1, 'Select a model'),
|
||||
modelId: z.coerce.string().min(1, 'Select a model'),
|
||||
year: z
|
||||
.coerce.number()
|
||||
.int()
|
||||
@@ -44,12 +44,12 @@ export const yearSchema = z.object({
|
||||
|
||||
export const trimSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
yearId: z.string().min(1, 'Select a year'),
|
||||
yearId: z.coerce.string().min(1, 'Select a year'),
|
||||
});
|
||||
|
||||
export const engineSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
trimId: z.string().min(1, 'Select a trim'),
|
||||
trimId: z.coerce.string().min(1, 'Select a trim'),
|
||||
displacement: z.string().optional(),
|
||||
cylinders: z
|
||||
.preprocess(
|
||||
@@ -96,22 +96,22 @@ export const buildDefaultValues = (
|
||||
case 'models':
|
||||
return {
|
||||
name: (entity as CatalogModel).name,
|
||||
makeId: (entity as CatalogModel).makeId,
|
||||
makeId: String((entity as CatalogModel).makeId),
|
||||
};
|
||||
case 'years':
|
||||
return {
|
||||
modelId: (entity as CatalogYear).modelId,
|
||||
modelId: String((entity as CatalogYear).modelId),
|
||||
year: (entity as CatalogYear).year,
|
||||
};
|
||||
case 'trims':
|
||||
return {
|
||||
name: (entity as CatalogTrim).name,
|
||||
yearId: (entity as CatalogTrim).yearId,
|
||||
yearId: String((entity as CatalogTrim).yearId),
|
||||
};
|
||||
case 'engines':
|
||||
return {
|
||||
name: (entity as CatalogEngine).name,
|
||||
trimId: (entity as CatalogEngine).trimId,
|
||||
trimId: String((entity as CatalogEngine).trimId),
|
||||
displacement: (entity as CatalogEngine).displacement ?? undefined,
|
||||
cylinders: (entity as CatalogEngine).cylinders ?? undefined,
|
||||
fuel_type: (entity as CatalogEngine).fuel_type ?? undefined,
|
||||
@@ -125,22 +125,22 @@ export const buildDefaultValues = (
|
||||
case 'models':
|
||||
return {
|
||||
name: '',
|
||||
makeId: context.make?.id ?? '',
|
||||
makeId: context.make?.id ? String(context.make.id) : '',
|
||||
};
|
||||
case 'years':
|
||||
return {
|
||||
modelId: context.model?.id ?? '',
|
||||
modelId: context.model?.id ? String(context.model.id) : '',
|
||||
year: undefined,
|
||||
};
|
||||
case 'trims':
|
||||
return {
|
||||
name: '',
|
||||
yearId: context.year?.id ?? '',
|
||||
yearId: context.year?.id ? String(context.year.id) : '',
|
||||
};
|
||||
case 'engines':
|
||||
return {
|
||||
name: '',
|
||||
trimId: context.trim?.id ?? '',
|
||||
trimId: context.trim?.id ? String(context.trim.id) : '',
|
||||
displacement: '',
|
||||
fuel_type: '',
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { adminApi } from '../api/admin.api';
|
||||
import { adminApi, AuditLogsResponse } from '../api/admin.api';
|
||||
import { AdminAuditLog } from '../types/admin.types';
|
||||
|
||||
/**
|
||||
@@ -67,11 +67,11 @@ export function useAuditLogStream(
|
||||
|
||||
// Query for fetching audit logs
|
||||
const {
|
||||
data: rawLogs = [],
|
||||
data: auditLogResponse,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
} = useQuery<AuditLogsResponse>({
|
||||
queryKey: ['auditLogs', resourceType, offset, limit],
|
||||
queryFn: async () => {
|
||||
const logs = await adminApi.listAuditLogs();
|
||||
@@ -87,21 +87,29 @@ export function useAuditLogStream(
|
||||
});
|
||||
|
||||
// Filter logs by resource type if specified
|
||||
const rawLogs = auditLogResponse?.logs;
|
||||
const logs = Array.isArray(rawLogs) ? rawLogs : [];
|
||||
const totalFromApi =
|
||||
typeof auditLogResponse?.total === 'number'
|
||||
? auditLogResponse.total
|
||||
: logs.length;
|
||||
|
||||
const filteredLogs = resourceType
|
||||
? rawLogs.filter((log) => log.resourceType === resourceType)
|
||||
: rawLogs;
|
||||
? logs.filter((log) => log.resourceType === resourceType)
|
||||
: logs;
|
||||
|
||||
// Apply pagination
|
||||
const paginatedLogs = filteredLogs.slice(offset, offset + limit);
|
||||
|
||||
// Calculate pagination state
|
||||
const paginationTotal = resourceType ? filteredLogs.length : totalFromApi;
|
||||
const pagination: PaginationState = {
|
||||
offset,
|
||||
limit,
|
||||
total: filteredLogs.length,
|
||||
total: paginationTotal,
|
||||
};
|
||||
|
||||
const hasMore = offset + limit < filteredLogs.length;
|
||||
const hasMore = offset + limit < paginationTotal;
|
||||
|
||||
/**
|
||||
* Navigate to next page
|
||||
|
||||
@@ -958,6 +958,16 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
}
|
||||
}, [canViewLevel, currentLevelRequirement.message, selection]);
|
||||
|
||||
const formatDateValue = useCallback((value?: string) => {
|
||||
if (!value) {
|
||||
return '—';
|
||||
}
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.valueOf())
|
||||
? '—'
|
||||
: date.toLocaleDateString();
|
||||
}, []);
|
||||
|
||||
const columns = useMemo<GridColumn<CatalogRow>[]>(() => {
|
||||
const baseColumns: GridColumn<CatalogRow>[] = [
|
||||
{
|
||||
@@ -974,14 +984,14 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
headerName: 'Created',
|
||||
sortable: true,
|
||||
renderCell: (row) =>
|
||||
new Date(row.createdAt).toLocaleDateString(),
|
||||
formatDateValue(row.createdAt),
|
||||
},
|
||||
{
|
||||
field: 'updatedAt',
|
||||
headerName: 'Updated',
|
||||
sortable: true,
|
||||
renderCell: (row) =>
|
||||
new Date(row.updatedAt).toLocaleDateString(),
|
||||
formatDateValue(row.updatedAt),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
@@ -1041,7 +1051,7 @@ export const AdminCatalogPage: React.FC = () => {
|
||||
];
|
||||
|
||||
return baseColumns;
|
||||
}, [handleDeleteSingle, handleDrillDown, openEditDialog, selection.level]);
|
||||
}, [formatDateValue, handleDeleteSingle, handleDrillDown, openEditDialog, selection.level]);
|
||||
|
||||
const renderTree = useCallback(
|
||||
(nodes: TreeNode[], depth = 0): React.ReactNode =>
|
||||
|
||||
Reference in New Issue
Block a user