diff --git a/web/src/components/cards/multi-tenancy/tenant-isolation-setup/__tests__/useTenantIsolationSetup.test.ts b/web/src/components/cards/multi-tenancy/tenant-isolation-setup/__tests__/useTenantIsolationSetup.test.ts index 70b4d6ae32..4b513117c0 100644 --- a/web/src/components/cards/multi-tenancy/tenant-isolation-setup/__tests__/useTenantIsolationSetup.test.ts +++ b/web/src/components/cards/multi-tenancy/tenant-isolation-setup/__tests__/useTenantIsolationSetup.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' +import { renderHook } from '@testing-library/react' import { useTenantIsolationSetup } from '../useTenantIsolationSetup' const mockUseOvnStatus = vi.fn() @@ -68,15 +69,15 @@ describe('useTenantIsolationSetup', () => { kubevirt: { detected: true, health: 'healthy' }, }) - const result = useTenantIsolationSetup() + const { result } = renderHook(() => useTenantIsolationSetup()) - expect(result.readyCount).toBe(4) - expect(result.totalComponents).toBe(4) - expect(result.allReady).toBe(true) - expect(result.isolationScore).toBe(3) - expect(result.isolationLevels.map((level) => level.status)).toEqual(['ready', 'ready', 'ready']) - expect(result.isLoading).toBe(false) - expect(result.isDemoData).toBe(false) + expect(result.current.readyCount).toBe(4) + expect(result.current.totalComponents).toBe(4) + expect(result.current.allReady).toBe(true) + expect(result.current.isolationScore).toBe(3) + expect(result.current.isolationLevels.map((level) => level.status)).toEqual(['ready', 'ready', 'ready']) + expect(result.current.isLoading).toBe(false) + expect(result.current.isDemoData).toBe(false) }) it('marks control plane as degraded when one component is unhealthy', () => { @@ -87,16 +88,16 @@ describe('useTenantIsolationSetup', () => { kubevirt: { detected: false, health: 'unknown' }, }) - const result = useTenantIsolationSetup() + const { result } = renderHook(() => useTenantIsolationSetup()) - expect(result.readyCount).toBe(3) - expect(result.allReady).toBe(false) - expect(result.isolationLevels).toEqual([ + expect(result.current.readyCount).toBe(3) + expect(result.current.allReady).toBe(false) + expect(result.current.isolationLevels).toEqual([ { type: 'Control-plane', status: 'degraded', provider: 'KubeFlex + K3s' }, { type: 'Data-plane', status: 'missing', provider: 'KubeVirt' }, { type: 'Network', status: 'ready', provider: 'OVN-Kubernetes' }, ]) - expect(result.isolationScore).toBe(1) + expect(result.current.isolationScore).toBe(1) }) it('combines loading and demo flags across all sources', () => { @@ -107,10 +108,10 @@ describe('useTenantIsolationSetup', () => { kubevirt: { detected: true, health: 'healthy', loading: false, isDemoData: false }, }) - const result = useTenantIsolationSetup() + const { result } = renderHook(() => useTenantIsolationSetup()) - expect(result.isLoading).toBe(true) - expect(result.isDemoData).toBe(false) - expect(result.totalIsolationLevels).toBe(3) + expect(result.current.isLoading).toBe(true) + expect(result.current.isDemoData).toBe(false) + expect(result.current.totalIsolationLevels).toBe(3) }) }) diff --git a/web/src/components/dashboard/CardFactoryModal.tsx b/web/src/components/dashboard/CardFactoryModal.tsx index 485c2478db..ccc3b5c149 100644 --- a/web/src/components/dashboard/CardFactoryModal.tsx +++ b/web/src/components/dashboard/CardFactoryModal.tsx @@ -5,6 +5,7 @@ import { CheckCircle } from 'lucide-react' import { BaseModal, ConfirmDialog } from '../../lib/modals' import { cn } from '../../lib/cn' +import { useToast } from '../ui/Toast' import { deleteDynamicCard, getAllDynamicCards } from '../../lib/dynamic-cards' import type { DynamicCardDefinition } from '../../lib/dynamic-cards/types' import { CardFactoryTemplates } from './CardFactoryTemplates' @@ -31,6 +32,7 @@ const SAVE_MESSAGE_TIMEOUT_MS = 3000 // Duration to display save/error messages export function CardFactoryModal({ isOpen, onClose, onCardCreated, embedded = false }: CardFactoryModalProps) { const { t } = useTranslation() + const { showToast } = useToast() const [tab, setTab] = useState('declarative') // Manage state @@ -61,6 +63,7 @@ export function CardFactoryModal({ isOpen, onClose, onCardCreated, embedded = fa const handleDelete = (id: string) => { deleteDynamicCard(id) setExistingCards(getAllDynamicCards()) + showToast(t('dashboard.cardFactory.deleteSuccess'), 'success') } // Handle save message with timeout diff --git a/web/src/components/settings/sections/LocalClustersSection.tsx b/web/src/components/settings/sections/LocalClustersSection.tsx index 1d6e0507b0..a7250f7e4e 100644 --- a/web/src/components/settings/sections/LocalClustersSection.tsx +++ b/web/src/components/settings/sections/LocalClustersSection.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { Container, RefreshCw, Plus, Trash2, Check, AlertCircle, Loader2, Plug, Unplug, Bot, ExternalLink, Monitor } from 'lucide-react' import { Button } from '../../ui/Button' +import { useToast } from '../../ui/Toast' import { useLocalClusterTools } from '../../../hooks/useLocalClusterTools' import { emitLocalClusterCreated } from '../../../lib/analytics' import { friendlyErrorMessage } from '../../../lib/clusterErrors' @@ -48,6 +49,7 @@ function getToolDescription(tool: string) { // ------------------------------------------------------------------ export function LocalClustersSection() { const { t } = useTranslation() + const { showToast } = useToast() const navigate = useNavigate() const { installedTools, @@ -123,9 +125,14 @@ export function LocalClustersSection() { const handleDelete = async (tool: string, name: string) => { try { - await deleteCluster(tool, name) + const success = await deleteCluster(tool, name) + if (success) { + showToast(t('settings.localClusters.deleteSuccess', { name }), 'success') + } else { + showToast(t('settings.localClusters.deleteError', { name }), 'error') + } } catch { - // deleteCluster handles errors internally; ignore unexpected throws + showToast(t('settings.localClusters.deleteError', { name }), 'error') } } diff --git a/web/src/locales/en/common.json b/web/src/locales/en/common.json index 4ec399f7aa..f378a701ae 100644 --- a/web/src/locales/en/common.json +++ b/web/src/locales/en/common.json @@ -969,7 +969,9 @@ "kubevirtDetectedCount_other": "KubeVirt detected on {{count}} clusters", "kubevirtNotDetected": "KubeVirt Not Detected", "kubevirtInstallHint": "KubeVirt lets you run virtual machines alongside containers on your Kubernetes clusters. Use the guided mission to install it.", - "kubevirtOpenMission": "Open Install KubeVirt Mission" + "kubevirtOpenMission": "Open Install KubeVirt Mission", + "deleteSuccess": "Cluster \"{{name}}\" deleted", + "deleteError": "Failed to delete cluster \"{{name}}\"" }, "backup": { "title": "Backup & Sync", @@ -1604,7 +1606,8 @@ "declarativeTableList": "Declarative (table/list)", "customCodeReact": "Custom Code (React)", "fieldsLabel": "Fields:", - "invalidJsonData": "Invalid JSON data." + "invalidJsonData": "Invalid JSON data.", + "deleteSuccess": "Card deleted" }, "statFactory": { "title": "Stat Block Factory", @@ -5515,7 +5518,9 @@ "selectCluster": "Select a cluster", "namespace": "Namespace", "selectNamespace": "Select a namespace", - "applyToAllClusters": "Apply to all clusters" + "applyToAllClusters": "Apply to all clusters", + "teamDeleted": "Team deleted", + "teamDeleteError": "Failed to delete team" }, "notFound": { "title": "Page not found", diff --git a/web/src/pages/TeamManagement.tsx b/web/src/pages/TeamManagement.tsx index 8a0ee5a0f4..8c9e7e15b0 100644 --- a/web/src/pages/TeamManagement.tsx +++ b/web/src/pages/TeamManagement.tsx @@ -1,10 +1,14 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { TeamList } from '../components/teams/TeamList' import { TeamDetail } from '../components/teams/TeamDetail' import { useTeams, useTeamDetail } from '../hooks/useTeams' +import { useToast } from '../components/ui/Toast' import type { TeamRole } from '../types/teams' export function TeamManagementPage() { + const { t } = useTranslation() + const { showToast } = useToast() const [selectedTeamId, setSelectedTeamId] = useState(null) const { teams, isLoading, createTeam, deleteTeam } = useTeams() const { team, addMember, removeMember } = useTeamDetail(selectedTeamId) @@ -15,7 +19,12 @@ export function TeamManagementPage() { const handleDelete = async () => { if (selectedTeamId) { - await deleteTeam(selectedTeamId) + const success = await deleteTeam(selectedTeamId) + if (success) { + showToast(t('teams.teamDeleted'), 'success') + } else { + showToast(t('teams.teamDeleteError'), 'error') + } setSelectedTeamId(null) } }