diff --git a/console/apps/webapp/src/Providers/GlobalProviders/GlobalProviders.tsx b/console/apps/webapp/src/Providers/GlobalProviders/GlobalProviders.tsx index 97ccd3e9..a3691286 100644 --- a/console/apps/webapp/src/Providers/GlobalProviders/GlobalProviders.tsx +++ b/console/apps/webapp/src/Providers/GlobalProviders/GlobalProviders.tsx @@ -18,8 +18,8 @@ import { AuthProvider } from "@agent-management-platform/auth"; import { ClientProvider } from "@agent-management-platform/api-client"; +import { NotificationProvider, ConfirmationDialogProvider } from "@agent-management-platform/shared-component"; import { OxygenUIThemeProvider } from "@wso2/oxygen-ui"; -import { ConfirmationDialogProvider } from "@agent-management-platform/shared-component"; export const GlobalProviders = ({ children, @@ -28,11 +28,13 @@ export const GlobalProviders = ({ }) => { return ( - - - {children} - - + + + + {children} + + + ); }; diff --git a/console/common/config/rush/pnpm-lock.yaml b/console/common/config/rush/pnpm-lock.yaml index 03713b39..4593afd8 100644 --- a/console/common/config/rush/pnpm-lock.yaml +++ b/console/common/config/rush/pnpm-lock.yaml @@ -718,6 +718,9 @@ importers: '@agent-management-platform/api-client': specifier: workspace:* version: link:../../libs/api-client + '@agent-management-platform/shared-component': + specifier: workspace:* + version: link:../../libs/shared-component '@agent-management-platform/types': specifier: workspace:* version: link:../../libs/types diff --git a/console/workspaces/libs/shared-component/src/components/BuildPanel.tsx b/console/workspaces/libs/shared-component/src/components/BuildPanel.tsx index 944eea0a..5f8e8d76 100644 --- a/console/workspaces/libs/shared-component/src/components/BuildPanel.tsx +++ b/console/workspaces/libs/shared-component/src/components/BuildPanel.tsx @@ -21,6 +21,7 @@ import { Wrench } from "@wso2/oxygen-ui-icons-react"; import { Box, Button, Typography } from "@wso2/oxygen-ui"; import { FormProvider, useForm } from "react-hook-form"; import { TextInput, DrawerHeader, DrawerContent } from "@agent-management-platform/views"; +import { useNotification } from "../providers"; interface BuildPanelProps { onClose: () => void; @@ -41,41 +42,41 @@ export function BuildPanel({ agentName, }: BuildPanelProps) { const { mutate: buildAgent, isPending } = useBuildAgent(); + const { notify } = useNotification(); const { data: agent, isLoading: isLoadingAgent } = useGetAgent({ orgName, projName, agentName, - }); const methods = useForm({ + }); + + const methods = useForm({ defaultValues: { branch: "main", commitId: "", }, }); - const handleBuild = async () => { - try { - const formData = methods.getValues(); - buildAgent({ - params: { - orgName, - projName, - agentName, - }, - query: { - commitId: formData.commitId || "", - }, - }, { - onSuccess: () => { - onClose(); - }, - onError: (error) => { - console.error("Build trigger failed:", error); - }, - }); - } - catch (error) { - console.error("Build trigger failed:", error); - } + const handleBuild = () => { + const formData = methods.getValues(); + buildAgent({ + params: { + orgName, + projName, + agentName, + }, + query: { + commitId: formData.commitId || "", + }, + }, { + onSuccess: () => { + notify('success', 'Build triggered successfully'); + onClose(); + }, + onError: (error) => { + const message = error instanceof Error ? error.message : 'Build trigger failed'; + notify('error', message); + }, + }); }; return ( diff --git a/console/workspaces/libs/shared-component/src/components/ConfirmDialog.tsx b/console/workspaces/libs/shared-component/src/components/ConfirmDialog.tsx new file mode 100644 index 00000000..69b5b40d --- /dev/null +++ b/console/workspaces/libs/shared-component/src/components/ConfirmDialog.tsx @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025, 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 { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, + CircularProgress, +} from '@wso2/oxygen-ui'; + +export interface ConfirmDialogProps { + open: boolean; + title: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + confirmColor?: 'error' | 'primary' | 'secondary'; + onConfirm: () => void; + onCancel: () => void; + isLoading?: boolean; +} + +/** + * Reusable confirmation dialog for destructive or important actions. + * Use for delete confirmations, irreversible operations, etc. + */ +export function ConfirmDialog({ + open, + title, + message, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + confirmColor = 'error', + onConfirm, + onCancel, + isLoading = false, +}: ConfirmDialogProps) { + return ( + + {title} + + + {message} + + + + + + + + ); +} diff --git a/console/workspaces/libs/shared-component/src/components/DeploymentConfig.tsx b/console/workspaces/libs/shared-component/src/components/DeploymentConfig.tsx index 7c3567fd..33124bee 100644 --- a/console/workspaces/libs/shared-component/src/components/DeploymentConfig.tsx +++ b/console/workspaces/libs/shared-component/src/components/DeploymentConfig.tsx @@ -24,6 +24,7 @@ import { EnvironmentVariable } from "./EnvironmentVariable"; import type { Environment, EnvironmentVariable as EnvVar } from "@agent-management-platform/types"; import { useEffect } from "react"; import { TextInput, DrawerHeader, DrawerContent } from "@agent-management-platform/views"; +import { useNotification } from "../providers"; interface DeploymentConfigProps { onClose: () => void; @@ -49,6 +50,7 @@ export function DeploymentConfig({ imageId, }: DeploymentConfigProps) { const { mutate: deployAgent, isPending } = useDeployAgent(); + const { notify } = useNotification(); const { data: agent, isLoading: isLoadingAgent } = useGetAgent({ orgName, projName, @@ -77,34 +79,36 @@ export function DeploymentConfig({ }); }, [configurations, methods]); - const handleDeploy = async () => { - try { - const formData = methods.getValues(); + const handleDeploy = () => { + const formData = methods.getValues(); - const envVariables: EnvVar[] = formData.env - .filter((envVar: { key: string; value: string }) => envVar.key && envVar.value) - .map((envVar: { key: string; value: string }) => ({ - key: envVar.key, - value: envVar.value, - })); - deployAgent({ - params: { - orgName, - projName, - agentName, - }, - body: { - imageId: imageId, - env: envVariables.length > 0 ? envVariables : undefined, - }, - }, { - onSuccess: () => { - onClose(); - }, - }); - } catch { - // Error handling is done by the mutation - } + const envVariables: EnvVar[] = formData.env + .filter((envVar: { key: string; value: string }) => envVar.key && envVar.value) + .map((envVar: { key: string; value: string }) => ({ + key: envVar.key, + value: envVar.value, + })); + + deployAgent({ + params: { + orgName, + projName, + agentName, + }, + body: { + imageId: imageId, + env: envVariables.length > 0 ? envVariables : undefined, + }, + }, { + onSuccess: () => { + notify('success', 'Deployment started successfully'); + onClose(); + }, + onError: (error) => { + const message = error instanceof Error ? error.message : 'Deployment failed'; + notify('error', message); + }, + }); }; diff --git a/console/workspaces/libs/shared-component/src/components/index.ts b/console/workspaces/libs/shared-component/src/components/index.ts index efe16980..a1812ef6 100644 --- a/console/workspaces/libs/shared-component/src/components/index.ts +++ b/console/workspaces/libs/shared-component/src/components/index.ts @@ -18,6 +18,7 @@ export * from './BuildLogs'; export * from './BuildPanel'; +export * from './ConfirmDialog'; export * from './BuildSteps'; export * from './CodeBlock'; export * from './DeploymentConfig'; diff --git a/console/workspaces/libs/shared-component/src/index.ts b/console/workspaces/libs/shared-component/src/index.ts index 700fb529..efa59110 100644 --- a/console/workspaces/libs/shared-component/src/index.ts +++ b/console/workspaces/libs/shared-component/src/index.ts @@ -17,3 +17,4 @@ */ export * from './components'; +export * from './providers'; diff --git a/console/workspaces/libs/shared-component/src/providers/NotificationContext.ts b/console/workspaces/libs/shared-component/src/providers/NotificationContext.ts new file mode 100644 index 00000000..6c30dd27 --- /dev/null +++ b/console/workspaces/libs/shared-component/src/providers/NotificationContext.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2025, 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 { createContext } from 'react'; + +// Notification severity types matching MUI Alert +export type NotificationType = 'success' | 'error' | 'warning' | 'info'; + +// Shape of the context value +export interface NotificationContextType { + notify: (type: NotificationType, message: string) => void; +} + +// Context with null default - useNotification hook will throw if used outside provider +export const NotificationContext = createContext(null); diff --git a/console/workspaces/libs/shared-component/src/providers/NotificationProvider.tsx b/console/workspaces/libs/shared-component/src/providers/NotificationProvider.tsx new file mode 100644 index 00000000..50dbf718 --- /dev/null +++ b/console/workspaces/libs/shared-component/src/providers/NotificationProvider.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2025, 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 React, { useCallback, useMemo, useState } from 'react'; +import { Alert, Snackbar } from '@wso2/oxygen-ui'; +import { NotificationContext, NotificationType } from './NotificationContext'; + +interface Notification { + id: string; + type: NotificationType; + message: string; +} + +interface NotificationProviderProps { + children: React.ReactNode; +} + +const AUTO_HIDE_DURATION = 5000; + +export function NotificationProvider({ children }: NotificationProviderProps) { + const [notifications, setNotifications] = useState([]); + + // Add a new notification + const notify = useCallback((type: NotificationType, message: string) => { + const id = Date.now().toString(); + setNotifications((prev) => [...prev, { id, type, message }]); + }, []); + + // Remove a notification by id + const removeNotification = useCallback((id: string) => { + setNotifications((prev) => prev.filter((n) => n.id !== id)); + }, []); + + // Memoize context value to prevent unnecessary re-renders + const contextValue = useMemo(() => ({ notify }), [notify]); + + return ( + + {children} + {/* Render notifications - show only the first one, queue the rest */} + {notifications.length > 0 && ( + removeNotification(notifications[0].id)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + > + removeNotification(notifications[0].id)} + sx={{ width: '100%' }} + > + {notifications[0].message} + + + )} + + ); +} diff --git a/console/workspaces/libs/shared-component/src/providers/index.ts b/console/workspaces/libs/shared-component/src/providers/index.ts new file mode 100644 index 00000000..6d78f3af --- /dev/null +++ b/console/workspaces/libs/shared-component/src/providers/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2025, 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. + */ + +export * from './NotificationContext'; +export * from './NotificationProvider'; +export * from './useNotification'; diff --git a/console/workspaces/libs/shared-component/src/providers/useNotification.ts b/console/workspaces/libs/shared-component/src/providers/useNotification.ts new file mode 100644 index 00000000..04838beb --- /dev/null +++ b/console/workspaces/libs/shared-component/src/providers/useNotification.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2025, 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 { useContext } from 'react'; +import { NotificationContext } from './NotificationContext'; + +/** + * Hook to access notification functions. + * Must be used within a NotificationProvider. + */ +export function useNotification() { + const context = useContext(NotificationContext); + + if (!context) { + throw new Error('useNotification must be used within a NotificationProvider'); + } + + return context; +} diff --git a/console/workspaces/pages/add-new-agent/package.json b/console/workspaces/pages/add-new-agent/package.json index 8391b176..f13a0f49 100644 --- a/console/workspaces/pages/add-new-agent/package.json +++ b/console/workspaces/pages/add-new-agent/package.json @@ -48,6 +48,7 @@ "@wso2/oxygen-ui-icons-react": "0.0.1-alpha.9", "@agent-management-platform/views": "workspace:*", "@agent-management-platform/api-client": "workspace:*", + "@agent-management-platform/shared-component": "workspace:*", "@agent-management-platform/types": "workspace:*", "dayjs": "1.11.18", "yup": "1.4.0", diff --git a/console/workspaces/pages/add-new-agent/src/AddNewAgent.tsx b/console/workspaces/pages/add-new-agent/src/AddNewAgent.tsx index b99895e6..287f67b0 100644 --- a/console/workspaces/pages/add-new-agent/src/AddNewAgent.tsx +++ b/console/workspaces/pages/add-new-agent/src/AddNewAgent.tsx @@ -25,6 +25,7 @@ import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import { addAgentSchema, type AddAgentFormValues } from './form/schema'; import { useCreateAgent, useListAgents } from '@agent-management-platform/api-client'; +import { useNotification } from '@agent-management-platform/shared-component'; import { AgentFlowRouter } from './components/AgentFlowRouter'; import { CreateButtons } from './components/CreateButtons'; import { useAgentFlow } from './hooks/useAgentFlow'; @@ -32,6 +33,7 @@ import { buildAgentCreationPayload } from './utils/buildAgentPayload'; export const AddNewAgent: React.FC = () => { const navigate = useNavigate(); + const { notify } = useNotification(); const { orgId, projectId } = useParams<{ orgId: string; projectId?: string }>(); const methods = useForm({ resolver: yupResolver(addAgentSchema), @@ -80,6 +82,7 @@ export const AddNewAgent: React.FC = () => { createAgent(payload, { onSuccess: () => { + notify('success', 'Agent created successfully'); navigate(generatePath( absoluteRouteMap.children.org.children.projects.children.agents.path, { @@ -91,12 +94,11 @@ export const AddNewAgent: React.FC = () => { ); }, onError: (e: unknown) => { - // TODO: Show error toast/notification to user - // eslint-disable-next-line no-console - console.error('Failed to create agent:', e); + const message = e instanceof Error ? e.message : 'Failed to create agent'; + notify('error', message); } }); - }, [createAgent, navigate, params]); + }, [createAgent, navigate, notify, params]); const handleAddAgent = useMemo(() => methods.handleSubmit(onSubmit), [methods, onSubmit]); diff --git a/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx b/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx index fdb151be..f7500368 100644 --- a/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx +++ b/console/workspaces/pages/overview/src/AgentsList/AgentsList.tsx @@ -61,10 +61,10 @@ import { useDeleteAgent, useGetProject, } from "@agent-management-platform/api-client"; +import { useConfirmationDialog, useNotification } from "@agent-management-platform/shared-component"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { AgentTypeSummery } from "./subComponents/AgentTypeSummery"; -import { useConfirmationDialog } from "@agent-management-platform/shared-component"; dayjs.extend(relativeTime); @@ -123,20 +123,42 @@ export const AgentsList: React.FC = () => { projName: projectId, }); const { mutate: deleteAgent, isPending: isDeletingAgent } = useDeleteAgent(); + const { notify } = useNotification(); const { data: project, isLoading: isProjectLoading } = useGetProject({ orgName: orgId, projName: projectId, }); const { addConfirmation } = useConfirmationDialog(); + const handleDeleteAgent = useCallback( - (agentId: string) => { - deleteAgent({ - orgName: orgId, - projName: projectId, - agentName: agentId, + (agentId: string, agentName: string) => { + addConfirmation({ + title: "Delete Agent?", + description: `Are you sure you want to delete the agent "${agentName}"? This action cannot be undone.`, + onConfirm: () => { + deleteAgent( + { + orgName: orgId, + projName: projectId, + agentName: agentId, + }, + { + onSuccess: () => { + notify("success", `Agent "${agentName}" deleted successfully`); + }, + onError: (err) => { + const message = err instanceof Error ? err.message : "Failed to delete agent"; + notify("error", message); + }, + } + ); + }, + confirmButtonColor: "error", + confirmButtonIcon: , + confirmButtonText: "Delete", }); }, - [deleteAgent, orgId, projectId] + [addConfirmation, deleteAgent, orgId, projectId, notify] ); const handleRowMouseEnter = useCallback( @@ -300,16 +322,7 @@ export const AgentsList: React.FC = () => { size="small" onClick={(e) => { e.stopPropagation(); // Prevent row click if any - addConfirmation({ - title: "Delete Agent?", - description: `Are you sure you want to delete the agent "${row.displayName}"? This action cannot be undone.`, - onConfirm: () => { - handleDeleteAgent(row.name); - }, - confirmButtonColor: "error", - confirmButtonIcon: , - confirmButtonText: "Delete", - }); + handleDeleteAgent(row.name, row.displayName); }} > Delete @@ -333,7 +346,6 @@ export const AgentsList: React.FC = () => { theme.palette.primary.main, hoveredAgentId, isTouchDevice, - addConfirmation, handleDeleteAgent, ] );