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:
parent
803ca1cd1a
commit
73e533694e
@ -134,8 +134,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
const MenuSection = ({ title, items }: { title: string; items: MenuItem[] }) => (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="pb-1 px-3">
|
<div className="pb-1 px-2 md:px-2 lg:px-3">
|
||||||
<div className="text-[11px] font-semibold text-[#6b7280] uppercase tracking-[0.88px]">
|
<div className="text-[10px] md:text-[10px] lg:text-[11px] font-semibold text-[#6b7280] uppercase tracking-[0.88px]">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +154,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
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
|
isActive
|
||||||
? 'shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]'
|
? 'shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]'
|
||||||
: 'text-[#0f1724] hover:bg-gray-50'
|
: 'text-[#0f1724] hover:bg-gray-50'
|
||||||
@ -169,7 +169,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4 shrink-0" />
|
<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>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -241,9 +241,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Desktop Sidebar */}
|
{/* 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 */}
|
{/* 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">
|
<div className="flex gap-3 items-center px-2">
|
||||||
{!isSuperAdmin && logoUrl ? (
|
{!isSuperAdmin && logoUrl ? (
|
||||||
<img
|
<img
|
||||||
@ -266,7 +266,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
>
|
>
|
||||||
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
|
||||||
</div>
|
</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'}
|
{(!isSuperAdmin && logoUrl) ? '' : 'QAssure'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -284,9 +284,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
|
|
||||||
{/* Support Center */}
|
{/* Support Center */}
|
||||||
<div className="mt-auto w-full">
|
<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]" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -30,6 +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(),
|
||||||
base_url: z
|
base_url: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, 'base_url is required')
|
.min(1, 'base_url is required')
|
||||||
@ -85,6 +86,7 @@ export const NewModuleModal = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
description: null,
|
description: null,
|
||||||
framework: null,
|
framework: null,
|
||||||
|
webhookurl: null,
|
||||||
endpoints: null,
|
endpoints: null,
|
||||||
kafka_topics: null,
|
kafka_topics: null,
|
||||||
cpu_request: null,
|
cpu_request: null,
|
||||||
@ -112,6 +114,7 @@ export const NewModuleModal = ({
|
|||||||
version: '',
|
version: '',
|
||||||
runtime_language: '',
|
runtime_language: '',
|
||||||
framework: null,
|
framework: null,
|
||||||
|
webhookurl: null,
|
||||||
base_url: '',
|
base_url: '',
|
||||||
health_endpoint: '',
|
health_endpoint: '',
|
||||||
endpoints: null,
|
endpoints: null,
|
||||||
@ -155,7 +158,7 @@ export const NewModuleModal = ({
|
|||||||
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
|
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
|
||||||
const validationErrors = error.response.data.details;
|
const validationErrors = error.response.data.details;
|
||||||
validationErrors.forEach((detail: { path: string; message: string }) => {
|
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, {
|
setError(detail.path as keyof NewModuleFormData, {
|
||||||
type: 'server',
|
type: 'server',
|
||||||
message: detail.message,
|
message: detail.message,
|
||||||
@ -352,6 +355,18 @@ export const NewModuleModal = ({
|
|||||||
/>
|
/>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* URL Configuration Section */}
|
{/* URL Configuration Section */}
|
||||||
|
|||||||
@ -152,6 +152,10 @@ export const ViewModuleModal = ({
|
|||||||
<label className="text-xs font-medium text-[#6b7280] mb-1 block">Health Endpoint</label>
|
<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>
|
<p className="text-sm font-medium text-[#0e1b2a]">{module.health_endpoint}</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -197,7 +197,7 @@ const Modules = (): ReactElement => {
|
|||||||
label: 'Actions',
|
label: 'Actions',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
render: (module) => (
|
render: (module) => (
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleViewModule(module.id)}
|
onClick={() => handleViewModule(module.id)}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
import { useAppSelector } from '@/hooks/redux-hooks';
|
import { useAppSelector } from '@/hooks/redux-hooks';
|
||||||
import { tenantService } from '@/services/tenant-service';
|
import { tenantService } from '@/services/tenant-service';
|
||||||
import type { AssignedModule } from '@/types/tenant';
|
import type { AssignedModule } from '@/types/tenant';
|
||||||
|
import { moduleService } from '@/services/module-service';
|
||||||
|
|
||||||
// Helper function to get status badge variant
|
// Helper function to get status badge variant
|
||||||
const getStatusVariant = (status: string | null): 'success' | 'failure' | 'process' => {
|
const getStatusVariant = (status: string | null): 'success' | 'failure' | 'process' => {
|
||||||
@ -26,7 +27,8 @@ const getStatusVariant = (status: string | null): 'success' | 'failure' | 'proce
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Modules = (): ReactElement => {
|
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 [modules, setModules] = useState<AssignedModule[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -58,6 +60,33 @@ const Modules = (): ReactElement => {
|
|||||||
fetchTenantModules();
|
fetchTenantModules();
|
||||||
}, [tenantId]);
|
}, [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
|
// Define table columns
|
||||||
const columns: Column<AssignedModule>[] = [
|
const columns: Column<AssignedModule>[] = [
|
||||||
{
|
{
|
||||||
@ -101,6 +130,22 @@ const Modules = (): ReactElement => {
|
|||||||
),
|
),
|
||||||
mobileLabel: 'URL',
|
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
|
// Mobile card renderer
|
||||||
@ -131,6 +176,15 @@ const Modules = (): ReactElement => {
|
|||||||
{module.base_url || 'N/A'}
|
{module.base_url || 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import apiClient from './api-client';
|
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 = {
|
export const moduleService = {
|
||||||
getAll: async (
|
getAll: async (
|
||||||
@ -66,4 +66,13 @@ export const moduleService = {
|
|||||||
const response = await apiClient.post<CreateModuleResponse>('/modules', data);
|
const response = await apiClient.post<CreateModuleResponse>('/modules', data);
|
||||||
return response.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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export interface Module {
|
|||||||
status: string;
|
status: string;
|
||||||
runtime_language: string | null;
|
runtime_language: string | null;
|
||||||
framework: string | null;
|
framework: string | null;
|
||||||
|
webhookurl: string | null;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
health_endpoint: string;
|
health_endpoint: string;
|
||||||
endpoints: string[] | null;
|
endpoints: string[] | null;
|
||||||
@ -83,3 +84,20 @@ export interface CreateModuleResponse {
|
|||||||
};
|
};
|
||||||
message?: string;
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -11,46 +11,4 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "src"),
|
"@": 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,
|
|
||||||
// },
|
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user