feat: Add LTTS logo and update various UI components and pages across superadmin and tenant sections.
This commit is contained in:
parent
6efcb90f3e
commit
757a9f216b
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/LTTS.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>qassure-frontend</title>
|
||||
</head>
|
||||
|
||||
4
public/LTTS.svg
Normal file
4
public/LTTS.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 24 KiB |
@ -78,6 +78,35 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
|
||||
const isSuperAdmin = isSuperAdminCheck();
|
||||
|
||||
// Get role name for display
|
||||
const getRoleName = (): string => {
|
||||
if (isSuperAdmin) {
|
||||
return 'Super Admin';
|
||||
}
|
||||
let rolesArray: string[] = [];
|
||||
if (Array.isArray(roles)) {
|
||||
rolesArray = roles;
|
||||
} else if (typeof roles === 'string') {
|
||||
try {
|
||||
rolesArray = JSON.parse(roles);
|
||||
} catch {
|
||||
rolesArray = [];
|
||||
}
|
||||
}
|
||||
// Get the first role and format it
|
||||
if (rolesArray.length > 0) {
|
||||
const role = rolesArray[0];
|
||||
// Convert snake_case to Title Case
|
||||
return role
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
return 'User';
|
||||
};
|
||||
|
||||
const roleName = getRoleName();
|
||||
|
||||
// Fetch theme if tenant admin
|
||||
if (!isSuperAdmin) {
|
||||
useTenantTheme();
|
||||
@ -212,9 +241,21 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
>
|
||||
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
||||
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
||||
</div>
|
||||
{(!isSuperAdmin && logoUrl) ? null : (
|
||||
<div
|
||||
className="text-[12px] font-semibold capitalize mt-[7px] -translate-y-1/2"
|
||||
style={{
|
||||
color: !isSuperAdmin && theme?.accent_color ? theme.accent_color : '#084cc8',
|
||||
}}
|
||||
>
|
||||
{roleName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@ -238,6 +279,18 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
<span className="text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Powered by LTTS */}
|
||||
<div className="w-full flex flex-col items-center justify-center gap-0 px-2">
|
||||
<p className="text-[10px] font-medium text-[#9ca3af] capitalize leading-normal">
|
||||
Powered by
|
||||
</p>
|
||||
<img
|
||||
src="/LTTS.svg"
|
||||
alt="L&T Technology Services"
|
||||
className="h-6 w-auto max-w-[150px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Desktop Sidebar */}
|
||||
@ -266,9 +319,21 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
>
|
||||
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-base md:text-base lg:text-base xl:text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
|
||||
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
||||
</div>
|
||||
{(!isSuperAdmin && logoUrl) ? null : (
|
||||
<div
|
||||
className="text-[12px] font-semibold capitalize mt-[7px] -translate-y-1/2"
|
||||
style={{
|
||||
color: !isSuperAdmin && theme?.accent_color ? theme.accent_color : '#084cc8',
|
||||
}}
|
||||
>
|
||||
{roleName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -289,6 +354,18 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
<span className="text-xs md:text-xs lg:text-[13px] font-medium text-[#0f1724]">Support Center</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Powered by LTTS */}
|
||||
<div className="w-full flex flex-col items-center justify-center gap-0 px-2">
|
||||
<p className="text-[10px] font-medium text-[#9ca3af] capitalize leading-normal">
|
||||
Powered by
|
||||
</p>
|
||||
<img
|
||||
src="/LTTS.svg"
|
||||
alt="L&T Technology Services"
|
||||
className="h-6 w-auto max-w-[150px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -65,7 +65,7 @@ export const DataTable = <T,>({
|
||||
return (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`px-3 md:px-4 lg:px-5 py-2 md:py-2.5 lg:py-3 ${alignClass} text-[10px] md:text-xs font-medium text-[#9aa6b2] uppercase`}
|
||||
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-xs font-medium text-[#9aa6b2] uppercase`}
|
||||
>
|
||||
{column.label}
|
||||
</th>
|
||||
@ -75,7 +75,7 @@ export const DataTable = <T,>({
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-3 md:px-4 lg:px-5 py-6 md:py-7 lg:py-8 text-center text-xs md:text-sm text-[#6b7280]">
|
||||
<td colSpan={columns.length} className="px-3 md:px-2 lg:px-4 xl:px-5 py-6 md:py-3 lg:py-7 xl:py-8 text-center text-xs md:text-xs lg:text-sm text-[#6b7280]">
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
@ -109,7 +109,7 @@ export const DataTable = <T,>({
|
||||
return (
|
||||
<th
|
||||
key={column.key}
|
||||
className={`px-3 md:px-4 lg:px-5 py-2 md:py-2.5 lg:py-3 ${alignClass} text-[10px] md:text-xs font-medium text-[#9aa6b2] uppercase`}
|
||||
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-xs font-medium text-[#9aa6b2] uppercase`}
|
||||
>
|
||||
{column.label}
|
||||
</th>
|
||||
@ -131,7 +131,7 @@ export const DataTable = <T,>({
|
||||
? 'text-center'
|
||||
: 'text-left';
|
||||
return (
|
||||
<td key={column.key} className={`px-3 md:px-4 lg:px-5 py-2.5 md:py-3 lg:py-4 ${alignClass} text-xs md:text-sm`}>
|
||||
<td key={column.key} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-sm`}>
|
||||
{column.render ? column.render(item) : String((item as any)[column.key])}
|
||||
</td>
|
||||
);
|
||||
|
||||
@ -69,7 +69,7 @@ const editRoleSchema = z.object({
|
||||
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
|
||||
),
|
||||
description: z.string().min(1, 'Description is required'),
|
||||
modules: z.array(z.string().uuid()).optional().nullable(),
|
||||
modules: z.array(z.uuid()).optional().nullable(),
|
||||
permissions: z.array(z.object({
|
||||
resource: z.string(),
|
||||
action: z.string(),
|
||||
|
||||
@ -12,13 +12,13 @@ import {
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from '@/components/shared';
|
||||
import { tenantService } from '@/services/tenant-service';
|
||||
// import { tenantService } from '@/services/tenant-service';
|
||||
import { roleService } from '@/services/role-service';
|
||||
import type { User } from '@/types/user';
|
||||
|
||||
// Validation schema
|
||||
const editUserSchema = z.object({
|
||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
||||
email: z.email({ message: 'Please enter a valid email address' }),
|
||||
first_name: z.string().min(1, 'First name is required'),
|
||||
last_name: z.string().min(1, 'Last name is required'),
|
||||
status: z.enum(['active', 'suspended', 'deleted'], {
|
||||
@ -58,7 +58,7 @@ export const EditUserModal = ({
|
||||
const [isLoadingUser, setIsLoadingUser] = useState<boolean>(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const loadedUserIdRef = useRef<string | null>(null);
|
||||
const [selectedTenantId, setSelectedTenantId] = useState<string>('');
|
||||
// const [selectedTenantId, setSelectedTenantId] = useState<string>('');
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<string>('');
|
||||
|
||||
const {
|
||||
@ -75,71 +75,74 @@ export const EditUserModal = ({
|
||||
});
|
||||
|
||||
const statusValue = watch('status');
|
||||
const tenantIdValue = watch('tenant_id');
|
||||
// const tenantIdValue = watch('tenant_id');
|
||||
const roleIdValue = watch('role_id');
|
||||
|
||||
// Store tenant and role names from user response
|
||||
const [currentTenantName, setCurrentTenantName] = useState<string>('');
|
||||
// const [currentTenantName, setCurrentTenantName] = useState<string>('');
|
||||
const [currentRoleName, setCurrentRoleName] = useState<string>('');
|
||||
// Store initial options for immediate display
|
||||
const [initialTenantOption, setInitialTenantOption] = useState<{ value: string; label: string } | null>(null);
|
||||
// const [initialTenantOption, setInitialTenantOption] = useState<{ value: string; label: string } | null>(null);
|
||||
const [initialRoleOption, setInitialRoleOption] = useState<{ value: string; label: string } | null>(null);
|
||||
|
||||
console.log('roleIdValue', roleIdValue);
|
||||
console.log('initialRoleOption', initialRoleOption);
|
||||
|
||||
// Load tenants for dropdown - ensure selected tenant is included
|
||||
const loadTenants = async (page: number, limit: number) => {
|
||||
const response = await tenantService.getAll(page, limit);
|
||||
let options = response.data.map((tenant) => ({
|
||||
value: tenant.id,
|
||||
label: tenant.name,
|
||||
}));
|
||||
// const loadTenants = async (page: number, limit: number) => {
|
||||
// const response = await tenantService.getAll(page, limit);
|
||||
// let options = response.data.map((tenant) => ({
|
||||
// value: tenant.id,
|
||||
// label: tenant.name,
|
||||
// }));
|
||||
|
||||
// Always include initial option if it exists and matches the selected value
|
||||
if (initialTenantOption && page === 1) {
|
||||
const exists = options.find((opt) => opt.value === initialTenantOption.value);
|
||||
if (!exists) {
|
||||
options = [initialTenantOption, ...options];
|
||||
}
|
||||
}
|
||||
// // Always include initial option if it exists and matches the selected value
|
||||
// if (initialTenantOption && page === 1) {
|
||||
// const exists = options.find((opt) => opt.value === initialTenantOption.value);
|
||||
// if (!exists) {
|
||||
// options = [initialTenantOption, ...options];
|
||||
// }
|
||||
// }
|
||||
|
||||
// If we have a selected tenant ID and it's not in the current options, add it with stored name
|
||||
if (selectedTenantId && page === 1 && !initialTenantOption) {
|
||||
const existingOption = options.find((opt) => opt.value === selectedTenantId);
|
||||
if (!existingOption) {
|
||||
// If we have the name from user response, use it; otherwise fetch
|
||||
if (currentTenantName) {
|
||||
options = [
|
||||
{
|
||||
value: selectedTenantId,
|
||||
label: currentTenantName,
|
||||
},
|
||||
...options,
|
||||
];
|
||||
} else {
|
||||
try {
|
||||
const tenantResponse = await tenantService.getById(selectedTenantId);
|
||||
if (tenantResponse.success) {
|
||||
// Prepend the selected tenant to the options
|
||||
options = [
|
||||
{
|
||||
value: tenantResponse.data.id,
|
||||
label: tenantResponse.data.name,
|
||||
},
|
||||
...options,
|
||||
];
|
||||
}
|
||||
} catch (err) {
|
||||
// If fetching fails, just continue with existing options
|
||||
console.warn('Failed to fetch selected tenant:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// // If we have a selected tenant ID and it's not in the current options, add it with stored name
|
||||
// if (selectedTenantId && page === 1 && !initialTenantOption) {
|
||||
// const existingOption = options.find((opt) => opt.value === selectedTenantId);
|
||||
// if (!existingOption) {
|
||||
// // If we have the name from user response, use it; otherwise fetch
|
||||
// if (currentTenantName) {
|
||||
// options = [
|
||||
// {
|
||||
// value: selectedTenantId,
|
||||
// label: currentTenantName,
|
||||
// },
|
||||
// ...options,
|
||||
// ];
|
||||
// } else {
|
||||
// try {
|
||||
// const tenantResponse = await tenantService.getById(selectedTenantId);
|
||||
// if (tenantResponse.success) {
|
||||
// // Prepend the selected tenant to the options
|
||||
// options = [
|
||||
// {
|
||||
// value: tenantResponse.data.id,
|
||||
// label: tenantResponse.data.name,
|
||||
// },
|
||||
// ...options,
|
||||
// ];
|
||||
// }
|
||||
// } catch (err) {
|
||||
// // If fetching fails, just continue with existing options
|
||||
// console.warn('Failed to fetch selected tenant:', err);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
options,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
};
|
||||
// return {
|
||||
// options,
|
||||
// pagination: response.pagination,
|
||||
// };
|
||||
// };
|
||||
|
||||
// Load roles for dropdown - ensure selected role is included
|
||||
const loadRoles = async (page: number, limit: number) => {
|
||||
@ -216,18 +219,18 @@ export const EditUserModal = ({
|
||||
// Extract tenant and role IDs from nested objects or fallback to direct properties
|
||||
const tenantId = user.tenant?.id || user.tenant_id || '';
|
||||
const roleId = user.role?.id || user.role_id || '';
|
||||
const tenantName = user.tenant?.name || '';
|
||||
// const tenantName = user.tenant?.name || '';
|
||||
const roleName = user.role?.name || '';
|
||||
|
||||
setSelectedTenantId(tenantId);
|
||||
// setSelectedTenantId(tenantId);
|
||||
setSelectedRoleId(roleId);
|
||||
setCurrentTenantName(tenantName);
|
||||
// setCurrentTenantName(tenantName);
|
||||
setCurrentRoleName(roleName);
|
||||
|
||||
// Set initial options for immediate display using names from user response
|
||||
if (tenantId && tenantName) {
|
||||
setInitialTenantOption({ value: tenantId, label: tenantName });
|
||||
}
|
||||
// if (tenantId && tenantName) {
|
||||
// setInitialTenantOption({ value: tenantId, label: tenantName });
|
||||
// }
|
||||
if (roleId && roleName) {
|
||||
setInitialRoleOption({ value: roleId, label: roleName });
|
||||
}
|
||||
@ -256,11 +259,11 @@ export const EditUserModal = ({
|
||||
} else if (!isOpen) {
|
||||
// Only reset when modal is closed
|
||||
loadedUserIdRef.current = null;
|
||||
setSelectedTenantId('');
|
||||
// setSelectedTenantId('');
|
||||
setSelectedRoleId('');
|
||||
setCurrentTenantName('');
|
||||
// setCurrentTenantName('');
|
||||
setCurrentRoleName('');
|
||||
setInitialTenantOption(null);
|
||||
// setInitialTenantOption(null);
|
||||
setInitialRoleOption(null);
|
||||
reset({
|
||||
email: '',
|
||||
@ -404,8 +407,8 @@ export const EditUserModal = ({
|
||||
</div>
|
||||
|
||||
{/* Tenant and Role Row */}
|
||||
<div className={`grid ${defaultTenantId ? 'grid-cols-1' : 'grid-cols-2'} gap-5 pb-4`}>
|
||||
{!defaultTenantId && (
|
||||
<div className={`grid grid-cols-2 gap-5 pb-4`}>
|
||||
{/* {!defaultTenantId && (
|
||||
<PaginatedSelect
|
||||
label="Assign Tenant"
|
||||
required
|
||||
@ -416,8 +419,8 @@ export const EditUserModal = ({
|
||||
initialOption={initialTenantOption || undefined}
|
||||
error={errors.tenant_id?.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
)} */}
|
||||
{currentRoleName !== 'Tenant Admin' && (
|
||||
<PaginatedSelect
|
||||
label="Assign Role"
|
||||
required
|
||||
@ -428,8 +431,7 @@ export const EditUserModal = ({
|
||||
initialOption={initialRoleOption || undefined}
|
||||
error={errors.role_id?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{/* Status */}
|
||||
<FormSelect
|
||||
label="Status"
|
||||
@ -441,6 +443,8 @@ export const EditUserModal = ({
|
||||
error={errors.status?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@ -69,7 +69,7 @@ const newRoleSchema = z.object({
|
||||
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
|
||||
),
|
||||
description: z.string().min(1, 'Description is required'),
|
||||
modules: z.array(z.string().uuid()).optional().nullable(),
|
||||
modules: z.array(z.uuid()).optional().nullable(),
|
||||
permissions: z.array(z.object({
|
||||
resource: z.string(),
|
||||
action: z.string(),
|
||||
|
||||
@ -16,7 +16,7 @@ import { roleService } from '@/services/role-service';
|
||||
// Validation schema
|
||||
const newUserSchema = z
|
||||
.object({
|
||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
||||
email: z.email({ message: 'Please enter a valid email address' }),
|
||||
password: z.string().min(1, 'Password is required').min(6, 'Password must be at least 6 characters'),
|
||||
confirmPassword: z.string().min(1, 'Confirm password is required'),
|
||||
first_name: z.string().min(1, 'First name is required'),
|
||||
|
||||
@ -31,10 +31,15 @@ const newModuleSchema = z.object({
|
||||
.max(50, 'runtime_language must be at most 50 characters'),
|
||||
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(),
|
||||
base_url: z
|
||||
frontend_base_url: z
|
||||
.string()
|
||||
.min(1, 'base_url is required')
|
||||
.max(255, 'base_url must be at most 255 characters')
|
||||
.min(1, 'frontend_base_url is required')
|
||||
.max(255, 'frontend_base_url must be at most 255 characters')
|
||||
.url('Invalid URL format'),
|
||||
backend_base_url: z
|
||||
.string()
|
||||
.min(1, 'backend_base_url is required')
|
||||
.max(255, 'backend_base_url must be at most 255 characters')
|
||||
.url('Invalid URL format'),
|
||||
health_endpoint: z
|
||||
.string()
|
||||
@ -50,8 +55,8 @@ const newModuleSchema = z.object({
|
||||
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(),
|
||||
consecutive_failures: z.number().int().optional().nullable(),
|
||||
registered_by: z.string().uuid().optional().nullable(),
|
||||
tenant_id: z.string().uuid().optional().nullable(),
|
||||
registered_by: z.uuid().optional().nullable(),
|
||||
tenant_id: z.uuid().optional().nullable(),
|
||||
metadata: z.any().optional().nullable(),
|
||||
});
|
||||
|
||||
@ -113,7 +118,8 @@ export const NewModuleModal = ({
|
||||
runtime_language: '',
|
||||
framework: null,
|
||||
webhookurl: null,
|
||||
base_url: '',
|
||||
frontend_base_url: '',
|
||||
backend_base_url: '',
|
||||
health_endpoint: '',
|
||||
endpoints: null,
|
||||
kafka_topics: null,
|
||||
@ -155,7 +161,7 @@ export const NewModuleModal = ({
|
||||
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
|
||||
const validationErrors = error.response.data.details;
|
||||
validationErrors.forEach((detail: { path: string; message: string }) => {
|
||||
if (detail.path === 'name' || detail.path === 'module_id' || detail.path === 'description' || detail.path === 'version' || detail.path === 'runtime_language' || detail.path === 'framework' || detail.path === 'webhookurl' || detail.path === 'base_url' || detail.path === 'health_endpoint' || detail.path === 'endpoints' || detail.path === 'kafka_topics' || detail.path === 'cpu_request' || detail.path === 'cpu_limit' || detail.path === 'memory_request' || detail.path === 'memory_limit' || detail.path === 'min_replicas' || detail.path === 'max_replicas' || detail.path === 'last_health_check' || detail.path === 'consecutive_failures' || detail.path === 'registered_by' || detail.path === 'tenant_id' || detail.path === 'metadata') {
|
||||
if (detail.path === 'name' || detail.path === 'module_id' || detail.path === 'description' || detail.path === 'version' || detail.path === 'runtime_language' || detail.path === 'framework' || detail.path === 'webhookurl' || detail.path === 'frontend_base_url' || detail.path === 'backend_base_url' || detail.path === 'health_endpoint' || detail.path === 'endpoints' || detail.path === 'kafka_topics' || detail.path === 'cpu_request' || detail.path === 'cpu_limit' || detail.path === 'memory_request' || detail.path === 'memory_limit' || detail.path === 'min_replicas' || detail.path === 'max_replicas' || detail.path === 'last_health_check' || detail.path === 'consecutive_failures' || detail.path === 'registered_by' || detail.path === 'tenant_id' || detail.path === 'metadata') {
|
||||
setError(detail.path as keyof NewModuleFormData, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
@ -370,14 +376,28 @@ export const NewModuleModal = ({
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">URL Configuration</h3>
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex gap-5">
|
||||
<div className="flex-1">
|
||||
<FormField
|
||||
label="Base URL"
|
||||
label="Frontend Base URL"
|
||||
required
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
error={errors.base_url?.message}
|
||||
{...register('base_url')}
|
||||
placeholder="https://frontend.example.com"
|
||||
error={errors.frontend_base_url?.message}
|
||||
{...register('frontend_base_url')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<FormField
|
||||
label="Backend Base URL"
|
||||
required
|
||||
type="url"
|
||||
placeholder="https://backend.example.com"
|
||||
error={errors.backend_base_url?.message}
|
||||
{...register('backend_base_url')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
label="Health Endpoint"
|
||||
|
||||
@ -145,8 +145,12 @@ export const ViewModuleModal = ({
|
||||
<p className="text-sm font-medium text-[#0e1b2a]">{module.framework || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Base URL</label>
|
||||
<p className="text-sm font-medium text-[#0e1b2a] font-mono break-all">{module.base_url}</p>
|
||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Frontend Base URL</label>
|
||||
<p className="text-sm font-medium text-[#0e1b2a] font-mono break-all">{module.frontend_base_url || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Backend Base URL</label>
|
||||
<p className="text-sm font-medium text-[#0e1b2a] font-mono break-all">{module.backend_base_url || 'N/A'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Health Endpoint</label>
|
||||
|
||||
@ -34,13 +34,14 @@ const tenantDetailsSchema = z.object({
|
||||
}).optional().nullable(),
|
||||
max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(),
|
||||
max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(),
|
||||
modules: z.array(z.string().uuid()).optional().nullable(),
|
||||
modules: z.array(z.uuid()).optional().nullable(),
|
||||
});
|
||||
|
||||
// Step 2: Contact Details Schema - user creation + organization address
|
||||
const contactDetailsSchema = z
|
||||
.object({
|
||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
||||
email: z
|
||||
.email({ message: 'Please enter a valid email address' }),
|
||||
password: z.string().min(1, 'Password is required').min(6, 'Password must be at least 6 characters'),
|
||||
confirmPassword: z.string().min(1, 'Confirm password is required'),
|
||||
first_name: z.string().min(1, 'First name is required'),
|
||||
@ -113,6 +114,8 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
// Form instances for each step
|
||||
const tenantDetailsForm = useForm<TenantDetailsForm>({
|
||||
resolver: zodResolver(tenantDetailsSchema),
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
name: '',
|
||||
slug: '',
|
||||
@ -139,6 +142,8 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
|
||||
const contactDetailsForm = useForm<ContactDetailsForm>({
|
||||
resolver: zodResolver(contactDetailsSchema),
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
@ -157,6 +162,8 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
|
||||
const settingsForm = useForm<SettingsForm>({
|
||||
resolver: zodResolver(settingsSchema),
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
enable_sso: false,
|
||||
enable_2fa: false,
|
||||
@ -331,14 +338,18 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
setLogoError(null);
|
||||
setFaviconError(null);
|
||||
|
||||
if (!logoFileUrl && !logoFile) {
|
||||
const isLogoMissing = !logoFileUrl && !logoFile;
|
||||
const isFaviconMissing = !faviconFileUrl && !faviconFile;
|
||||
|
||||
if (isLogoMissing) {
|
||||
setLogoError('Logo is required');
|
||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
||||
return;
|
||||
}
|
||||
|
||||
if (!faviconFileUrl && !faviconFile) {
|
||||
if (isFaviconMissing) {
|
||||
setFaviconError('Favicon is required');
|
||||
}
|
||||
|
||||
if (isLogoMissing || isFaviconMissing) {
|
||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
||||
return;
|
||||
}
|
||||
@ -379,40 +390,122 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
showToast.success(message);
|
||||
navigate('/tenants');
|
||||
} catch (err: any) {
|
||||
// Handle validation errors from API - same as NewTenantModal
|
||||
// Clear previous errors
|
||||
tenantDetailsForm.clearErrors();
|
||||
contactDetailsForm.clearErrors();
|
||||
settingsForm.clearErrors();
|
||||
|
||||
// Handle validation errors from API
|
||||
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
|
||||
const validationErrors = err.response.data.details;
|
||||
let hasTenantErrors = false;
|
||||
let hasContactErrors = false;
|
||||
let hasSettingsErrors = false;
|
||||
|
||||
validationErrors.forEach((detail: { path: string; message: string }) => {
|
||||
// Handle tenant details errors
|
||||
if (
|
||||
detail.path === 'name' ||
|
||||
detail.path === 'slug' ||
|
||||
detail.path === 'status' ||
|
||||
detail.path === 'subscription_tier' ||
|
||||
detail.path === 'max_users' ||
|
||||
detail.path === 'max_modules' ||
|
||||
detail.path === 'module_ids'
|
||||
const path = detail.path;
|
||||
|
||||
// Handle nested paths first
|
||||
if (path.startsWith('settings.contact.')) {
|
||||
// Contact details errors from nested path
|
||||
hasContactErrors = true;
|
||||
const fieldName = path.replace('settings.contact.', '');
|
||||
if (fieldName === 'confirmPassword') {
|
||||
// Skip confirmPassword as it's not in the form schema
|
||||
return;
|
||||
}
|
||||
contactDetailsForm.setError(fieldName as keyof ContactDetailsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
} else if (path.startsWith('settings.branding.')) {
|
||||
// Settings/branding errors from nested path
|
||||
hasSettingsErrors = true;
|
||||
const fieldName = path.replace('settings.branding.', '');
|
||||
// Map file_path fields to form fields
|
||||
if (fieldName === 'logo_file_path') {
|
||||
setLogoError(detail.message);
|
||||
} else if (fieldName === 'favicon_file_path') {
|
||||
setFaviconError(detail.message);
|
||||
} else {
|
||||
settingsForm.setError(fieldName as keyof SettingsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
} else if (path.startsWith('settings.')) {
|
||||
// Other settings errors
|
||||
hasSettingsErrors = true;
|
||||
const fieldName = path.replace('settings.', '');
|
||||
settingsForm.setError(fieldName as keyof SettingsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
// Handle tenant details errors (step 1)
|
||||
else if (
|
||||
path === 'name' ||
|
||||
path === 'slug' ||
|
||||
path === 'domain' ||
|
||||
path === 'status' ||
|
||||
path === 'subscription_tier' ||
|
||||
path === 'max_users' ||
|
||||
path === 'max_modules' ||
|
||||
path === 'module_ids'
|
||||
) {
|
||||
const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path;
|
||||
hasTenantErrors = true;
|
||||
const fieldPath = path === 'module_ids' ? 'modules' : path;
|
||||
tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
// Handle contact details errors
|
||||
// Handle contact details errors (step 2) - direct paths
|
||||
else if (
|
||||
detail.path === 'email' ||
|
||||
detail.path === 'password' ||
|
||||
detail.path === 'first_name' ||
|
||||
detail.path === 'last_name'
|
||||
path === 'email' ||
|
||||
path === 'password' ||
|
||||
path === 'first_name' ||
|
||||
path === 'last_name' ||
|
||||
path === 'contact_phone' ||
|
||||
path === 'address_line1' ||
|
||||
path === 'address_line2' ||
|
||||
path === 'city' ||
|
||||
path === 'state' ||
|
||||
path === 'postal_code' ||
|
||||
path === 'country'
|
||||
) {
|
||||
contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, {
|
||||
hasContactErrors = true;
|
||||
contactDetailsForm.setError(path as keyof ContactDetailsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
// Handle settings errors (step 3) - direct paths
|
||||
else if (
|
||||
path === 'enable_sso' ||
|
||||
path === 'enable_2fa' ||
|
||||
path === 'primary_color' ||
|
||||
path === 'secondary_color' ||
|
||||
path === 'accent_color'
|
||||
) {
|
||||
hasSettingsErrors = true;
|
||||
settingsForm.setError(path as keyof SettingsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate to the step with errors
|
||||
if (hasTenantErrors) {
|
||||
setCurrentStep(1);
|
||||
} else if (hasContactErrors) {
|
||||
setCurrentStep(2);
|
||||
} else if (hasSettingsErrors) {
|
||||
setCurrentStep(3);
|
||||
}
|
||||
} else {
|
||||
// Handle general errors
|
||||
const errorObj = err?.response?.data?.error;
|
||||
const errorMessage =
|
||||
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
|
||||
@ -425,6 +518,7 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.',
|
||||
});
|
||||
showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.');
|
||||
setCurrentStep(1); // Navigate to first step for general errors
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@ -778,6 +872,12 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
Set resource limits and security preferences for this tenant.
|
||||
</p>
|
||||
</div>
|
||||
{/* General Error Display */}
|
||||
{settingsForm.formState.errors.root && (
|
||||
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.root.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Branding Section */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
||||
|
||||
@ -34,12 +34,12 @@ const tenantDetailsSchema = z.object({
|
||||
}).optional().nullable(),
|
||||
max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(),
|
||||
max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(),
|
||||
modules: z.array(z.string().uuid()).optional().nullable(),
|
||||
modules: z.array(z.uuid()).optional().nullable(),
|
||||
});
|
||||
|
||||
// Step 2: Contact Details Schema - NO password fields
|
||||
const contactDetailsSchema = z.object({
|
||||
email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
|
||||
email: z.email({ message: 'Please enter a valid email address' }),
|
||||
first_name: z.string().min(1, 'First name is required'),
|
||||
last_name: z.string().min(1, 'Last name is required'),
|
||||
contact_phone: z
|
||||
@ -122,6 +122,8 @@ const EditTenant = (): ReactElement => {
|
||||
// Form instances for each step
|
||||
const tenantDetailsForm = useForm<TenantDetailsForm>({
|
||||
resolver: zodResolver(tenantDetailsSchema),
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
name: '',
|
||||
slug: '',
|
||||
@ -136,6 +138,8 @@ const EditTenant = (): ReactElement => {
|
||||
|
||||
const contactDetailsForm = useForm<ContactDetailsForm>({
|
||||
resolver: zodResolver(contactDetailsSchema),
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
email: '',
|
||||
first_name: '',
|
||||
@ -152,6 +156,8 @@ const EditTenant = (): ReactElement => {
|
||||
|
||||
const settingsForm = useForm<SettingsForm>({
|
||||
resolver: zodResolver(settingsSchema),
|
||||
mode: 'onChange',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
enable_sso: false,
|
||||
enable_2fa: false,
|
||||
@ -433,14 +439,18 @@ const EditTenant = (): ReactElement => {
|
||||
setLogoError(null);
|
||||
setFaviconError(null);
|
||||
|
||||
if (!logoFilePath) {
|
||||
const isLogoMissing = !logoFilePath;
|
||||
const isFaviconMissing = !faviconFilePath;
|
||||
|
||||
if (isLogoMissing) {
|
||||
setLogoError('Logo is required');
|
||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
||||
return;
|
||||
}
|
||||
|
||||
if (!faviconFilePath) {
|
||||
if (isFaviconMissing) {
|
||||
setFaviconError('Favicon is required');
|
||||
}
|
||||
|
||||
if (isLogoMissing || isFaviconMissing) {
|
||||
setCurrentStep(3); // Go to settings step where logo/favicon are
|
||||
return;
|
||||
}
|
||||
@ -476,47 +486,130 @@ const EditTenant = (): ReactElement => {
|
||||
showToast.success(message);
|
||||
navigate('/tenants');
|
||||
} catch (err: any) {
|
||||
// Clear previous errors
|
||||
tenantDetailsForm.clearErrors();
|
||||
contactDetailsForm.clearErrors();
|
||||
settingsForm.clearErrors();
|
||||
|
||||
// Handle validation errors from API
|
||||
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
|
||||
const validationErrors = err.response.data.details;
|
||||
let hasTenantErrors = false;
|
||||
let hasContactErrors = false;
|
||||
let hasSettingsErrors = false;
|
||||
|
||||
validationErrors.forEach((detail: { path: string; message: string }) => {
|
||||
if (
|
||||
detail.path === 'name' ||
|
||||
detail.path === 'slug' ||
|
||||
detail.path === 'status' ||
|
||||
detail.path === 'subscription_tier' ||
|
||||
detail.path === 'max_users' ||
|
||||
detail.path === 'max_modules' ||
|
||||
detail.path === 'module_ids'
|
||||
const path = detail.path;
|
||||
|
||||
// Handle nested paths first
|
||||
if (path.startsWith('settings.contact.')) {
|
||||
// Contact details errors from nested path
|
||||
hasContactErrors = true;
|
||||
const fieldName = path.replace('settings.contact.', '');
|
||||
contactDetailsForm.setError(fieldName as keyof ContactDetailsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
} else if (path.startsWith('settings.branding.')) {
|
||||
// Settings/branding errors from nested path
|
||||
hasSettingsErrors = true;
|
||||
const fieldName = path.replace('settings.branding.', '');
|
||||
// Map file_path fields to form fields
|
||||
if (fieldName === 'logo_file_path') {
|
||||
setLogoError(detail.message);
|
||||
} else if (fieldName === 'favicon_file_path') {
|
||||
setFaviconError(detail.message);
|
||||
} else {
|
||||
settingsForm.setError(fieldName as keyof SettingsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
} else if (path.startsWith('settings.')) {
|
||||
// Other settings errors
|
||||
hasSettingsErrors = true;
|
||||
const fieldName = path.replace('settings.', '');
|
||||
settingsForm.setError(fieldName as keyof SettingsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
// Handle tenant details errors (step 1)
|
||||
else if (
|
||||
path === 'name' ||
|
||||
path === 'slug' ||
|
||||
path === 'domain' ||
|
||||
path === 'status' ||
|
||||
path === 'subscription_tier' ||
|
||||
path === 'max_users' ||
|
||||
path === 'max_modules' ||
|
||||
path === 'module_ids'
|
||||
) {
|
||||
const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path;
|
||||
hasTenantErrors = true;
|
||||
const fieldPath = path === 'module_ids' ? 'modules' : path;
|
||||
tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
} else if (
|
||||
detail.path === 'email' ||
|
||||
detail.path === 'first_name' ||
|
||||
detail.path === 'last_name' ||
|
||||
detail.path === 'contact_phone' ||
|
||||
detail.path === 'address_line1' ||
|
||||
detail.path === 'city' ||
|
||||
detail.path === 'state' ||
|
||||
detail.path === 'postal_code' ||
|
||||
detail.path === 'country'
|
||||
}
|
||||
// Handle contact details errors (step 2) - direct paths
|
||||
else if (
|
||||
path === 'email' ||
|
||||
path === 'first_name' ||
|
||||
path === 'last_name' ||
|
||||
path === 'contact_phone' ||
|
||||
path === 'address_line1' ||
|
||||
path === 'address_line2' ||
|
||||
path === 'city' ||
|
||||
path === 'state' ||
|
||||
path === 'postal_code' ||
|
||||
path === 'country'
|
||||
) {
|
||||
contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, {
|
||||
hasContactErrors = true;
|
||||
contactDetailsForm.setError(path as keyof ContactDetailsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
// Handle settings errors (step 3) - direct paths
|
||||
else if (
|
||||
path === 'enable_sso' ||
|
||||
path === 'enable_2fa' ||
|
||||
path === 'primary_color' ||
|
||||
path === 'secondary_color' ||
|
||||
path === 'accent_color'
|
||||
) {
|
||||
hasSettingsErrors = true;
|
||||
settingsForm.setError(path as keyof SettingsForm, {
|
||||
type: 'server',
|
||||
message: detail.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate to the step with errors
|
||||
if (hasTenantErrors) {
|
||||
setCurrentStep(1);
|
||||
} else if (hasContactErrors) {
|
||||
setCurrentStep(2);
|
||||
} else if (hasSettingsErrors) {
|
||||
setCurrentStep(3);
|
||||
}
|
||||
} else {
|
||||
// Handle general errors
|
||||
const errorObj = err?.response?.data?.error;
|
||||
const errorMessage =
|
||||
err?.response?.data?.error?.message ||
|
||||
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
|
||||
(typeof errorObj === 'string' ? errorObj : null) ||
|
||||
err?.response?.data?.message ||
|
||||
err?.message ||
|
||||
'Failed to update tenant. Please try again.';
|
||||
showToast.error(errorMessage);
|
||||
tenantDetailsForm.setError('root', {
|
||||
type: 'server',
|
||||
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.',
|
||||
});
|
||||
showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.');
|
||||
setCurrentStep(1); // Navigate to first step for general errors
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
@ -878,6 +971,12 @@ const EditTenant = (): ReactElement => {
|
||||
<h2 className="text-lg font-semibold text-[#0f1724]">Configuration & Limits</h2>
|
||||
<p className="text-sm text-[#6b7280] mt-1">Set resource limits and security preferences for this tenant.</p>
|
||||
</div>
|
||||
{/* General Error Display */}
|
||||
{settingsForm.formState.errors.root && (
|
||||
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||
<p className="text-sm text-[#ef4444]">{settingsForm.formState.errors.root.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Branding Section - Same as CreateTenantWizard */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
||||
|
||||
@ -245,13 +245,18 @@ const Settings = (): ReactElement => {
|
||||
setLogoError(null);
|
||||
setFaviconError(null);
|
||||
|
||||
if (!logoFilePath) {
|
||||
const isLogoMissing = !logoFilePath;
|
||||
const isFaviconMissing = !faviconFilePath;
|
||||
|
||||
if (isLogoMissing) {
|
||||
setLogoError('Logo is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!faviconFilePath) {
|
||||
if (isFaviconMissing) {
|
||||
setFaviconError('Favicon is required');
|
||||
}
|
||||
|
||||
if (isLogoMissing || isFaviconMissing) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@ export interface Module {
|
||||
runtime_language: string | null;
|
||||
framework: string | null;
|
||||
webhookurl: string | null;
|
||||
base_url: string;
|
||||
frontend_base_url: string;
|
||||
backend_base_url: string;
|
||||
health_endpoint: string;
|
||||
endpoints: string[] | null;
|
||||
kafka_topics: string[] | null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user