Refactor NewModuleModal to comment out health_status field and improve code readability. Update TenantDetails to fetch modules based on tenant ID and adjust ModulesTab to handle module data more effectively. Modify module service to accept tenant ID for fetching modules. Clean up unused code in RolesTable and UsersTable components by removing header text.

This commit is contained in:
Yashwin 2026-02-04 16:37:31 +05:30
parent dd71820ac9
commit c656594154
5 changed files with 262 additions and 229 deletions

View File

@ -49,7 +49,7 @@ const newModuleSchema = z.object({
min_replicas: z.number().int().min(1, 'min_replicas must be at least 1').max(50, 'min_replicas must be at most 50').optional().nullable(),
max_replicas: z.number().int().min(1, 'max_replicas must be at least 1').max(50, 'max_replicas must be at most 50').optional().nullable(),
last_health_check: z.string().optional().nullable(),
health_status: z.string().max(20, 'health_status must be at most 20 characters').optional().nullable(),
// health_status: z.string().max(20, 'health_status must be at most 20 characters').optional().nullable(),
consecutive_failures: z.number().int().optional().nullable(),
registered_by: z.string().uuid().optional().nullable(),
tenant_id: z.string().uuid().optional().nullable(),
@ -96,7 +96,7 @@ export const NewModuleModal = ({
min_replicas: null,
max_replicas: null,
last_health_check: null,
health_status: null,
// health_status: null,
consecutive_failures: null,
registered_by: null,
tenant_id: null,
@ -126,7 +126,7 @@ export const NewModuleModal = ({
min_replicas: null,
max_replicas: null,
last_health_check: null,
health_status: null,
// health_status: null,
consecutive_failures: null,
registered_by: null,
tenant_id: null,
@ -274,52 +274,52 @@ export const NewModuleModal = ({
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
</div>
)}
{!apiKey && (
<div className="flex flex-col gap-0">
{/* Basic Information Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">Basic Information</h3>
<div className="flex flex-col gap-0">
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Module ID"
required
placeholder="Enter module ID (e.g., my-module)"
error={errors.module_id?.message}
{...register('module_id')}
/>
</div>
<div className="flex-1">
<div className="flex flex-col gap-0">
{/* Basic Information Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">Basic Information</h3>
<div className="flex flex-col gap-0">
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Module ID"
required
placeholder="Enter module ID (e.g., my-module)"
error={errors.module_id?.message}
{...register('module_id')}
/>
<FormField
label="Module Name"
required
placeholder="Enter module name"
error={errors.name?.message}
{...register('name')}
/>
</div>
</div>
<div className="flex-1">
<FormField
label="Module Name"
required
placeholder="Enter module name"
error={errors.name?.message}
{...register('name')}
/>
</div>
</div>
<FormField
label="Description"
placeholder="Enter module description (optional)"
error={errors.description?.message}
{...register('description')}
/>
<FormField
label="Description"
placeholder="Enter module description (optional)"
error={errors.description?.message}
{...register('description')}
/>
{/* <div className="flex gap-5">
{/* <div className="flex gap-5">
<div className="flex-1"> */}
<FormField
label="Version"
required
placeholder="e.g., 1.0.0"
error={errors.version?.message}
{...register('version')}
/>
{/* </div> */}
{/* <div className="flex-1">
<FormField
label="Version"
required
placeholder="e.g., 1.0.0"
error={errors.version?.message}
{...register('version')}
/>
{/* </div> */}
{/* <div className="flex-1">
<FormSelect
label="Status"
placeholder="Select Status"
@ -330,151 +330,152 @@ export const NewModuleModal = ({
/>
</div>
</div> */}
</div>
</div>
</div>
{/* Runtime Information Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">Runtime Information</h3>
<div className="flex gap-5">
<div className="flex-1">
{/* Runtime Information Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">Runtime Information</h3>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Runtime Language"
required
placeholder="e.g., Node.js, Python, Java"
error={errors.runtime_language?.message}
{...register('runtime_language')}
/>
</div>
<div className="flex-1">
<FormField
label="Framework"
placeholder="e.g., Express, Django, Spring (optional)"
error={errors.framework?.message}
{...register('framework')}
/>
</div>
</div>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Webhook URL"
placeholder="e.g., https://example.com/webhook"
error={errors.webhookurl?.message}
{...register('webhookurl')}
/>
</div>
<div className="flex-1">
</div>
</div>
</div>
{/* URL Configuration Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">URL Configuration</h3>
<div className="flex flex-col gap-0">
<FormField
label="Runtime Language"
label="Base URL"
required
placeholder="e.g., Node.js, Python, Java"
error={errors.runtime_language?.message}
{...register('runtime_language')}
type="url"
placeholder="https://example.com"
error={errors.base_url?.message}
{...register('base_url')}
/>
</div>
<div className="flex-1">
<FormField
label="Framework"
placeholder="e.g., Express, Django, Spring (optional)"
error={errors.framework?.message}
{...register('framework')}
label="Health Endpoint"
required
placeholder="/health"
error={errors.health_endpoint?.message}
{...register('health_endpoint')}
/>
</div>
</div>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Webhook URL"
placeholder="e.g., https://example.com/webhook"
error={errors.webhookurl?.message}
{...register('webhookurl')}
/>
</div>
<div className="flex-1">
</div>
</div>
</div>
{/* URL Configuration Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">URL Configuration</h3>
<div className="flex flex-col gap-0">
<FormField
label="Base URL"
required
type="url"
placeholder="https://example.com"
error={errors.base_url?.message}
{...register('base_url')}
/>
{/* Resource Configuration Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">Resource Configuration (Optional)</h3>
<div className="flex flex-col gap-0">
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="CPU Request"
placeholder="e.g., 100m, 0.5"
error={errors.cpu_request?.message}
{...register('cpu_request')}
/>
</div>
<div className="flex-1">
<FormField
label="CPU Limit"
placeholder="e.g., 500m, 1"
error={errors.cpu_limit?.message}
{...register('cpu_limit')}
/>
</div>
</div>
<FormField
label="Health Endpoint"
required
placeholder="/health"
error={errors.health_endpoint?.message}
{...register('health_endpoint')}
/>
</div>
</div>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Memory Request"
placeholder="e.g., 128Mi, 512Mi"
error={errors.memory_request?.message}
{...register('memory_request')}
/>
</div>
<div className="flex-1">
<FormField
label="Memory Limit"
placeholder="e.g., 256Mi, 1Gi"
error={errors.memory_limit?.message}
{...register('memory_limit')}
/>
</div>
</div>
{/* Resource Configuration Section */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">Resource Configuration (Optional)</h3>
<div className="flex flex-col gap-0">
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="CPU Request"
placeholder="e.g., 100m, 0.5"
error={errors.cpu_request?.message}
{...register('cpu_request')}
/>
</div>
<div className="flex-1">
<FormField
label="CPU Limit"
placeholder="e.g., 500m, 1"
error={errors.cpu_limit?.message}
{...register('cpu_limit')}
/>
</div>
</div>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Memory Request"
placeholder="e.g., 128Mi, 512Mi"
error={errors.memory_request?.message}
{...register('memory_request')}
/>
</div>
<div className="flex-1">
<FormField
label="Memory Limit"
placeholder="e.g., 256Mi, 1Gi"
error={errors.memory_limit?.message}
{...register('memory_limit')}
/>
</div>
</div>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Min Replicas"
type="number"
min="1"
max="50"
step="1"
placeholder="1"
error={errors.min_replicas?.message}
{...register('min_replicas', {
setValueAs: (value) => {
if (value === '' || value === null || value === undefined) return null;
const num = Number(value);
return isNaN(num) ? null : num;
},
})}
/>
</div>
<div className="flex-1">
<FormField
label="Max Replicas"
type="number"
min="1"
max="50"
step="1"
placeholder="5"
error={errors.max_replicas?.message}
{...register('max_replicas', {
setValueAs: (value) => {
if (value === '' || value === null || value === undefined) return null;
const num = Number(value);
return isNaN(num) ? null : num;
},
})}
/>
<div className="flex gap-5">
<div className="flex-1">
<FormField
label="Min Replicas"
type="number"
min="1"
max="50"
step="1"
placeholder="1"
error={errors.min_replicas?.message}
{...register('min_replicas', {
setValueAs: (value) => {
if (value === '' || value === null || value === undefined) return null;
const num = Number(value);
return isNaN(num) ? null : num;
},
})}
/>
</div>
<div className="flex-1">
<FormField
label="Max Replicas"
type="number"
min="1"
max="50"
step="1"
placeholder="5"
error={errors.max_replicas?.message}
{...register('max_replicas', {
setValueAs: (value) => {
if (value === '' || value === null || value === undefined) return null;
const num = Number(value);
return isNaN(num) ? null : num;
},
})}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</form>
</Modal >
);

View File

@ -288,7 +288,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol
<>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-[#0f1724]">Roles</h3>
<h3 className="text-lg font-semibold text-[#0f1724]"></h3>
<div className="flex items-center gap-2">
<FilterDropdown
label="Scope"

View File

@ -330,7 +330,7 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use
<>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-[#0f1724]">Users</h3>
<h3 className="text-lg font-semibold text-[#0f1724]"></h3>
<div className="flex items-center gap-2">
<FilterDropdown
label="Status"

View File

@ -10,8 +10,6 @@ import {
History,
CreditCard,
Edit,
CheckCircle2,
XCircle,
Settings,
Image as ImageIcon,
} from 'lucide-react';
@ -25,8 +23,10 @@ import {
import { UsersTable, RolesTable } from '@/components/superadmin';
import { tenantService } from '@/services/tenant-service';
import { auditLogService } from '@/services/audit-log-service';
import type { Tenant, AssignedModule } from '@/types/tenant';
import { moduleService } from '@/services/module-service';
import type { Tenant } from '@/types/tenant';
import type { AuditLog } from '@/types/audit-log';
import type { MyModule } from '@/types/module';
import { formatDate } from '@/utils/format-date';
type TabType = 'overview' | 'users' | 'roles' | 'modules' | 'settings' | 'license' | 'audit-logs' | 'billing';
@ -278,10 +278,8 @@ const TenantDetails = (): ReactElement => {
{activeTab === 'roles' && id && (
<RolesTable tenantId={id} compact={true} />
)}
{activeTab === 'modules' && tenant && (
<ModulesTab
modules={tenant.assignedModules || []}
/>
{activeTab === 'modules' && id && (
<ModulesTab tenantId={id} />
)}
{activeTab === 'settings' && tenant && (
<SettingsTab tenant={tenant} />
@ -386,31 +384,51 @@ const OverviewTab = ({ tenant, stats }: OverviewTabProps): ReactElement => {
// Modules Tab Component
interface ModulesTabProps {
modules: AssignedModule[];
tenantId: string;
}
const ModulesTab = ({ modules }: ModulesTabProps): ReactElement => {
const [enabledModules, setEnabledModules] = useState<Set<string>>(new Set());
const ModulesTab = ({ tenantId }: ModulesTabProps): ReactElement => {
const [modules, setModules] = useState<MyModule[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Initialize enabled modules (assuming all are enabled by default)
setEnabledModules(new Set(modules.map((m) => m.id)));
}, [modules]);
const toggleModule = (moduleId: string): void => {
setEnabledModules((prev) => {
const next = new Set(prev);
if (next.has(moduleId)) {
next.delete(moduleId);
// Fetch modules for this tenant
const fetchModules = async (): Promise<void> => {
try {
setIsLoading(true);
setError(null);
const response = await moduleService.getMyModules(tenantId);
if (response.success && response.data) {
setModules(response.data);
} else {
next.add(moduleId);
setError('Failed to load modules');
}
return next;
});
// TODO: Call API to enable/disable module
} catch (err: any) {
setError(err?.response?.data?.error?.message || 'Failed to load modules');
} finally {
setIsLoading(false);
}
};
const columns: Column<AssignedModule>[] = [
useEffect(() => {
if (tenantId) {
fetchModules();
}
}, [tenantId]);
// Launch module handler
const handleLaunchModule = async (moduleId: string): Promise<void> => {
try {
const response = await moduleService.launch(moduleId, tenantId);
if (response.success && response.data.launch_url) {
window.open(response.data.launch_url, '_blank', 'noopener,noreferrer');
}
} catch (err: any) {
setError(err?.response?.data?.error?.message || 'Failed to launch module');
}
};
const columns: Column<MyModule>[] = [
{
key: 'name',
label: 'Module Name',
@ -423,6 +441,13 @@ const ModulesTab = ({ modules }: ModulesTabProps): ReactElement => {
</div>
),
},
{
key: 'module_id',
label: 'Module ID',
render: (module) => (
<span className="text-sm font-normal text-[#6b7280] font-mono">{module.module_id}</span>
),
},
{
key: 'version',
label: 'Version',
@ -448,45 +473,47 @@ const ModulesTab = ({ modules }: ModulesTabProps): ReactElement => {
),
},
{
key: 'enabled',
label: 'Enabled',
key: 'base_url',
label: 'Base URL',
render: (module) => (
<button
onClick={() => toggleModule(module.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
enabledModules.has(module.id)
? 'bg-green-50 text-green-700 border border-green-200'
: 'bg-gray-50 text-gray-700 border border-gray-200'
}`}
>
{enabledModules.has(module.id) ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<XCircle className="w-4 h-4" />
)}
<span>{enabledModules.has(module.id) ? 'Enabled' : 'Disabled'}</span>
</button>
<span className="text-sm font-normal text-[#6b7280] font-mono truncate max-w-[200px]">
{module.base_url || 'N/A'}
</span>
),
},
{
key: 'created_at',
label: 'Registered',
key: 'assigned_at',
label: 'Assigned At',
render: (module) => (
<span className="text-sm font-normal text-[#6b7280]">{formatDate(module.created_at)}</span>
<span className="text-sm font-normal text-[#6b7280]">{formatDate(module.assigned_at)}</span>
),
},
{
key: 'actions',
label: 'Actions',
align: 'right',
render: (module) => (
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => handleLaunchModule(module.id)}
className="text-sm text-[#FFC107] hover:text-[#FFC107] font-medium transition-colors cursor-pointer"
>
Lunch
</button>
</div>
),
},
];
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-[#0f1724]">Modules</h3>
</div>
<DataTable
columns={columns}
data={modules}
keyExtractor={(module) => module.id}
isLoading={false}
isLoading={isLoading}
error={error}
emptyMessage="No modules assigned to this tenant"
/>
</div>
@ -593,9 +620,9 @@ const AuditLogsTab = ({
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
{/* <div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-[#0f1724]">Audit Logs</h3>
</div>
</div> */}
<DataTable
columns={columns}
data={auditLogs}

View File

@ -75,8 +75,13 @@ export const moduleService = {
const response = await apiClient.post<LaunchModuleResponse>(url);
return response.data;
},
getMyModules: async (): Promise<MyModulesResponse> => {
const response = await apiClient.get<MyModulesResponse>('/modules/my');
getMyModules: async (tenantId?: string | null): Promise<MyModulesResponse> => {
const params = new URLSearchParams();
if (tenantId) {
params.append('tenant_id', tenantId);
}
const url = `/modules/my${params.toString() ? `?${params.toString()}` : ''}`;
const response = await apiClient.get<MyModulesResponse>(url);
return response.data;
},
};