Implement webhook URL functionality in NewModuleModal and ViewModuleModal. Update Modules component to include launch module feature for super admins, enhancing module management capabilities. Refactor Sidebar for improved layout and responsiveness.

This commit is contained in:
Yashwin 2026-02-03 18:05:26 +05:30
parent 803ca1cd1a
commit 73e533694e
8 changed files with 172 additions and 114 deletions

View File

@ -134,8 +134,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
<div className="w-full">
<div className="flex flex-col gap-1">
<div className="pb-1 px-3">
<div className="text-[11px] font-semibold text-[#6b7280] uppercase tracking-[0.88px]">
<div className="pb-1 px-2 md:px-2 lg:px-3">
<div className="text-[10px] md:text-[10px] lg:text-[11px] font-semibold text-[#6b7280] uppercase tracking-[0.88px]">
{title}
</div>
</div>
@ -154,7 +154,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
}
}}
className={cn(
'flex items-center gap-2.5 px-3 py-2 rounded-md transition-colors min-h-[44px]',
'flex items-center gap-2 md:gap-2 lg:gap-2.5 px-2 md:px-2 lg:px-3 py-2 rounded-md transition-colors min-h-[44px]',
isActive
? 'shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]'
: 'text-[#0f1724] hover:bg-gray-50'
@ -169,7 +169,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
}
>
<Icon className="w-4 h-4 shrink-0" />
<span className="text-[13px] font-medium whitespace-nowrap">{item.label}</span>
<span className="text-xs md:text-xs lg:text-[13px] font-medium whitespace-nowrap">{item.label}</span>
</Link>
);
})}
@ -241,9 +241,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
</aside>
{/* Desktop Sidebar */}
<aside className="hidden md:flex bg-white border border-[rgba(0,0,0,0.08)] rounded-xl p-3 md:p-3 lg:p-[17px] w-[220px] md:w-[220px] lg:w-[240px] h-full max-h-screen flex-col gap-4 md:gap-4 lg:gap-6 shrink-0 overflow-hidden">
<aside className="hidden md:flex bg-white border border-[rgba(0,0,0,0.08)] rounded-xl p-2 md:p-2.5 lg:p-3 xl:p-[17px] w-[160px] md:w-[160px] lg:w-[180px] xl:w-[240px] h-full max-h-screen flex-col gap-3 md:gap-3.5 lg:gap-4 xl:gap-6 shrink-0 overflow-hidden">
{/* Logo */}
<div className="w-full md:w-[190px] lg:w-[206px] shrink-0">
<div className="w-full md:w-[140px] lg:w-[160px] xl:w-[206px] shrink-0">
<div className="flex gap-3 items-center px-2">
{!isSuperAdmin && logoUrl ? (
<img
@ -266,7 +266,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
>
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
</div>
<div className="text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
<div className="text-base md:text-base lg:text-base xl:text-[18px] font-bold text-[#0f1724] tracking-[-0.36px]">
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
</div>
</div>
@ -284,9 +284,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{/* Support Center */}
<div className="mt-auto w-full">
<button className="w-full bg-white border border-[rgba(0,0,0,0.08)] rounded-md px-[13px] py-[9px] flex gap-2.5 items-center hover:bg-gray-50 transition-colors">
<button className="w-full bg-white border border-[rgba(0,0,0,0.08)] rounded-md px-2 md:px-2.5 lg:px-[13px] py-[9px] flex gap-2 md:gap-2 lg:gap-2.5 items-center hover:bg-gray-50 transition-colors">
<HelpCircle className="w-4 h-4 shrink-0 text-[#0f1724]" />
<span className="text-[13px] font-medium text-[#0f1724]">Support Center</span>
<span className="text-xs md:text-xs lg:text-[13px] font-medium text-[#0f1724]">Support Center</span>
</button>
</div>
</aside>

View File

@ -30,6 +30,7 @@ const newModuleSchema = z.object({
.min(1, 'runtime_language is required')
.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
.string()
.min(1, 'base_url is required')
@ -85,6 +86,7 @@ export const NewModuleModal = ({
defaultValues: {
description: null,
framework: null,
webhookurl: null,
endpoints: null,
kafka_topics: null,
cpu_request: null,
@ -112,6 +114,7 @@ export const NewModuleModal = ({
version: '',
runtime_language: '',
framework: null,
webhookurl: null,
base_url: '',
health_endpoint: '',
endpoints: null,
@ -155,7 +158,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 === '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 === 'health_status' || 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 === '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 === 'health_status' || 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,
@ -352,6 +355,18 @@ export const NewModuleModal = ({
/>
</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 */}

View File

@ -152,6 +152,10 @@ export const ViewModuleModal = ({
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Health Endpoint</label>
<p className="text-sm font-medium text-[#0e1b2a]">{module.health_endpoint}</p>
</div>
<div>
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Webhook URL</label>
<p className="text-sm font-medium text-[#0e1b2a]">{module.webhookurl || '-'}</p>
</div>
</div>
</div>

View File

@ -197,7 +197,7 @@ const Modules = (): ReactElement => {
label: 'Actions',
align: 'right',
render: (module) => (
<div className="flex justify-end">
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => handleViewModule(module.id)}

View File

@ -9,6 +9,7 @@ import {
import { useAppSelector } from '@/hooks/redux-hooks';
import { tenantService } from '@/services/tenant-service';
import type { AssignedModule } from '@/types/tenant';
import { moduleService } from '@/services/module-service';
// Helper function to get status badge variant
const getStatusVariant = (status: string | null): 'success' | 'failure' | 'process' => {
@ -26,7 +27,8 @@ const getStatusVariant = (status: string | null): 'success' | 'failure' | 'proce
};
const Modules = (): ReactElement => {
const tenantId = useAppSelector((state) => state.auth.tenantId);
const { roles, tenantId } = useAppSelector((state) => state.auth);
// const tenantId = useAppSelector((state) => state.auth.tenantId);
const [modules, setModules] = useState<AssignedModule[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@ -58,6 +60,33 @@ const Modules = (): ReactElement => {
fetchTenantModules();
}, [tenantId]);
// Launch module handler
const handleLaunchModule = async (moduleId: string): Promise<void> => {
try {
// Check if user is super_admin
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
rolesArray = roles;
} else if (typeof roles === 'string') {
try {
rolesArray = JSON.parse(roles);
} catch {
rolesArray = [];
}
}
const isSuperAdmin = rolesArray.includes('super_admin');
// If super_admin, pass tenantId as query param
const launchTenantId = isSuperAdmin ? tenantId : null;
const response = await moduleService.launch(moduleId, launchTenantId);
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');
}
};
// Define table columns
const columns: Column<AssignedModule>[] = [
{
@ -101,6 +130,22 @@ const Modules = (): ReactElement => {
),
mobileLabel: 'URL',
},
{
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>
),
},
];
// Mobile card renderer
@ -131,6 +176,15 @@ const Modules = (): ReactElement => {
{module.base_url || 'N/A'}
</p>
</div>
<div className="col-span-2">
<button
type="button"
onClick={() => handleLaunchModule(module.id)}
className="text-sm text-[#FFC107] hover:text-[#FFC107] font-medium transition-colors"
>
Lunch
</button>
</div>
</div>
</div>
);

View File

@ -1,5 +1,5 @@
import apiClient from './api-client';
import type { ModulesResponse, GetModuleResponse, CreateModuleRequest, CreateModuleResponse } from '@/types/module';
import type { ModulesResponse, GetModuleResponse, CreateModuleRequest, CreateModuleResponse, LaunchModuleResponse } from '@/types/module';
export const moduleService = {
getAll: async (
@ -66,4 +66,13 @@ export const moduleService = {
const response = await apiClient.post<CreateModuleResponse>('/modules', data);
return response.data;
},
launch: async (id: string, tenantId?: string | null): Promise<LaunchModuleResponse> => {
const params = new URLSearchParams();
if (tenantId) {
params.append('tenant_id', tenantId);
}
const url = `/modules/${id}/launch${params.toString() ? `?${params.toString()}` : ''}`;
const response = await apiClient.post<LaunchModuleResponse>(url);
return response.data;
},
};

View File

@ -7,6 +7,7 @@ export interface Module {
status: string;
runtime_language: string | null;
framework: string | null;
webhookurl: string | null;
base_url: string;
health_endpoint: string;
endpoints: string[] | null;
@ -83,3 +84,20 @@ export interface CreateModuleResponse {
};
message?: string;
}
export interface LaunchModuleResponse {
success: boolean;
data: {
launch_url: string;
expires_in: number;
module: {
id: string;
module_id: string;
name: string;
};
tenant: {
id: string;
name: string;
};
};
}

View File

@ -11,46 +11,4 @@ export default defineConfig({
"@": path.resolve(__dirname, "src"),
},
},
// build: {
// rollupOptions: {
// output: {
// manualChunks(id) {
// // Feature-based chunks - only for source files
// if (!id.includes('node_modules')) {
// if (id.includes('/src/pages/superadmin/')) {
// return 'superadmin';
// }
// if (id.includes('/src/pages/tenant/')) {
// return 'tenant';
// }
// return;
// }
// // Vendor chunks - group React ecosystem (including Redux) together
// // to avoid circular dependencies
// if (
// id.includes('node_modules/react') ||
// id.includes('node_modules/react-dom') ||
// id.includes('node_modules/react-router') ||
// id.includes('node_modules/@reduxjs') ||
// id.includes('node_modules/redux') ||
// id.includes('node_modules/react-redux') ||
// id.includes('node_modules/scheduler') ||
// id.includes('node_modules/object-assign')
// ) {
// return 'react-vendor';
// }
// // UI libraries
// if (id.includes('node_modules/lucide-react')) {
// return 'ui-vendor';
// }
// // All other node_modules go to vendor
// return 'vendor';
// },
// },
// },
// chunkSizeWarningLimit: 600,
// },
})