From 084b09f2d9c0f6289a3aa706102016440bc7533d Mon Sep 17 00:00:00 2001
From: Yashwin
Date: Tue, 20 Jan 2026 10:18:30 +0530
Subject: [PATCH] Update environment configuration for API base URL, add
ViewModuleModal export in shared components, enhance Login component error
handling, and implement Modules page with data fetching, filtering, and
pagination features.
---
.env | 2 +-
src/components/shared/ViewModuleModal.tsx | 274 ++++++++++++++++
src/components/shared/index.ts | 1 +
src/pages/Login.tsx | 65 +++-
src/pages/Modules.tsx | 366 +++++++++++++++++++++-
src/services/api-client.ts | 23 +-
src/services/module-service.ts | 28 ++
src/types/module.ts | 48 +++
8 files changed, 773 insertions(+), 34 deletions(-)
create mode 100644 src/components/shared/ViewModuleModal.tsx
create mode 100644 src/services/module-service.ts
create mode 100644 src/types/module.ts
diff --git a/.env b/.env
index e4d05f7..2ae02c5 100644
--- a/.env
+++ b/.env
@@ -1 +1 @@
-VITE_API_BASE_URL=http://localhost:3000/api/v1
+VITE_API_BASE_URL=https://backendqaasure.tech4bizsolutions.com/api/v1
diff --git a/src/components/shared/ViewModuleModal.tsx b/src/components/shared/ViewModuleModal.tsx
new file mode 100644
index 0000000..8642ca1
--- /dev/null
+++ b/src/components/shared/ViewModuleModal.tsx
@@ -0,0 +1,274 @@
+import { useEffect, useState } from 'react';
+import type { ReactElement } from 'react';
+import { Loader2 } from 'lucide-react';
+import { Modal, SecondaryButton, StatusBadge } from '@/components/shared';
+import type { Module } from '@/types/module';
+
+// 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';
+ }
+};
+
+// Helper function to format date
+const formatDate = (dateString: string | null): string => {
+ if (!dateString) return 'N/A';
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+};
+
+interface ViewModuleModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ moduleId: string | null;
+ onLoadModule: (id: string) => Promise;
+}
+
+export const ViewModuleModal = ({
+ isOpen,
+ onClose,
+ moduleId,
+ onLoadModule,
+}: ViewModuleModalProps): ReactElement | null => {
+ const [module, setModule] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Load module data when modal opens
+ useEffect(() => {
+ if (isOpen && moduleId) {
+ const loadModule = async (): Promise => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const data = await onLoadModule(moduleId);
+ setModule(data);
+ } catch (err: any) {
+ setError(err?.response?.data?.error?.message || 'Failed to load module details');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ loadModule();
+ } else {
+ setModule(null);
+ setError(null);
+ }
+ }, [isOpen, moduleId, onLoadModule]);
+
+ return (
+
+ Close
+
+ }
+ >
+
+ {isLoading && (
+
+
+
+ )}
+
+ {error && (
+
+ )}
+
+ {!isLoading && !error && module && (
+
+ {/* Basic Information */}
+
+
Basic Information
+
+
+
+
{module.module_id}
+
+
+
+
+
{module.description}
+
+
+
+
{module.version}
+
+
+
+
+
+ {module.status || 'Unknown'}
+
+
+
+
+
+
+
+ {module.health_status || 'N/A'}
+
+
+
+
+
+
+ {/* Technical Details */}
+
+
Technical Details
+
+
+
+
{module.runtime_language || 'N/A'}
+
+
+
+
{module.framework || 'N/A'}
+
+
+
+
{module.base_url}
+
+
+
+
{module.health_endpoint}
+
+
+
+
+ {/* Resource Limits */}
+
+
Resource Limits
+
+
+
+
{module.cpu_request}
+
+
+
+
{module.cpu_limit}
+
+
+
+
{module.memory_request}
+
+
+
+
{module.memory_limit}
+
+
+
+
{module.min_replicas}
+
+
+
+
{module.max_replicas}
+
+
+
+
+ {/* Additional Information */}
+ {(module.endpoints || module.kafka_topics || module.consecutive_failures !== null) && (
+
+
Additional Information
+
+ {module.endpoints && module.endpoints.length > 0 && (
+
+
+
+ {module.endpoints.map((endpoint, index) => (
+
+ {endpoint}
+
+ ))}
+
+
+ )}
+ {module.kafka_topics && module.kafka_topics.length > 0 && (
+
+
+
+ {module.kafka_topics.map((topic, index) => (
+
+ {topic}
+
+ ))}
+
+
+ )}
+ {module.consecutive_failures !== null && (
+
+
+
{module.consecutive_failures}
+
+ )}
+
+
+ )}
+
+ {/* Registration Information */}
+
+
Registration Information
+
+
+
+
{module.registered_by_email || module.registered_by}
+
+
+
+
{module.tenant_id}
+
+
+
+
{formatDate(module.last_health_check)}
+
+
+
+
+ {/* Timestamps */}
+
+
Timestamps
+
+
+
+
{formatDate(module.created_at)}
+
+
+
+
{formatDate(module.updated_at)}
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts
index 70cf955..56ed604 100644
--- a/src/components/shared/index.ts
+++ b/src/components/shared/index.ts
@@ -21,5 +21,6 @@ export { EditUserModal } from './EditUserModal';
export { NewRoleModal } from './NewRoleModal';
export { ViewRoleModal } from './ViewRoleModal';
export { EditRoleModal } from './EditRoleModal';
+export { ViewModuleModal } from './ViewModuleModal';
export { PageHeader } from './PageHeader';
export type { TabItem } from './PageHeader';
\ No newline at end of file
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index fb2bc6d..5945340 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -50,29 +50,36 @@ const Login = (): ReactElement => {
}
}, [isAuthenticated, navigate]);
- // Clear errors when component mounts
+ // Clear errors only on component mount, not on every auth state change
useEffect(() => {
+ // Only clear errors on initial mount
dispatch(clearError());
setGeneralError('');
- }, [dispatch]);
-
- const onSubmit = async (data: LoginFormData, event?: React.BaseSyntheticEvent): Promise => {
- // Explicitly prevent form default submission
- event?.preventDefault();
- event?.stopPropagation();
+ clearErrors();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // Empty dependency array - only run on mount
+ const onSubmit = async (data: LoginFormData): Promise => {
+ // Clear previous errors
setGeneralError('');
clearErrors();
+ dispatch(clearError());
try {
const result = await dispatch(loginAsync(data)).unwrap();
if (result) {
+ // Only navigate on success
navigate('/dashboard');
}
} catch (error: any) {
- if (error?.payload) {
- const loginError = error.payload as LoginError;
+ // Clear Redux error state since we're handling errors locally
+ dispatch(clearError());
+ // Handle error from unwrap() - error is the rejected value from rejectWithValue
+ const loginError = error as LoginError;
+
+ if (loginError && typeof loginError === 'object') {
+ // Check for validation errors with details array
if ('details' in loginError && Array.isArray(loginError.details)) {
// Validation errors from server - set field-specific errors
loginError.details.forEach((detail) => {
@@ -81,16 +88,32 @@ const Login = (): ReactElement => {
type: 'server',
message: detail.message,
});
+ } else {
+ // If error is for a field we don't handle, show as general error
+ setGeneralError(detail.message);
}
});
- } else if ('error' in loginError && typeof loginError.error === 'object') {
- // General error from server
- setGeneralError(loginError.error.message || 'Login failed');
+ } else if ('error' in loginError) {
+ // Check if error is an object with message property
+ if (typeof loginError.error === 'object' && loginError.error !== null && 'message' in loginError.error) {
+ // General error from server with object structure
+ setGeneralError(loginError.error.message || 'Login failed');
+ } else if (typeof loginError.error === 'string') {
+ // Error is a string
+ setGeneralError(loginError.error);
+ } else {
+ setGeneralError('Login failed. Please check your credentials.');
+ }
} else {
- setGeneralError('An unexpected error occurred');
+ // Fallback for unknown error structure
+ setGeneralError('An unexpected error occurred. Please try again.');
}
+ } else if (error?.message) {
+ // Network error or other error
+ setGeneralError(error.message);
} else {
- setGeneralError(error?.message || 'Login failed');
+ // Complete fallback
+ setGeneralError('Login failed. Please check your credentials and try again.');
}
}
};
@@ -120,19 +143,27 @@ const Login = (): ReactElement => {
- {/* General Error Message */}
- {(generalError || error) && (
+ {/* General Error Message - Prioritize local error over Redux error */}
+ {generalError && (
-
{generalError || error}
+
{generalError}
+
+ )}
+ {/* Show Redux error only if no local error and no field errors */}
+ {!generalError && error && !errors.email && !errors.password && (
+
)}