Compare commits

...

15 Commits

Author SHA1 Message Date
github-actions[bot] 248ffa4962 chore: release v0.2.16 2026-02-13 17:54:57 +00:00
Chris Farhood 0b082984a7 chore: bump version to 0.2.16 2026-02-13 12:53:36 -05:00
Chris Farhood 4da3513015 feat: add displayName to package.json for proper UI display
Set displayName to 'Sealed Secrets' so the plugin settings list shows
the friendly name instead of the package name 'headlamp-sealed-secrets'.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-13 12:53:28 -05:00
github-actions[bot] 74af59ef50 chore: release v0.2.15 2026-02-13 15:13:39 +00:00
Chris Farhood 67287158fd chore: bump version to 0.2.15 2026-02-13 10:12:38 -05:00
Chris Farhood dbc1fb199b fix: correct settings JSX structure, update display name, improve params handling
- Fix extra closing Box tag in SettingsPage causing blank display
- Change display name from 'Sealed Secrets Plugin for Headlamp' to 'Sealed Secrets'
- Use default values for params to avoid undefined in hooks (fixes retry button issue)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-13 10:12:38 -05:00
github-actions[bot] c63afb1461 chore: release v0.2.14 2026-02-13 12:39:41 +00:00
Chris Farhood 3429b32625 chore: bump version to 0.2.14 2026-02-13 07:38:45 -05:00
Chris Farhood 5cf360b591 fix: enable drawer scrolling, fix blank settings page, and eliminate retry button requirement
- Add overflow: auto to drawer Box wrapper for vertical scrolling
- Remove unnecessary SectionBox wrapper from SettingsPage (Headlamp provides container)
- Add param guard to prevent race condition on initial detail view load

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-13 07:38:45 -05:00
github-actions[bot] 889504962d chore: release v0.2.13 2026-02-13 11:34:16 +00:00
Chris Farhood 7b51df5ce5 chore: bump version to 0.2.13 2026-02-13 06:33:22 -05:00
Chris Farhood a3b860c1f5 fix: use friendly name 'Sealed Secrets' in settings UI
Changed plugin settings registration name from 'headlamp-sealed-secrets'
to 'Sealed Secrets' for better user experience in Settings → Plugins.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-13 06:33:22 -05:00
github-actions[bot] 4efe88cf6e chore: release v0.2.12 2026-02-13 02:41:59 +00:00
Chris Farhood 0ded85fe23 chore: bump version to 0.2.12 2026-02-12 21:41:08 -05:00
Chris Farhood b08df4fb76 feat: improve UX with drawer detail view and proper settings placement
Major UX improvements:
- Changed detail view from full page to drawer (slides from right)
- Moved plugin settings from sidebar to Settings → Plugins (proper pattern)
- Fixed React error #310 by adding defensive String() wrappers
- Fixed syncMessage getter to always return string
- Added safety checks for encryptedData access
- Added error handling for useGet failures

