From ec9c7363f61d8aed7a57c2d595983984bd795b19 Mon Sep 17 00:00:00 2001 From: Dilan Induwara <153802063+Induwara04@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:45:54 +0530 Subject: [PATCH 1/2] Fix UI Issues in AI Workspcae --- portals/ai-workspace/src/hooks/useGateway.ts | 38 ++- .../applications/ApplicationsList.tsx | 3 +- .../GenAIApplicationsSummaryCardSection.tsx | 5 +- .../externalServers/ExternalServersList.tsx | 18 +- .../MCPProxiesSummaryCardSection.tsx | 2 +- .../appShellPages/gateways/EditGateway.tsx | 2 +- .../appShellPages/gateways/GatewaysList.tsx | 2 +- .../appShellPages/gateways/ViewGateway.tsx | 171 ++++++------ .../projects/ProjectListView.tsx | 2 +- .../appShellPages/projects/ProjectsList.tsx | 5 +- .../ProviderTemplateOverview.tsx | 8 +- .../ProviderTemplatesList.tsx | 2 +- .../appShellPages/proxies/LLMProxiesList.tsx | 39 ++- .../proxies/LLMProxiesSummaryCardSection.tsx | 2 +- .../proxies/LLMProxyOverview.tsx | 24 +- .../serviceProvider/ProvidersList.tsx | 3 +- .../ServiceProviderOverview.tsx | 247 +++++++++++++++--- .../ServiceProvidersSummaryCard.tsx | 3 +- 18 files changed, 401 insertions(+), 175 deletions(-) diff --git a/portals/ai-workspace/src/hooks/useGateway.ts b/portals/ai-workspace/src/hooks/useGateway.ts index 86a3cdede5..b16f0c7627 100644 --- a/portals/ai-workspace/src/hooks/useGateway.ts +++ b/portals/ai-workspace/src/hooks/useGateway.ts @@ -20,6 +20,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { getGateways, registerGateway, + rotateGatewayToken, updateGateway, deleteGateway, HybridGateway, @@ -52,7 +53,15 @@ interface UseGatewayListReturn { isDeleting: boolean; } -export function useGatewayList(): UseGatewayListReturn { +type UseGatewayListOptions = { + pollAlways?: boolean; + pollingIntervalMs?: number; +}; + +export function useGatewayList( + options: UseGatewayListOptions = {} +): UseGatewayListReturn { + const { pollAlways = false, pollingIntervalMs = 5000 } = options; const [gateways, setGateways] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -101,13 +110,14 @@ export function useGatewayList(): UseGatewayListReturn { fetchGateways(); }, [fetchGateways]); - // Poll every 5 seconds while any gateway is inactive + // Poll while the page needs fresh status updates. const pollingRef = useRef | null>(null); useEffect(() => { const hasInactive = gateways.some((gw) => !gw.isActive); + const shouldPoll = pollAlways || hasInactive; - if (hasInactive && !isLoading) { + if (shouldPoll && !isLoading) { if (!pollingRef.current) { pollingRef.current = setInterval(async () => { if (!organizationId) return; @@ -123,7 +133,7 @@ export function useGatewayList(): UseGatewayListReturn { } catch (err) { console.error('Polling failed:', err); } - }, 5000); + }, pollingIntervalMs); } } else if (pollingRef.current) { clearInterval(pollingRef.current); @@ -136,7 +146,7 @@ export function useGatewayList(): UseGatewayListReturn { pollingRef.current = null; } }; - }, [gateways, isLoading, organizationId]); + }, [gateways, isLoading, organizationId, pollAlways, pollingIntervalMs]); // Auto-select first gateway when gateways list changes and none is selected useEffect(() => { @@ -154,10 +164,26 @@ export function useGatewayList(): UseGatewayListReturn { setIsCreating(true); try { const response = await registerGateway(data, organizationId); + let initialToken = response.data.token ?? null; + + // The create-gateway response does not always include the one-time + // registration token, so generate it immediately for the first-view flow. + if (!initialToken) { + try { + initialToken = await rotateGatewayToken(response.data.id, organizationId); + } catch (tokenError) { + console.error('Failed to generate initial gateway registration token:', tokenError); + showSnackbar( + 'Gateway created, but the initial registration token could not be generated. Use Reconfigure on the gateway page.', + 'warning' + ); + } + } + const newGateway: HybridGateway = { ...response.data, status: response.data.isActive ? 'connected' : 'pending', - token: response.data.token, + token: initialToken, }; // Add to local state diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/applications/ApplicationsList.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/applications/ApplicationsList.tsx index 589c58567d..834dd42a2c 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/applications/ApplicationsList.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/applications/ApplicationsList.tsx @@ -461,8 +461,7 @@ export default function ApplicationsList() { ) : ( filteredApplications.map((app) => { - const descriptionText = - app.description?.trim() ?? 'No description'; + const descriptionText = app.description?.trim() || ''; const lastUpdated = app.lastUpdated ?? app.updatedAt ?? app.createdAt; diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/applications/GenAIApplicationsSummaryCardSection.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/applications/GenAIApplicationsSummaryCardSection.tsx index ace2e5e56f..32e5766e61 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/applications/GenAIApplicationsSummaryCardSection.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/applications/GenAIApplicationsSummaryCardSection.tsx @@ -272,10 +272,7 @@ export default function GenAIApplicationsSummaryCardSection({ fontSize="0.7rem" noWrap > - {truncateWords( - application.description || 'No description', - 12 - )} + {truncateWords(application.description?.trim() || '', 12)} diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsx index 42e4c5531f..a65793fff9 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsx @@ -62,7 +62,16 @@ import { mcpProxiesApis } from '../../../../apis/MCP/mcpProxiesApis'; import type { MCPServer } from '../../../../utils/types'; import NoMCPServers from '../../../../assets/images/NoMCPServers.svg'; -export default function ExternalServersList(): JSX.Element { +function getErrorDescription(error: unknown, fallbackMessage: string): string { + return ( + (error as any)?.response?.data?.description || + (error as any)?.response?.data?.message || + (error instanceof Error ? error.message : null) || + fallbackMessage + ); +} + +export default function ExternalServersList(): React.JSX.Element { const navigate = useNavigate(); const { projectSlug } = useParams<{ projectSlug: string }>(); const { @@ -162,8 +171,11 @@ export default function ExternalServersList(): JSX.Element { await mcpProxiesApis.deleteMCPServer(serverId, apimBaseUrl); setServers((prev) => prev.filter((s) => s.id !== serverId)); showSnackbar('MCP Proxy deleted successfully.', 'success'); - } catch { - showSnackbar('Failed to delete MCP Proxy.', 'error'); + } catch (error) { + showSnackbar( + getErrorDescription(error, 'Failed to delete MCP Proxy.'), + 'error' + ); } setDeleteTarget(null); }; diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/MCPProxiesSummaryCardSection.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/MCPProxiesSummaryCardSection.tsx index 156b4d8287..7cc162ad5a 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/MCPProxiesSummaryCardSection.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/MCPProxiesSummaryCardSection.tsx @@ -271,7 +271,7 @@ export default function MCPProxiesSummaryCardSection({ fontSize="0.7rem" noWrap > - {truncateWords(server.description || 'No description', 12)} + {truncateWords(server.description?.trim() || '', 12)} diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/EditGateway.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/EditGateway.tsx index 444925e90a..9f4f8f88e1 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/EditGateway.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/EditGateway.tsx @@ -126,7 +126,7 @@ export default function EditGateway() { ? [normalizedVhost, ...otherEndpoints] : undefined, functionalityType, - description: description || undefined, + description, }); showSnackbar('AI Gateway updated successfully', 'success'); diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx index 4636093baf..8891dc502d 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx @@ -78,7 +78,7 @@ export default function GatewaysList() { } | null>(null); const { gateways, isLoading, error, refetch, deleteGatewayById } = - useGatewayList(); + useGatewayList({ pollAlways: true }); // Prefetch environments so they're available when navigating to Add/Edit views useEnvironments(); diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/ViewGateway.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/ViewGateway.tsx index aba864b97b..72782c553e 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/ViewGateway.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/ViewGateway.tsx @@ -50,6 +50,7 @@ import { Download, Eye, EyeOff, + CheckCircle2, ChevronLeft, Clock, Edit, @@ -215,6 +216,7 @@ export default function ViewGateway() { const [activeColorScheme, setActiveColorScheme] = useState(() => getActiveColorScheme(), ); + const [connectionStatusDots, setConnectionStatusDots] = useState(""); const [showSetupBanner, setShowSetupBanner] = useState(true); const [isConfigsDrawerOpen, setIsConfigsDrawerOpen] = useState(false); const [isConfigsLoading, setIsConfigsLoading] = useState(false); @@ -335,6 +337,22 @@ ENVFILE`; return unsubscribe; }, []); + useEffect(() => { + if (gateway?.isActive) { + setConnectionStatusDots(""); + return; + } + + const frames = ["", ".", "..", "..."]; + let frameIndex = 0; + const intervalId = window.setInterval(() => { + frameIndex = (frameIndex + 1) % frames.length; + setConnectionStatusDots(frames[frameIndex]); + }, 450); + + return () => window.clearInterval(intervalId); + }, [gateway?.isActive]); + // Load gateway data useEffect(() => { const loadGateway = async () => { @@ -365,7 +383,6 @@ ENVFILE`; // Poll gateway status: 5s when inactive, 15s when active const pollingRef = useRef | null>(null); const pollingIntervalRef = useRef(null); - const hasAutoGeneratedTokenRef = useRef(false); useEffect(() => { if (!gateway?.id || !currentOrganization?.uuid) return; @@ -415,29 +432,6 @@ ENVFILE`; prevIsActiveRef.current = gateway?.isActive; }, [gateway?.isActive, showSnackbar]); - // Auto-generate a token when a non-active gateway has no token in memory - useEffect(() => { - if ( - !hasAutoGeneratedTokenRef.current && - !registrationToken && - gateway?.id && - !gateway.isActive && - currentOrganization?.uuid - ) { - hasAutoGeneratedTokenRef.current = true; - regenerateGatewayToken() - .then((newToken) => { - setRegistrationTokenState(newToken); - setRegistrationToken(newToken); - }) - .catch(() => { - hasAutoGeneratedTokenRef.current = false; - }); - } - // regenerateGatewayToken is stable within the same gateway/org context - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [gateway?.id, gateway?.isActive, currentOrganization?.uuid]); - const handleCopy = (text: string, label: string) => { navigator.clipboard.writeText(text); showSnackbar(`${label} copied to clipboard`, "success"); @@ -460,32 +454,6 @@ GATEWAY_REGISTRATION_TOKEN=${registrationToken || ""}`; showSnackbar("keys.env file downloaded", "success"); }; - const handleOpenGatewayConfigsDrawer = async () => { - if (!gateway?.id || !currentOrganization?.uuid) { - return; - } - - setIsConfigsDrawerOpen(true); - setIsConfigsLoading(true); - setGatewayConfigsError(null); - - try { - const response = await getGatewayConfigs( - gateway.id, - currentOrganization.uuid, - ); - setGatewayConfigs(response.data || {}); - } catch (err: any) { - console.error("Failed to load gateway configs:", err); - setGatewayConfigs(null); - setGatewayConfigsError( - err?.message || "Failed to load gateway configurations", - ); - } finally { - setIsConfigsLoading(false); - } - }; - const handleBack = () => { const listPath = buildOrgPath(currentOrganization, "/gateways"); navigate(listPath); @@ -521,6 +489,42 @@ GATEWAY_REGISTRATION_TOKEN=${registrationToken || ""}`; setIsRegenerateDialogOpen(true); }; + const renderGatewayConnectionStatus = () => { + if (!gateway) { + return null; + } + + if (gateway.isActive) { + return ( + + + + Gateway connected + + + ); + } + + return ( + + + + Waiting for gateway to connect{connectionStatusDots} + + + ); + }; + const handleConfirmRegenerateToken = async () => { try { setIsRegeneratingToken(true); @@ -597,7 +601,7 @@ GATEWAY_REGISTRATION_TOKEN=${registrationToken || ""}`; ? "Event Gateway" : "API Gateway"; - const descriptionText = gateway?.description?.trim() || "No description"; + const descriptionText = gateway?.description?.trim() || ""; const truncatedDescription = descriptionText.length > 200 ? `${descriptionText.slice(0, 200).trim()}…` @@ -737,24 +741,8 @@ GATEWAY_REGISTRATION_TOKEN=${registrationToken || ""}`; {/* Get Started Card */} - + Get Started - {gateway?.isActive && ( @@ -1004,10 +992,10 @@ GATEWAY_REGISTRATION_TOKEN=${registrationToken || ""}`; color="text.secondary" sx={{ mb: 2 }} > - The registration token is single-use. If you need to - reconfigure the gateway, generate a new token—this will - revoke the old token and disconnect the gateway from the - control plane. + Your existing local gateway can still use the previously + generated token. If you want to generate a new token and + new configuration command, click Reconfigure. This will + revoke the previous token. + + + + ); if (!isAdminOrgLevel) { return ( @@ -1051,7 +1204,11 @@ function ServiceProviderOverviewContent() { - + - + + {/* For gateway-created (read-only) providers the deployments remain + viewable (deploy/redeploy/restore/undeploy are disabled on the page + itself), so the button navigates but is relabelled "View Deployments". */} - + + + + + {providerDeleteAction} @@ -1487,6 +1647,7 @@ function ServiceProviderOverviewContent() { {projectSelectionDialog} + {deleteDialog} ); } diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProvidersSummaryCard.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProvidersSummaryCard.tsx index f2495f52ec..efb2d656f1 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProvidersSummaryCard.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProvidersSummaryCard.tsx @@ -324,8 +324,7 @@ export default function ServiceProvidersSummaryCard({ ); const templateLogo = templateLogoMap[templateKey]; const hasTemplateLogo = Boolean(templateLogo); - const descriptionText = - provider.description?.trim() ?? 'No description'; + const descriptionText = provider.description?.trim() || ''; return ( Date: Wed, 8 Jul 2026 00:16:51 +0530 Subject: [PATCH 2/2] Resolve Comments --- .../appShellPages/gateways/GatewaysList.tsx | 24 +++++++++---------- .../proxies/LLMProxyOverview.tsx | 11 +++++++-- .../ServiceProviderOverview.tsx | 22 ++++++++++++----- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx index 8891dc502d..81d0c878db 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/gateways/GatewaysList.tsx @@ -185,23 +185,23 @@ export default function GatewaysList() { sx={{ ml: 'auto', flexShrink: 0 }} > {isAdmin && filteredGateways.length > 0 ? ( - + // - + // ) : null} diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx index 2e2932d342..836f276102 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx @@ -247,11 +247,18 @@ function ProxyOverviewContent() { }; const handleDelete = async () => { - if (!proxy) return; + if (!proxy || isDeleting) return; try { setIsDeleting(true); await deleteProxyApi(); - await refreshProxies(); + try { + await refreshProxies(); + } catch (refreshError) { + console.error( + 'Failed to refresh proxies after deleting App LLM Proxy:', + refreshError + ); + } navigate(proxiesPath, { state: { proxyDeleted: true }, }); diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx index 77d661dc0c..2143197c02 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx @@ -291,6 +291,7 @@ function ServiceProviderOverviewContent() { name: string; } | null>(null); const [deleteConfirmationInput, setDeleteConfirmationInput] = useState(''); + const [isDeletingProvider, setIsDeletingProvider] = useState(false); const [checkingProviderId, setCheckingProviderId] = useState( null ); @@ -543,9 +544,10 @@ function ServiceProviderOverviewContent() { const [highlightApiKeySection, setHighlightApiKeySection] = useState(false); const handleDeleteConfirm = async () => { - if (!deleteTarget) return; + if (!deleteTarget || isDeletingProvider) return; try { + setIsDeletingProvider(true); await deleteProvider(); await refreshProviders(); showSnackbar('Provider deleted successfully.', 'success'); @@ -554,6 +556,8 @@ function ServiceProviderOverviewContent() { navigate(providersPath, { replace: true }); } catch { showSnackbar('Failed to delete provider. Please try again.', 'error'); + } finally { + setIsDeletingProvider(false); } }; @@ -1022,6 +1026,7 @@ function ServiceProviderOverviewContent() { { + if (isDeletingProvider) return; setDeleteTarget(null); setDeleteConfirmationInput(''); }} @@ -1049,6 +1054,7 @@ function ServiceProviderOverviewContent() {