/** * SealedSecret Detail View * * Shows detailed information about a specific SealedSecret including * encrypted data, template, resulting Secret, and actions */ import { Icon } from '@iconify/react'; import { K8s } from '@kinvolk/headlamp-plugin/lib'; import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { NameValueTable, SectionBox, SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Alert, Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Drawer, IconButton, Typography, } from '@mui/material'; import { useSnackbar } from 'notistack'; import React from 'react'; import { useParams } from 'react-router-dom'; import { usePermissions } from '../hooks/usePermissions'; import { getPluginConfig, rotateSealedSecret } from '../lib/controller'; import { canDecryptSecrets } from '../lib/rbac'; import { SealedSecret } from '../lib/SealedSecretCRD'; import { SealedSecretScope } from '../types'; import { DecryptDialog } from './DecryptDialog'; import { SealedSecretDetailSkeleton } from './LoadingSkeletons'; /** * Format scope for display */ function formatScope(scope: SealedSecretScope): string { switch (scope) { case 'strict': return 'Strict'; case 'namespace-wide': return 'Namespace-wide'; case 'cluster-wide': return 'Cluster-wide'; default: return scope; } } /** * SealedSecret detail view component */ export function SealedSecretDetail() { const { namespace = '', name = '' } = useParams<{ namespace: string; name: string }>(); const [sealedSecret, error] = SealedSecret.useGet(name || undefined, namespace || undefined); const [secret] = K8s.ResourceClasses.Secret.useGet(name || undefined, namespace || undefined); const [decryptKey, setDecryptKey] = React.useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); const [rotating, setRotating] = React.useState(false); const [canDecrypt, setCanDecrypt] = React.useState(false); const { enqueueSnackbar } = useSnackbar(); const { permissions } = usePermissions(namespace || undefined); // Check if user can decrypt secrets (requires get permission on Secrets) React.useEffect(() => { let cancelled = false; if (namespace) { canDecryptSecrets(namespace).then((result) => { if (!cancelled) setCanDecrypt(result); }); } return () => { cancelled = true; }; }, [namespace]); // Wait for required params before rendering if (!namespace || !name) { return ; } // Show error if fetch failed if (error) { return ( Failed to load SealedSecret {String(error)} ); } // Show loading skeleton while data is being fetched if (!sealedSecret) { return ; } // Memoize callbacks to prevent re-renders const handleDelete = React.useCallback(async () => { try { await sealedSecret.delete(); enqueueSnackbar('SealedSecret deleted successfully', { variant: 'success' }); window.history.back(); } catch (error: any) { enqueueSnackbar(`Failed to delete SealedSecret: ${error.message}`, { variant: 'error' }); } setDeleteDialogOpen(false); }, [sealedSecret, enqueueSnackbar]); const handleRotate = React.useCallback(async () => { setRotating(true); try { const config = getPluginConfig(); const yaml = JSON.stringify(sealedSecret.jsonData); await rotateSealedSecret(config, yaml); enqueueSnackbar('SealedSecret re-encrypted successfully', { variant: 'success' }); // The resource will auto-refresh via the watch } catch (error: any) { enqueueSnackbar(`Failed to re-encrypt: ${error.message}`, { variant: 'error' }); } finally { setRotating(false); } }, [sealedSecret, enqueueSnackbar]); // Safety check - should never happen due to early returns above, but be defensive if (!sealedSecret?.spec?.encryptedData) { return ; } const encryptedKeys = Object.keys(sealedSecret.spec.encryptedData); const handleClose = () => { window.history.back(); }; return ( <> {sealedSecret.metadata.name} {permissions?.canUpdate && ( )} {permissions?.canDelete && ( )} } > {sealedSecret.isSynced ? 'Synced' : 'Not Synced'} ), }, { name: 'Status Message', value: String(sealedSecret.syncMessage || 'Unknown'), hide: !sealedSecret.syncCondition, }, { name: 'Age', value: String(sealedSecret.getAge() || ''), }, { name: 'Created', value: sealedSecret.metadata.creationTimestamp ? new Date(sealedSecret.metadata.creationTimestamp).toLocaleString() : 'Unknown', }, ]} /> ({ key, value: sealedSecret.spec.encryptedData[key], }))} columns={[ { label: 'Key', getter: (row: { key: string; value: string }) => row.key, }, { label: 'Encrypted Value', getter: (row: { key: string; value: string }) => { const val = row.value; return val.length > 40 ? val.substring(0, 40) + '...' : val; }, }, { label: 'Actions', getter: (row: { key: string; value: string }) => canDecrypt ? ( ) : ( ), }, ]} /> {sealedSecret.spec.template && ( )} {secret ? ( Secret exists, }, { name: 'Keys', value: String(Object.keys(secret.data || {}).join(', ') || 'None'), }, { name: 'Link', value: ( View Secret ), }, ]} /> ) : ( Secret not yet created

The controller will create the Secret once it processes this SealedSecret.

)}
{decryptKey && ( setDecryptKey(null)} /> )} setDeleteDialogOpen(false)} aria-labelledby="delete-dialog-title"> Delete SealedSecret? Are you sure you want to delete the SealedSecret {name}? This will also delete the resulting Kubernetes Secret.
); }