The drawer approach keeps the list visible while viewing details,
matching Headlamp's design patterns. Settings are now properly
located in the global Settings → Plugins section instead of
cluttering the plugin's sidebar navigation.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-12 21:41:08 -05:00
8 changed files with 173 additions and 109 deletions
+5 -5
View File
@@ -1,13 +1,13 @@
# Artifact Hub package metadata file # Artifact Hub package metadata file
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml # https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml
version: 0.2.10 version: 0.2.16
name: headlamp-sealed-secrets name: headlamp-sealed-secrets
displayName: Sealed Secrets Plugin for Headlamp displayName: Sealed Secrets
createdAt: "2026-02-12T00:00:00Z" createdAt: "2026-02-12T00:00:00Z"
description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption and RBAC-aware UI description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption and RBAC-aware UI
license: Apache-2.0 license: Apache-2.0
homeURL: https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin homeURL: https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin
appVersion: 0.2.10 appVersion: 0.2.16
containersImages: containersImages:
- name: sealed-secrets-controller - name: sealed-secrets-controller
image: docker.io/bitnami/sealed-secrets-controller:v0.24.0 image: docker.io/bitnami/sealed-secrets-controller:v0.24.0
@@ -19,8 +19,8 @@ keywords:
- encryption - encryption
- security - security
annotations: annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v0.2.10/headlamp-sealed-secrets-0.2.10.tar.gz" headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v0.2.16/headlamp-sealed-secrets-0.2.16.tar.gz"
headlamp/plugin/archive-checksum: sha256:f78b772929d9e26dcbb34a52c233f79219ee010619ab34469e3c9bf12ee81435 headlamp/plugin/archive-checksum: sha256:a71c98341e07a9bb946a75ee3a3d139b9dcb6c905464950685fc6779b229e5cf
headlamp/plugin/version-compat: ">=0.13.0" headlamp/plugin/version-compat: ">=0.13.0"
headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop" headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop"
links: links:
+2 -1
View File
@@ -1,6 +1,7 @@
{ {
"name": "headlamp-sealed-secrets", "name": "headlamp-sealed-secrets",
"version": "0.2.10", "displayName": "Sealed Secrets",
"version": "0.2.16",
"description": "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets", "description": "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets",
"files": [ "files": [
"dist", "dist",
@@ -35,6 +35,10 @@ abstract class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoun
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo); console.error('Error caught by boundary:', error, errorInfo);
console.error('Error type:', typeof error);
console.error('Error keys:', Object.keys(error));
console.error('Error message:', error.message);
console.error('Error toString:', String(error));
this.setState({ errorInfo }); this.setState({ errorInfo });
} }
@@ -95,7 +99,14 @@ export class CryptoErrorBoundary extends BaseErrorBoundary {
variant="body2" variant="body2"
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }} sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
> >
Error: {this.state.error.message} {(() => {
try {
const msg = this.state.error.message || this.state.error.toString();
return `Error: ${String(msg)}`;
} catch (e) {
return 'Error: [Unable to display error message]';
}
})()}
</Typography> </Typography>
)} )}
</Alert> </Alert>
@@ -143,7 +154,14 @@ export class ApiErrorBoundary extends BaseErrorBoundary {
variant="body2" variant="body2"
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }} sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
> >
Error: {this.state.error.message} {(() => {
try {
const msg = this.state.error.message || this.state.error.toString();
return `Error: ${String(msg)}`;
} catch (e) {
return 'Error: [Unable to display error message]';
}
})()}
</Typography> </Typography>
)} )}
</Alert> </Alert>
@@ -182,7 +200,14 @@ export class GenericErrorBoundary extends BaseErrorBoundary {
variant="body2" variant="body2"
sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }} sx={{ mt: 2, fontFamily: 'monospace', fontSize: '0.875rem' }}
> >
Error: {this.state.error.message} {(() => {
try {
const msg = this.state.error.message || this.state.error.toString();
return `Error: ${String(msg)}`;
} catch (e) {
return 'Error: [Unable to display error message]';
}
})()}
</Typography> </Typography>
)} )}
</Alert> </Alert>
@@ -5,6 +5,7 @@
* encrypted data, template, resulting Secret, and actions * encrypted data, template, resulting Secret, and actions
*/ */
import { Icon } from '@iconify/react';
import { K8s } from '@kinvolk/headlamp-plugin/lib'; import { K8s } from '@kinvolk/headlamp-plugin/lib';
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { import {
@@ -13,7 +14,18 @@ import {
SimpleTable, SimpleTable,
StatusLabel, StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import {
Alert,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Drawer,
IconButton,
Typography,
} from '@mui/material';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@@ -45,15 +57,15 @@ function formatScope(scope: SealedSecretScope): string {
* SealedSecret detail view component * SealedSecret detail view component
*/ */
export function SealedSecretDetail() { export function SealedSecretDetail() {
const { namespace, name } = useParams<{ namespace: string; name: string }>(); const { namespace = '', name = '' } = useParams<{ namespace: string; name: string }>();
const [sealedSecret] = SealedSecret.useGet(name, namespace); const [sealedSecret, error] = SealedSecret.useGet(name || undefined, namespace || undefined);
const [secret] = K8s.ResourceClasses.Secret.useGet(name, namespace); const [secret] = K8s.ResourceClasses.Secret.useGet(name || undefined, namespace || undefined);
const [decryptKey, setDecryptKey] = React.useState<string | null>(null); const [decryptKey, setDecryptKey] = React.useState<string | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const [rotating, setRotating] = React.useState(false); const [rotating, setRotating] = React.useState(false);
const [canDecrypt, setCanDecrypt] = React.useState(false); const [canDecrypt, setCanDecrypt] = React.useState(false);
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const { permissions } = usePermissions(namespace); const { permissions } = usePermissions(namespace || undefined);
// Check if user can decrypt secrets (requires get permission on Secrets) // Check if user can decrypt secrets (requires get permission on Secrets)
React.useEffect(() => { React.useEffect(() => {
@@ -62,6 +74,27 @@ export function SealedSecretDetail() {
} }
}, [namespace]); }, [namespace]);
// Wait for required params before rendering
if (!namespace || !name) {
return <SealedSecretDetailSkeleton />;
}
// Show error if fetch failed
if (error) {
return (
<Box p={3}>
<Alert severity="error">
<Typography variant="h6" gutterBottom>
Failed to load SealedSecret
</Typography>
<Typography variant="body2">
{String(error)}
</Typography>
</Alert>
</Box>
);
}
// Show loading skeleton while data is being fetched // Show loading skeleton while data is being fetched
if (!sealedSecret) { if (!sealedSecret) {
return <SealedSecretDetailSkeleton />; return <SealedSecretDetailSkeleton />;
@@ -94,16 +127,41 @@ export function SealedSecretDetail() {
} }
}, [sealedSecret, enqueueSnackbar]); }, [sealedSecret, enqueueSnackbar]);
const encryptedKeys = Object.keys(sealedSecret.spec.encryptedData || {}); // Safety check - should never happen due to early returns above, but be defensive
if (!sealedSecret?.spec?.encryptedData) {
return <SealedSecretDetailSkeleton />;
}
const encryptedKeys = Object.keys(sealedSecret.spec.encryptedData);
const handleClose = () => {
window.history.back();
};
return ( return (
<> <>
<Box> <Drawer
<SectionBox anchor="right"
title={ open
<Box display="flex" alignItems="center" justifyContent="space-between"> onClose={handleClose}
<span>{sealedSecret.metadata.name}</span> PaperProps={{
<Box> sx: {
width: { xs: '100%', sm: '600px', md: '800px' },
maxWidth: '100%',
},
}}
>
<Box sx={{ height: '100%', overflow: 'auto' }}>
<SectionBox
title={
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center" gap={1}>
<IconButton onClick={handleClose} edge="start" size="small">
<Icon icon="mdi:close" />
</IconButton>
<span>{sealedSecret.metadata.name}</span>
</Box>
<Box>
{permissions?.canUpdate && ( {permissions?.canUpdate && (
<Button <Button
variant="outlined" variant="outlined"
@@ -131,15 +189,15 @@ export function SealedSecretDetail() {
rows={[ rows={[
{ {
name: 'Name', name: 'Name',
value: sealedSecret.metadata.name, value: String(sealedSecret.metadata.name || ''),
}, },
{ {
name: 'Namespace', name: 'Namespace',
value: sealedSecret.metadata.namespace, value: String(sealedSecret.metadata.namespace || ''),
}, },
{ {
name: 'Scope', name: 'Scope',
value: formatScope(sealedSecret.scope), value: String(formatScope(sealedSecret.scope)),
}, },
{ {
name: 'Sync Status', name: 'Sync Status',
@@ -151,16 +209,18 @@ export function SealedSecretDetail() {
}, },
{ {
name: 'Status Message', name: 'Status Message',
value: sealedSecret.syncMessage, value: String(sealedSecret.syncMessage || 'Unknown'),
hide: !sealedSecret.syncCondition, hide: !sealedSecret.syncCondition,
}, },
{ {
name: 'Age', name: 'Age',
value: sealedSecret.getAge(), value: String(sealedSecret.getAge() || ''),
}, },
{ {
name: 'Created', name: 'Created',
value: new Date(sealedSecret.metadata.creationTimestamp!).toLocaleString(), value: sealedSecret.metadata.creationTimestamp
? new Date(sealedSecret.metadata.creationTimestamp).toLocaleString()
: 'Unknown',
}, },
]} ]}
/> />
@@ -207,16 +267,18 @@ export function SealedSecretDetail() {
rows={[ rows={[
{ {
name: 'Secret Type', name: 'Secret Type',
value: sealedSecret.spec.template.type || 'Opaque', value: String(sealedSecret.spec.template.type || 'Opaque'),
}, },
{ {
name: 'Labels', name: 'Labels',
value: JSON.stringify(sealedSecret.spec.template.metadata?.labels || {}), value: String(JSON.stringify(sealedSecret.spec.template.metadata?.labels || {})),
hide: !sealedSecret.spec.template.metadata?.labels, hide: !sealedSecret.spec.template.metadata?.labels,
}, },
{ {
name: 'Annotations', name: 'Annotations',
value: JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {}), value: String(
JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {})
),
hide: !sealedSecret.spec.template.metadata?.annotations, hide: !sealedSecret.spec.template.metadata?.annotations,
}, },
]} ]}
@@ -234,7 +296,7 @@ export function SealedSecretDetail() {
}, },
{ {
name: 'Keys', name: 'Keys',
value: Object.keys(secret.data || {}).join(', '), value: String(Object.keys(secret.data || {}).join(', ') || 'None'),
}, },
{ {
name: 'Link', name: 'Link',
@@ -242,8 +304,8 @@ export function SealedSecretDetail() {
<Link <Link
routeName="secret" routeName="secret"
params={{ params={{
namespace: secret.metadata.namespace, namespace: String(secret.metadata.namespace || ''),
name: secret.metadata.name, name: String(secret.metadata.name || ''),
}} }}
> >
View Secret View Secret
@@ -259,29 +321,30 @@ export function SealedSecretDetail() {
</Box> </Box>
)} )}
</SectionBox> </SectionBox>
</Box> </Box>
{decryptKey && ( {decryptKey && (
<DecryptDialog <DecryptDialog
sealedSecret={sealedSecret} sealedSecret={sealedSecret}
secretKey={decryptKey} secretKey={decryptKey}
onClose={() => setDecryptKey(null)} onClose={() => setDecryptKey(null)}
/> />
)} )}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}> <Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete SealedSecret?</DialogTitle> <DialogTitle>Delete SealedSecret?</DialogTitle>
<DialogContent> <DialogContent>
Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also
delete the resulting Kubernetes Secret. delete the resulting Kubernetes Secret.
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button> <Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={handleDelete} color="error"> <Button onClick={handleDelete} color="error">
Delete Delete
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Drawer>
</> </>
); );
} }
@@ -13,11 +13,13 @@ import {
} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Box, Button } from '@mui/material'; import { Box, Button } from '@mui/material';
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom';
import { usePermission } from '../hooks/usePermissions'; import { usePermission } from '../hooks/usePermissions';
import { SealedSecret } from '../lib/SealedSecretCRD'; import { SealedSecret } from '../lib/SealedSecretCRD';
import { SealedSecretScope } from '../types'; import { SealedSecretScope } from '../types';
import { EncryptDialog } from './EncryptDialog'; import { EncryptDialog } from './EncryptDialog';
import { SealedSecretListSkeleton } from './LoadingSkeletons'; import { SealedSecretListSkeleton } from './LoadingSkeletons';
import { SealedSecretDetail } from './SealedSecretDetail';
import { VersionWarning } from './VersionWarning'; import { VersionWarning } from './VersionWarning';
/** /**
@@ -40,6 +42,7 @@ function formatScope(scope: SealedSecretScope): string {
* SealedSecrets list view component * SealedSecrets list view component
*/ */
export function SealedSecretList() { export function SealedSecretList() {
const { namespace, name } = useParams<{ namespace?: string; name?: string }>();
const [sealedSecrets, error, loading] = SealedSecret.useList(); const [sealedSecrets, error, loading] = SealedSecret.useList();
const [createDialogOpen, setCreateDialogOpen] = React.useState(false); const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
const { allowed: canCreate } = usePermission(undefined, 'canCreate'); const { allowed: canCreate } = usePermission(undefined, 'canCreate');
@@ -159,6 +162,8 @@ export function SealedSecretList() {
</SectionBox> </SectionBox>
<EncryptDialog open={createDialogOpen} onClose={handleCloseDialog} /> <EncryptDialog open={createDialogOpen} onClose={handleCloseDialog} />
{namespace && name && <SealedSecretDetail />}
</> </>
); );
} }
@@ -4,7 +4,6 @@
* Configuration page for the Sealed Secrets plugin * Configuration page for the Sealed Secrets plugin
*/ */
import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Box, Button, Divider, TextField, Typography } from '@mui/material'; import { Box, Button, Divider, TextField, Typography } from '@mui/material';
import { useSnackbar } from 'notistack'; import { useSnackbar } from 'notistack';
import React from 'react'; import React from 'react';
@@ -35,10 +34,7 @@ export function SettingsPage() {
}; };
return ( return (
<SectionBox <Box p={3}>
title="Sealed Secrets Plugin Settings"
>
<Box p={3}>
<Typography variant="body1" paragraph id="settings-description"> <Typography variant="body1" paragraph id="settings-description">
Configure the connection to your Sealed Secrets controller. These settings are stored in Configure the connection to your Sealed Secrets controller. These settings are stored in
your browser's local storage. your browser's local storage.
@@ -155,7 +151,6 @@ export function SettingsPage() {
<dd style={{ display: 'inline', margin: 0 }}>8080</dd> <dd style={{ display: 'inline', margin: 0 }}>8080</dd>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
</SectionBox>
); );
} }
+18 -35
View File
@@ -16,12 +16,12 @@
import { import {
registerDetailsViewSection, registerDetailsViewSection,
registerPluginSettings,
registerRoute, registerRoute,
registerSidebarEntry, registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib'; } from '@kinvolk/headlamp-plugin/lib';
import React from 'react'; import React from 'react';
import { ApiErrorBoundary, GenericErrorBoundary } from './components/ErrorBoundary'; import { ApiErrorBoundary, GenericErrorBoundary } from './components/ErrorBoundary';
import { SealedSecretDetail } from './components/SealedSecretDetail';
import { SealedSecretList } from './components/SealedSecretList'; import { SealedSecretList } from './components/SealedSecretList';
import { SealingKeysView } from './components/SealingKeysView'; import { SealingKeysView } from './components/SealingKeysView';
import { SecretDetailsSection } from './components/SecretDetailsSection'; import { SecretDetailsSection } from './components/SecretDetailsSection';
@@ -56,21 +56,13 @@ registerSidebarEntry({
url: '/sealedsecrets/keys', url: '/sealedsecrets/keys',
}); });
// "Settings" child entry
registerSidebarEntry({
parent: 'sealed-secrets',
name: 'sealed-secrets-settings',
label: 'Settings',
url: '/sealedsecrets/settings',
});
/** /**
* Register routes * Register routes
*/ */
// List view // List view with optional detail drawer
registerRoute({ registerRoute({
path: '/sealedsecrets', path: '/sealedsecrets/:namespace?/:name?',
sidebar: 'sealed-secrets-list', sidebar: 'sealed-secrets-list',
component: () => ( component: () => (
<ApiErrorBoundary> <ApiErrorBoundary>
@@ -78,18 +70,6 @@ registerRoute({
</ApiErrorBoundary> </ApiErrorBoundary>
), ),
exact: true, exact: true,
});
// Detail view
registerRoute({
path: '/sealedsecrets/:namespace/:name',
sidebar: 'sealed-secrets-list',
component: () => (
<ApiErrorBoundary>
<SealedSecretDetail />
</ApiErrorBoundary>
),
exact: true,
name: 'sealedsecret', name: 'sealedsecret',
}); });
@@ -105,18 +85,6 @@ registerRoute({
exact: true, exact: true,
}); });
// Settings page
registerRoute({
path: '/sealedsecrets/settings',
sidebar: 'sealed-secrets-settings',
component: () => (
<GenericErrorBoundary>
<SettingsPage />
</GenericErrorBoundary>
),
exact: true,
});
/** /**
* Register integration with Secret detail view * Register integration with Secret detail view
* *
@@ -132,3 +100,18 @@ registerDetailsViewSection(({ resource }) => {
} }
return null; return null;
}); });
/**
* Register plugin settings
*
* Settings will appear in Settings → Plugins → Sealed Secrets
*/
registerPluginSettings(
'Sealed Secrets',
() => (
<GenericErrorBoundary>
<SettingsPage />
</GenericErrorBoundary>
),
true // Display save button
);
@@ -101,7 +101,9 @@ export class SealedSecret extends KubeObject<SealedSecretInterface> {
if (!condition) { if (!condition) {
return 'Unknown'; return 'Unknown';
} }
return condition.message || condition.reason || condition.status; // Ensure we always return a string, not an object
const message = condition.message || condition.reason || condition.status;
return String(message || 'Unknown');
} }
/** /**
@@ -120,21 +122,11 @@ export class SealedSecret extends KubeObject<SealedSecretInterface> {
} }
const result = await tryCatchAsync(async () => { const result = await tryCatchAsync(async () => {
// Query the CRD to get available versions // Query the CRD to get available versions using Headlamp's API proxy
const response = await fetch( const crd = await ApiProxy.request(
'/apis/apiextensions.k8s.io/v1/customresourcedefinitions/sealedsecrets.bitnami.com' '/apis/apiextensions.k8s.io/v1/customresourcedefinitions/sealedsecrets.bitnami.com'
); );
if (!response.ok) {
if (response.status === 404) {
throw new Error('SealedSecrets CRD not found. Please install Sealed Secrets on the cluster.');
}
const errorText = await response.text().catch(() => response.statusText);
throw new Error(`Failed to fetch CRD (${response.status} ${response.statusText}): ${errorText}`);
}
const crd = await response.json();
// Find the storage version (the version used for persistence) // Find the storage version (the version used for persistence)
const storageVersion = crd.spec?.versions?.find((v: any) => v.storage === true); const storageVersion = crd.spec?.versions?.find((v: any) => v.storage === true);