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

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

View File

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

View File

@ -75,8 +75,13 @@ export const moduleService = {
const response = await apiClient.post<LaunchModuleResponse>(url); const response = await apiClient.post<LaunchModuleResponse>(url);
return response.data; return response.data;
}, },
getMyModules: async (): Promise<MyModulesResponse> => { getMyModules: async (tenantId?: string | null): Promise<MyModulesResponse> => {
const response = await apiClient.get<MyModulesResponse>('/modules/my'); 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; return response.data;
}, },
}; };