Qassure-frontend/src/pages/Modules.tsx

369 lines
12 KiB
TypeScript

import { useState, useEffect } from 'react';
import type { ReactElement } from 'react';
import { Layout } from '@/components/layout/Layout';
import {
StatusBadge,
ViewModuleModal,
DataTable,
Pagination,
FilterDropdown,
type Column,
} from '@/components/shared';
import { Download, ArrowUpDown } from 'lucide-react';
import { moduleService } from '@/services/module-service';
import type { Module } from '@/types/module';
// Helper function to format date
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
};
// Helper function to get status badge variant
const getStatusVariant = (status: string | null): 'success' | 'failure' | 'process' => {
if (!status) return 'process';
switch (status.toLowerCase()) {
case 'running':
case 'active':
case 'healthy':
return 'success';
case 'stopped':
case 'failed':
case 'unhealthy':
return 'failure';
default:
return 'process';
}
};
const Modules = (): ReactElement => {
const [modules, setModules] = useState<Module[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Pagination state
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(5);
const [pagination, setPagination] = useState<{
page: number;
limit: number;
total: number;
totalPages: number;
hasMore: boolean;
}>({
page: 1,
limit: 5,
total: 0,
totalPages: 1,
hasMore: false,
});
// Filter state
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [orderBy, setOrderBy] = useState<string[] | null>(null);
// View modal
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
const [selectedModuleId, setSelectedModuleId] = useState<string | null>(null);
const fetchModules = async (
page: number,
itemsPerPage: number,
status: string | null = null,
sortBy: string[] | null = null
): Promise<void> => {
try {
setIsLoading(true);
setError(null);
const response = await moduleService.getAll(page, itemsPerPage, status, sortBy);
if (response.success) {
setModules(response.data);
setPagination(response.pagination);
} else {
setError('Failed to load modules');
}
} catch (err: any) {
setError(err?.response?.data?.error?.message || 'Failed to load modules');
} finally {
setIsLoading(false);
}
};
// Fetch modules on mount and when pagination/filters change
useEffect(() => {
fetchModules(currentPage, limit, statusFilter, orderBy);
}, [currentPage, limit, statusFilter, orderBy]);
// View module handler
const handleViewModule = (moduleId: string): void => {
setSelectedModuleId(moduleId);
setViewModalOpen(true);
};
// Load module for view
const loadModule = async (id: string): Promise<Module> => {
const response = await moduleService.getById(id);
return response.data;
};
// Define table columns
const columns: Column<Module>[] = [
{
key: 'name',
label: 'Module Name',
render: (module) => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
<span className="text-xs font-normal text-[#9aa6b2]">
{module.name.substring(0, 2).toUpperCase()}
</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-normal text-[#0f1724]">{module.name}</span>
<span className="text-xs text-[#6b7280] font-mono">{module.module_id}</span>
</div>
</div>
),
mobileLabel: 'Name',
},
{
key: 'description',
label: 'Description',
render: (module) => (
<span className="text-sm font-normal text-[#0f1724] line-clamp-1">{module.description}</span>
),
},
{
key: 'version',
label: 'Version',
render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">{module.version}</span>
),
},
{
key: 'status',
label: 'Status',
render: (module) => (
<StatusBadge variant={getStatusVariant(module.status)}>
{module.status || 'Unknown'}
</StatusBadge>
),
},
{
key: 'health_status',
label: 'Health Status',
render: (module) => (
<StatusBadge variant={getStatusVariant(module.health_status)}>
{module.health_status || 'N/A'}
</StatusBadge>
),
},
{
key: 'runtime_language',
label: 'Runtime',
render: (module) => (
<span className="text-sm font-normal text-[#0f1724]">
{module.runtime_language || 'N/A'}
</span>
),
},
{
key: 'created_at',
label: 'Registered Date',
render: (module) => (
<span className="text-sm font-normal text-[#6b7280]">{formatDate(module.created_at)}</span>
),
mobileLabel: 'Registered',
},
{
key: 'actions',
label: 'Actions',
align: 'right',
render: (module) => (
<div className="flex justify-end">
<button
type="button"
onClick={() => handleViewModule(module.id)}
className="text-sm text-[#112868] hover:text-[#0d1f4e] font-medium transition-colors"
>
View
</button>
</div>
),
},
];
// Mobile card renderer
const mobileCardRenderer = (module: Module) => (
<div className="p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="w-8 h-8 bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
<span className="text-xs font-normal text-[#9aa6b2]">
{module.name.substring(0, 2).toUpperCase()}
</span>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-[#0f1724] truncate">{module.name}</h3>
<p className="text-xs text-[#6b7280] mt-0.5 truncate font-mono">{module.module_id}</p>
</div>
</div>
<button
type="button"
onClick={() => handleViewModule(module.id)}
className="text-sm text-[#112868] hover:text-[#0d1f4e] font-medium transition-colors shrink-0"
>
View
</button>
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-[#9aa6b2]">Status:</span>
<div className="mt-1">
<StatusBadge variant={getStatusVariant(module.status)}>
{module.status || 'Unknown'}
</StatusBadge>
</div>
</div>
<div>
<span className="text-[#9aa6b2]">Health:</span>
<div className="mt-1">
<StatusBadge variant={getStatusVariant(module.health_status)}>
{module.health_status || 'N/A'}
</StatusBadge>
</div>
</div>
<div>
<span className="text-[#9aa6b2]">Version:</span>
<p className="text-[#0f1724] font-normal mt-1">{module.version}</p>
</div>
<div>
<span className="text-[#9aa6b2]">Runtime:</span>
<p className="text-[#0f1724] font-normal mt-1">{module.runtime_language || 'N/A'}</p>
</div>
<div>
<span className="text-[#9aa6b2]">Registered:</span>
<p className="text-[#6b7280] font-normal mt-1">{formatDate(module.created_at)}</p>
</div>
<div>
<span className="text-[#9aa6b2]">Description:</span>
<p className="text-[#0f1724] font-normal mt-1 line-clamp-2">{module.description}</p>
</div>
</div>
</div>
);
return (
<Layout
currentPage="Modules"
pageHeader={{
title: 'Module List',
description: 'View and manage all system modules registered in the QAssure platform.',
}}
>
{/* Table Container */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
{/* Table Header with Filters */}
<div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Filters */}
<div className="flex flex-wrap items-center gap-3">
{/* Status Filter */}
<FilterDropdown
label="Status"
options={[
{ value: 'running', label: 'Running' },
{ value: 'stopped', label: 'Stopped' },
{ value: 'failed', label: 'Failed' },
]}
value={statusFilter}
onChange={(value) => {
setStatusFilter(value as string | null);
setCurrentPage(1);
}}
placeholder="All"
/>
{/* Sort Filter */}
<FilterDropdown
label="Sort by"
options={[
{ value: ['name', 'asc'], label: 'Name (A-Z)' },
{ value: ['name', 'desc'], label: 'Name (Z-A)' },
{ value: ['module_id', 'asc'], label: 'Module ID (A-Z)' },
{ value: ['module_id', 'desc'], label: 'Module ID (Z-A)' },
{ value: ['status', 'asc'], label: 'Status (A-Z)' },
{ value: ['status', 'desc'], label: 'Status (Z-A)' },
{ value: ['created_at', 'asc'], label: 'Created (Oldest)' },
{ value: ['created_at', 'desc'], label: 'Created (Newest)' },
{ value: ['updated_at', 'asc'], label: 'Updated (Oldest)' },
{ value: ['updated_at', 'desc'], label: 'Updated (Newest)' },
]}
value={orderBy}
onChange={(value) => {
setOrderBy(value as string[] | null);
setCurrentPage(1);
}}
placeholder="Default"
showIcon
icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
/>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
{/* Export Button */}
<button
type="button"
className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors"
>
<Download className="w-3.5 h-3.5" />
<span>Export</span>
</button>
</div>
</div>
{/* Table */}
<DataTable
columns={columns}
data={modules}
keyExtractor={(module) => module.id}
isLoading={isLoading}
error={error}
mobileCardRenderer={mobileCardRenderer}
emptyMessage="No modules found"
/>
{/* Pagination */}
{pagination.total > 0 && (
<div className="border-t border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3">
<Pagination
currentPage={pagination.page}
totalPages={pagination.totalPages}
totalItems={pagination.total}
limit={limit}
onPageChange={(page: number) => setCurrentPage(page)}
onLimitChange={(newLimit: number) => {
setLimit(newLimit);
setCurrentPage(1);
}}
/>
</div>
)}
</div>
{/* View Module Modal */}
<ViewModuleModal
isOpen={viewModalOpen}
onClose={() => {
setViewModalOpen(false);
setSelectedModuleId(null);
}}
moduleId={selectedModuleId}
onLoadModule={loadModule}
/>
</Layout>
);
};
export default Modules;