Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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)
})
})
3 changes: 3 additions & 0 deletions web/src/components/dashboard/CardFactoryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<Tab>('declarative')

// Manage state
Expand Down Expand Up @@ -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')
}
Comment on lines 63 to 67

// Handle save message with timeout
Expand Down
11 changes: 9 additions & 2 deletions web/src/components/settings/sections/LocalClustersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -48,6 +49,7 @@ function getToolDescription(tool: string) {
// ------------------------------------------------------------------
export function LocalClustersSection() {
const { t } = useTranslation()
const { showToast } = useToast()
const navigate = useNavigate()
const {
installedTools,
Expand Down Expand Up @@ -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')
}
}

Expand Down
11 changes: 8 additions & 3 deletions web/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Comment on lines +1609 to +1610
},
"statFactory": {
"title": "Stat Block Factory",
Expand Down Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion web/src/pages/TeamManagement.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null)
const { teams, isLoading, createTeam, deleteTeam } = useTeams()
const { team, addMember, removeMember } = useTeamDetail(selectedTeamId)
Expand All @@ -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)
}
}
Expand Down
Loading