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 cad564379..6773fa80d 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersList.tsx
@@ -63,6 +63,7 @@ import { mcpProxiesApis } from '../../../../apis/MCP/mcpProxiesApis';
import type { MCPServer } from '../../../../utils/types';
import NoMCPServers from '../../../../assets/images/NoMCPServers.svg';
import { getErrorMessage } from '../../../../utils/apiError';
+import { GatewayArtifactDeleteWarning } from '../../../../utils/readOnlyArtifacts';
import { useAppAuth } from '../../../../contexts/AppAuthContext';
import { DISABLED_ACTION_SX, NO_PERMISSION_TOOLTIP, SCOPES } from '../../../../auth/permissions';
@@ -594,6 +595,12 @@ export default function ExternalServersList(): React.JSX.Element {
>
Delete external server
+ {deleteTarget?.readOnly ? (
+
+ ) : null}
Are you sure you want to delete {deleteTarget?.displayName}?
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx
index 6b47707c2..5d0f6d91e 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/externalServers/ExternalServersOverview.tsx
@@ -31,6 +31,11 @@ import {
Card,
Chip,
CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
Divider,
FormControl,
FormLabel,
@@ -55,6 +60,7 @@ import {
Edit,
Eye,
EyeOff,
+ Trash2,
} from '@wso2/oxygen-ui-icons-react';
import { FormattedMessage } from 'react-intl';
import { useAppShell } from '../../../../contexts/AppShellContext';
@@ -94,8 +100,16 @@ import type { EndpointValidationResponse } from './externalServersValidationType
import ExternalServerStepBanner from '../quickStart/ExternalServerStepBanner';
import type { ExternalServerStepBannerStepId } from '../quickStart/ExternalServerStepBanner';
import {
+ DisabledActionTooltip,
+ GatewayArtifactDeleteWarning,
GatewayArtifactReadOnlyBanner,
} from '../../../../utils/readOnlyArtifacts';
+import {
+ activeDeploymentDeleteBlockedReason,
+ countActiveDeployments,
+} from '../../../../utils/artifactDeletion';
+import { useAppAuth } from '../../../../contexts/AppAuthContext';
+import { NO_PERMISSION_TOOLTIP, SCOPES } from '../../../../auth/permissions';
function getInitials(name: string): string {
const words = name.trim().split(/\s+/);
@@ -226,6 +240,8 @@ export default function ExternalServersOverview(): JSX.Element {
const navigate = useNavigate();
const showSnackbar = useAIWorkspaceSnackbar();
+ const { hasPermission } = useAppAuth();
+ const canDeleteMcpProxy = hasPermission(SCOPES.MCP_PROXY_DELETE);
const [server, setServer] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [isSavingChanges, setIsSavingChanges] = useState(false);
@@ -236,6 +252,9 @@ export default function ExternalServersOverview(): JSX.Element {
const [selectedPolicies, setSelectedPolicies] = useState(
[]
);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [activeDeploymentCount, setActiveDeploymentCount] = useState(null);
const isReadOnlyServer = Boolean(server?.readOnly);
// Backend Connection tab
@@ -396,6 +415,61 @@ export default function ExternalServersOverview(): JSX.Element {
};
}, [organizationId, serverId, apimBaseUrl]);
+ useEffect(() => {
+ if (!organizationId || !serverId || !isReadOnlyServer || !canDeleteMcpProxy) {
+ setActiveDeploymentCount(null);
+ return;
+ }
+
+ let isCancelled = false;
+
+ const resolveActiveDeployments = async () => {
+ const deployments = await getMCPServerDeployments(serverId, apimBaseUrl);
+
+ if (isCancelled) return;
+
+ setActiveDeploymentCount(countActiveDeployments(deployments.list));
+ };
+
+ resolveActiveDeployments().catch((err) => {
+ logger.error(
+ 'Failed to resolve MCP Proxy deployments for delete guard:',
+ err
+ );
+ if (isCancelled) return;
+ setActiveDeploymentCount(null);
+ });
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [organizationId, serverId, isReadOnlyServer, canDeleteMcpProxy, apimBaseUrl]);
+
+ const deleteBlockedReason = useMemo(() => {
+ if (!isReadOnlyServer || activeDeploymentCount === null || activeDeploymentCount === 0) {
+ return null;
+ }
+ return activeDeploymentDeleteBlockedReason(
+ 'MCP Proxy',
+ activeDeploymentCount
+ );
+ }, [isReadOnlyServer, activeDeploymentCount]);
+
+ const handleDeleteConfirm = async () => {
+ if (!server || !serverId || isDeleting) return;
+ try {
+ setIsDeleting(true);
+ await mcpProxiesApis.deleteMCPServer(serverId, apimBaseUrl);
+ showSnackbar('MCP Proxy deleted successfully.', 'success');
+ setIsDeleteDialogOpen(false);
+ navigate(listPath, { replace: true });
+ } catch (err) {
+ showSnackbar(getErrorMessage(err, 'Failed to delete MCP Proxy.'), 'error');
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
const selectedGateway = useMemo(
() =>
deployedGateways.find((gateway) => gateway.id === selectedGatewayId) ??
@@ -919,7 +993,7 @@ export default function ExternalServersOverview(): JSX.Element {
{/* For gateway-created (read-only) proxies the deployments remain viewable
(deploy/redeploy/restore/undeploy are disabled on the page itself), so
@@ -1030,6 +1106,24 @@ export default function ExternalServersOverview(): JSX.Element {
/>
)}
+
+ setIsDeleteDialogOpen(true)}
+ aria-label={`Delete ${server.displayName}`}
+ data-cyid="delete-mcp-proxy-button"
+ >
+
+
+
@@ -1321,6 +1415,46 @@ export default function ExternalServersOverview(): JSX.Element {
+
+
);
}
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxiesList.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxiesList.tsx
index 3d89d73ab..a8cb6be7b 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxiesList.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxiesList.tsx
@@ -64,6 +64,7 @@ import NoProxies from '../../../../assets/images/NoProxies.svg';
import ErrorAlert from '../../../../Components/common/ErrorAlert';
import { useAIWorkspaceSnackbar } from '../../../../hooks/aiWorkspaceSnackbar';
import { getErrorMessage } from '../../../../utils/apiError';
+import { GatewayArtifactDeleteWarning } from '../../../../utils/readOnlyArtifacts';
import { useAppAuth } from '../../../../contexts/AppAuthContext';
import { NO_PERMISSION_TOOLTIP, SCOPES } from '../../../../auth/permissions';
@@ -118,6 +119,7 @@ export default function LLMProxiesList() {
const [deleteTarget, setDeleteTarget] = useState<{
id: string;
name: string;
+ readOnly: boolean;
} | null>(null);
useEffect(() => {
@@ -542,6 +544,7 @@ export default function LLMProxiesList() {
setDeleteTarget({
id: proxy.id,
name: proxy.displayName,
+ readOnly: Boolean(proxy.readOnly),
});
}}
aria-label={`Delete ${proxy.displayName}`}
@@ -569,6 +572,12 @@ export default function LLMProxiesList() {
>
Delete App LLM Proxy
+ {deleteTarget?.readOnly ? (
+
+ ) : null}
Are you sure you want to delete {deleteTarget?.name}?
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 a1d7fa5da..064e6ad99 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/proxies/LLMProxyOverview.tsx
@@ -77,8 +77,17 @@ import type {
} from '../../../../utils/types';
import { getErrorMessage } from '../../../../utils/apiError';
import {
+ activeDeploymentDeleteBlockedReason,
+ countActiveDeployments,
+} from '../../../../utils/artifactDeletion';
+import {
+ DisabledActionTooltip,
+ GatewayArtifactDeleteWarning,
GatewayArtifactReadOnlyBanner,
} from '../../../../utils/readOnlyArtifacts';
+import { getLLMProxyDeployments } from '../../../../apis/llmProxiesApis';
+import { PLATFORM_API_BASE_URL } from '../../../../paths';
+import { logger } from '../../../../utils/logger';
type TabPanelProps = {
value: number;
@@ -174,6 +183,53 @@ function ProxyOverviewContent() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const isReadOnlyProxy = Boolean(proxy?.readOnly);
+ const [activeDeploymentCount, setActiveDeploymentCount] = useState(null);
+
+ useEffect(() => {
+ const organizationId = currentOrganization?.uuid;
+ const proxyId = proxy?.id;
+ if (!organizationId || !proxyId || !isReadOnlyProxy || !canDeleteProxy) {
+ setActiveDeploymentCount(null);
+ return;
+ }
+
+ let isCancelled = false;
+
+ const resolveActiveDeployments = async () => {
+ const deployments = await getLLMProxyDeployments(
+ proxyId,
+ organizationId,
+ PLATFORM_API_BASE_URL
+ );
+
+ if (isCancelled) return;
+
+ setActiveDeploymentCount(countActiveDeployments(deployments.list));
+ };
+
+ resolveActiveDeployments().catch((err) => {
+ logger.error(
+ 'Failed to resolve App LLM Proxy deployments for delete guard:',
+ err
+ );
+ if (isCancelled) return;
+ setActiveDeploymentCount(null);
+ });
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [currentOrganization?.uuid, proxy?.id, isReadOnlyProxy, canDeleteProxy]);
+
+ const deleteBlockedReason = useMemo(() => {
+ if (!isReadOnlyProxy || activeDeploymentCount === null || activeDeploymentCount === 0) {
+ return null;
+ }
+ return activeDeploymentDeleteBlockedReason(
+ 'App LLM Proxy',
+ activeDeploymentCount
+ );
+ }, [isReadOnlyProxy, activeDeploymentCount]);
const getProviderId = (providerValue?: LLMProxy['provider']): string => {
if (!providerValue) return '';
@@ -475,20 +531,21 @@ function ProxyOverviewContent() {
>
{isReadOnlyProxy ? 'View Deployments' : 'Deploy to Gateway'}
-
-
- setDeleteDialogOpen(true)}
- aria-label="Delete proxy"
- >
-
-
-
-
+ setDeleteDialogOpen(true)}
+ aria-label="Delete proxy"
+ >
+
+
+
@@ -597,6 +654,12 @@ function ProxyOverviewContent() {
>
Delete App LLM Proxy
+ {isReadOnlyProxy ? (
+
+ ) : null}
Are you sure you want to delete {proxy.displayName}? This
action cannot be undone.
diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProvidersList.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProvidersList.tsx
index 672b98ca3..98e26a0f2 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProvidersList.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ProvidersList.tsx
@@ -65,6 +65,8 @@ import { useProviderTemplates } from '../../../../contexts/llmProvider/providerT
import useAIWorkspaceSnackbar from '../../../../hooks/aiWorkspaceSnackbar';
import * as llmProviderApis from '../../../../apis/llmProviderApis';
import { PLATFORM_API_BASE_URL } from '../../../../paths';
+import { getErrorMessage } from '../../../../utils/apiError';
+import { GatewayArtifactDeleteWarning } from '../../../../utils/readOnlyArtifacts';
import AnthropicLogo from '../../../../assets/brands/Anthropic.jpg';
import AWSBedrockLogo from '../../../../assets/brands/AWSBedrock.webp';
@@ -136,6 +138,7 @@ export default function ServiceProviders() {
const [deleteTarget, setDeleteTarget] = useState<{
id: string;
name: string;
+ readOnly: boolean;
} | null>(null);
const [deleteConfirmationInput, setDeleteConfirmationInput] = useState('');
const [assignTarget, setAssignTarget] = useState<{
@@ -182,14 +185,18 @@ export default function ServiceProviders() {
showSnackbar('Provider deleted successfully.', 'success');
setDeleteTarget(null);
setDeleteConfirmationInput('');
- } catch {
- showSnackbar('Failed to delete provider. Please try again.', 'error');
+ } catch (error) {
+ showSnackbar(
+ getErrorMessage(error, 'Failed to delete provider. Please try again.'),
+ 'error'
+ );
}
};
const checkProviderUsageAndConfirmDelete = async (
providerId: string,
- providerName: string
+ providerName: string,
+ isReadOnlyProvider: boolean
) => {
if (!currentOrganization?.uuid) {
showSnackbar(
@@ -217,7 +224,11 @@ export default function ServiceProviders() {
return;
}
- setDeleteTarget({ id: providerId, name: providerName });
+ setDeleteTarget({
+ id: providerId,
+ name: providerName,
+ readOnly: isReadOnlyProvider,
+ });
setDeleteConfirmationInput('');
} catch {
showSnackbar(
@@ -779,7 +790,8 @@ export default function ServiceProviders() {
event.stopPropagation();
void checkProviderUsageAndConfirmDelete(
providerId,
- provider.displayName
+ provider.displayName,
+ Boolean(provider.readOnly)
);
}}
aria-label={`Delete ${providerDisplayName}`}
@@ -811,6 +823,12 @@ export default function ServiceProviders() {
'{deleteTarget?.name ?? ''}'?
+ {deleteTarget?.readOnly ? (
+
+ ) : null}
This action will be irreversible and all related details will be
lost. Please type in the component name below to confirm.
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 1dbd39604..5847b9eb5 100644
--- a/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx
+++ b/portals/ai-workspace/src/pages/appShell/appShellPages/serviceProvider/ServiceProviderOverview.tsx
@@ -103,8 +103,15 @@ import {
} from '../../../../contexts/AIEntitiesContext';
import {
DisabledActionTooltip,
+ GatewayArtifactDeleteWarning,
GatewayArtifactReadOnlyBanner,
} from '../../../../utils/readOnlyArtifacts';
+import { getErrorMessage } from '../../../../utils/apiError';
+import {
+ activeDeploymentDeleteBlockedReason,
+ countActiveDeployments,
+ linkedProxiesDeleteBlockedReason,
+} from '../../../../utils/artifactDeletion';
import AnthropicLogo from '../../../../assets/brands/Anthropic.jpg';
import AWSBedrockLogo from '../../../../assets/brands/AWSBedrock.webp';
@@ -298,6 +305,8 @@ function ServiceProviderOverviewContent() {
const [checkingProviderId, setCheckingProviderId] = useState(
null
);
+ const [activeDeploymentCount, setActiveDeploymentCount] = useState(null);
+ const [linkedProxyCount, setLinkedProxyCount] = useState(null);
const showSnackbar = useAIWorkspaceSnackbar();
const hasUnsavedChanges = hasDraftChanges || isRateLimitingDirty;
const selectedGateway = useMemo(
@@ -490,6 +499,74 @@ function ServiceProviderOverviewContent() {
const proxyQuotaTooltip =
'You cannot create more App LLM Proxies because your organization has reached the maximum limit of 5 proxies.';
const isReadOnlyProvider = Boolean(provider?.readOnly);
+
+ useEffect(() => {
+ const organizationId = currentOrganization?.uuid;
+ const providerId = provider?.id;
+ if (!organizationId || !providerId || !canDelete) {
+ setActiveDeploymentCount(null);
+ setLinkedProxyCount(null);
+ return;
+ }
+
+ let isCancelled = false;
+
+ const resolveDeletePreconditions = async () => {
+ const [deploymentsResult, linkedProxiesResult] = await Promise.allSettled([
+ isReadOnlyProvider
+ ? getLLMProviderDeployments(providerId, organizationId, apimBaseUrl)
+ : Promise.resolve({ list: [], count: 0 }),
+ getLLMProviderProxies(providerId, organizationId, apimBaseUrl),
+ ]);
+
+ if (isCancelled) return;
+
+ setActiveDeploymentCount(
+ deploymentsResult.status === 'fulfilled'
+ ? countActiveDeployments(deploymentsResult.value.list)
+ : null
+ );
+ setLinkedProxyCount(
+ linkedProxiesResult.status === 'fulfilled'
+ ? (linkedProxiesResult.value.count ?? 0)
+ : null
+ );
+ };
+
+ resolveDeletePreconditions().catch((err) => {
+ logger.error(
+ 'Failed to resolve LLM Provider delete pre-conditions:',
+ err
+ );
+ if (isCancelled) return;
+ setActiveDeploymentCount(null);
+ setLinkedProxyCount(null);
+ });
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [
+ currentOrganization?.uuid,
+ provider?.id,
+ canDelete,
+ isReadOnlyProvider,
+ apimBaseUrl,
+ ]);
+
+ const deleteBlockedReason = useMemo(() => {
+ if (isReadOnlyProvider && activeDeploymentCount !== null && activeDeploymentCount > 0) {
+ return activeDeploymentDeleteBlockedReason(
+ 'LLM Provider',
+ activeDeploymentCount
+ );
+ }
+ if (linkedProxyCount !== null && linkedProxyCount > 0) {
+ return linkedProxiesDeleteBlockedReason(linkedProxyCount);
+ }
+ return null;
+ }, [isReadOnlyProvider, activeDeploymentCount, linkedProxyCount]);
+
const canCreateProxy = hasPermission(SCOPES.LLM_PROXY_CREATE);
const isCreateProxyDisabled =
!provider?.id || isProxyQuotaReached || !canCreateProxy;
@@ -559,8 +636,11 @@ function ServiceProviderOverviewContent() {
setDeleteTarget(null);
setDeleteConfirmationInput('');
navigate(providersPath, { replace: true });
- } catch {
- showSnackbar('Failed to delete provider. Please try again.', 'error');
+ } catch (error) {
+ showSnackbar(
+ getErrorMessage(error, 'Failed to delete provider. Please try again.'),
+ 'error'
+ );
} finally {
setIsDeletingProvider(false);
}
@@ -895,21 +975,28 @@ function ServiceProviderOverviewContent() {
);
const providerDeleteAction = isAdminOrgLevel && canDelete ? (
- {
- void checkProviderUsageAndConfirmDelete(
- providerKey,
- provider.displayName
- );
- }}
- aria-label={`Delete ${providerDisplayName}`}
- data-cyid="delete-provider-button"
+
-
-
+ {
+ void checkProviderUsageAndConfirmDelete(
+ providerKey,
+ provider.displayName
+ );
+ }}
+ aria-label={`Delete ${providerDisplayName}`}
+ data-cyid="delete-provider-button"
+ >
+
+
+
) : null;
const renderResourcesSpecViewer = () => {
@@ -1048,6 +1135,12 @@ function ServiceProviderOverviewContent() {
'{deleteTarget?.name ?? ''}'?
+ {isReadOnlyProvider ? (
+
+ ) : null}
This action will be irreversible and all related details will be
lost. Please type in the component name below to confirm.
diff --git a/portals/ai-workspace/src/utils/artifactDeletion.ts b/portals/ai-workspace/src/utils/artifactDeletion.ts
new file mode 100644
index 000000000..d9943cca3
--- /dev/null
+++ b/portals/ai-workspace/src/utils/artifactDeletion.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
+ *
+ * WSO2 LLC. licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { DeploymentResponse, DeploymentStatus } from './types';
+
+/**
+ * Deployment statuses the Platform API counts as "active" when deciding whether
+ * a gateway-created artifact may be deleted.
+ */
+export const ACTIVE_DEPLOYMENT_STATUSES: readonly DeploymentStatus[] = [
+ 'DEPLOYED',
+ 'DEPLOYING',
+ 'UNDEPLOYING',
+];
+
+/** True when any deployment in `deployments` is in a delete-blocking state. */
+export function hasActiveDeployment(
+ deployments: DeploymentResponse[] | undefined | null
+): boolean {
+ return (deployments ?? []).some((deployment) =>
+ (ACTIVE_DEPLOYMENT_STATUSES as readonly string[]).includes(
+ String(deployment?.status ?? '').toUpperCase()
+ )
+ );
+}
+
+/** Count of deployments in a delete-blocking state, for tooltip copy. */
+export function countActiveDeployments(
+ deployments: DeploymentResponse[] | undefined | null
+): number {
+ return (deployments ?? []).filter((deployment) =>
+ (ACTIVE_DEPLOYMENT_STATUSES as readonly string[]).includes(
+ String(deployment?.status ?? '').toUpperCase()
+ )
+ ).length;
+}
+
+/**
+ * Tooltip reason for a gateway-created artifact that still has active
+ * deployments. `artifactType` is the user-facing kind ("LLM Provider",
+ * "App LLM Proxy", "MCP Proxy").
+ */
+export function activeDeploymentDeleteBlockedReason(
+ artifactType: string,
+ activeCount: number
+): string {
+ const gatewayLabel = activeCount === 1 ? 'gateway' : 'gateways';
+ const countLabel = activeCount > 0 ? `${activeCount} ${gatewayLabel}` : 'a gateway';
+ return (
+ `This ${artifactType} was created from a gateway and is still deployed on ${countLabel}. ` +
+ `Undeploy it from the ${gatewayLabel} first, then delete it here.`
+ );
+}
+
+/**
+ * Tooltip reason for an LLM Provider that still has App LLM Proxies built on
+ * it. Deleting the provider would orphan them, so they have to go first.
+ */
+export function linkedProxiesDeleteBlockedReason(linkedProxyCount: number): string {
+ const proxyLabel = linkedProxyCount === 1 ? 'App LLM Proxy' : 'App LLM Proxies';
+ const usageVerb = linkedProxyCount === 1 ? 'is' : 'are';
+ return (
+ `${linkedProxyCount} ${proxyLabel} ${usageVerb} using this LLM Provider. ` +
+ `Delete or repoint ${linkedProxyCount === 1 ? 'it' : 'them'} before deleting the provider.`
+ );
+}
diff --git a/portals/ai-workspace/src/utils/readOnlyArtifacts.tsx b/portals/ai-workspace/src/utils/readOnlyArtifacts.tsx
index b6fc4782b..04ced1713 100644
--- a/portals/ai-workspace/src/utils/readOnlyArtifacts.tsx
+++ b/portals/ai-workspace/src/utils/readOnlyArtifacts.tsx
@@ -17,7 +17,7 @@
*/
import type { ReactNode } from 'react';
-import { Box, Card, Stack, Tooltip, Typography } from '@wso2/oxygen-ui';
+import { Alert, Box, Card, Stack, Tooltip, Typography } from '@wso2/oxygen-ui';
import { Lock } from '@wso2/oxygen-ui-icons-react';
export const GATEWAY_MANAGED_ARTIFACT_TOOLTIP =
@@ -50,6 +50,29 @@ export function GatewayArtifactReadOnlyBanner({
);
}
+/**
+ * Warning shown inside the delete confirmation dialog of a gateway-created
+ * (data-plane-originated) artifact.
+ * `artifactType` is the user-facing kind ("LLM Provider", "App LLM Proxy",
+ * "MCP Proxy"); `artifactName` is the artifact's display name.
+ */
+export function GatewayArtifactDeleteWarning({
+ artifactType,
+ artifactName,
+}: {
+ artifactType: string;
+ artifactName?: string;
+}) {
+ return (
+
+ This {artifactType} was created from a gateway. Make sure you have
+ undeployed{' '}
+ {artifactName ? {artifactName} : `this ${artifactType}`}{' '}
+ from the gateways first.
+
+ );
+}
+
type DisabledActionTooltipProps = {
children: ReactNode;
disabled: boolean;