Compare commits

...

21 Commits

Author SHA1 Message Date
github-actions[bot] cd5a8c40ee chore: release v0.2.18 2026-02-13 21:00:55 +00:00
Chris Farhood 1ec8340a0f fix: restore SectionBox wrapper in SettingsPage to fix React context
The plugin settings page requires SectionBox from CommonComponents
to properly initialize the React context. Without it, React.useState
is undefined causing runtime errors.

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 15:59:25 -05:00
github-actions[bot] 2f746486db chore: release v0.2.17 2026-02-13 18:01:14 +00:00
Chris Farhood 55b10c5ab2 fix: use jq instead of node for package name extraction
jq is available in GitHub Actions without needing Node.js setup first.

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:59:54 -05:00
Chris Farhood a7761e992b fix: make release workflow use dynamic package name from package.json
Changed hardcoded 'headlamp-sealed-secrets' references to dynamically
read package name, allowing package.json name to be '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:59:10 -05:00
Chris Farhood 679922e711 fix: change package name from headlamp-sealed-secrets to sealed-secrets
This matches the polaris plugin naming convention where the package
name is just 'polaris', causing Headlamp to display 'sealed-secrets'
in the plugin settings list instead of '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:56:44 -05:00
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
9 changed files with 191 additions and 117 deletions
+18 -10
View File
@@ -24,6 +24,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Get package name
id: package_name
working-directory: ./headlamp-sealed-secrets
run: |
PKG_NAME=$(jq -r '.name' package.json)
echo "name=${PKG_NAME}" >> $GITHUB_OUTPUT
echo "Package name: ${PKG_NAME}"
- name: Configure git
run: |
git config user.name "github-actions[bot]"
@@ -38,7 +46,7 @@ jobs:
- name: Update artifacthub-pkg.yml version
run: |
VERSION="${{ inputs.version }}"
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/headlamp-sealed-secrets-${VERSION}.tar.gz"
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${{ steps.package_name.outputs.name }}-${VERSION}.tar.gz"
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
sed -i "s|^appVersion:.*|appVersion: ${VERSION}|" artifacthub-pkg.yml
@@ -74,7 +82,7 @@ jobs:
- name: Move tarball to root
working-directory: ./headlamp-sealed-secrets
run: |
TARBALL="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
TARBALL="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
if [ ! -f "${TARBALL}" ]; then
echo "::error::Expected tarball ${TARBALL} not found"
ls -la *.tar.gz
@@ -85,7 +93,7 @@ jobs:
- name: Validate tarball name
run: |
EXPECTED="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
EXPECTED="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
ACTUAL=$(ls *.tar.gz)
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Tarball name mismatch! Expected: $EXPECTED, Got: $ACTUAL"
@@ -96,19 +104,19 @@ jobs:
- name: Compute checksum
id: compute_checksum
run: |
TARBALL="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
TARBALL="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
echo "Checksum: sha256:${CHECKSUM}"
- name: Verify tarball contents
run: |
TARBALL="headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
TARBALL="${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
echo "Tarball contents:"
tar -tzf "${TARBALL}" | head -20
# Verify main.js exists (structure is headlamp-sealed-secrets/main.js)
if ! tar -tzf "${TARBALL}" | grep -q "headlamp-sealed-secrets/main.js"; then
# Verify main.js exists (structure is <package-name>/main.js)
if ! tar -tzf "${TARBALL}" | grep -q "${{ steps.package_name.outputs.name }}/main.js"; then
echo "::error::main.js not found in tarball"
exit 1
fi
@@ -134,7 +142,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ inputs.version }}"
files: headlamp-sealed-secrets-${{ inputs.version }}.tar.gz
files: ${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz
fail_on_unmatched_files: true
draft: false
prerelease: false
@@ -147,9 +155,9 @@ jobs:
echo "Release Summary:"
echo "=================="
echo "Version: v${{ inputs.version }}"
echo "Tarball: headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
echo "Tarball: ${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
echo "Checksum: sha256:${{ steps.compute_checksum.outputs.checksum }}"
echo "Archive URL: https://github.com/${{ github.repository }}/releases/download/v${{ inputs.version }}/headlamp-sealed-secrets-${{ inputs.version }}.tar.gz"
echo "Archive URL: https://github.com/${{ github.repository }}/releases/download/v${{ inputs.version }}/${{ steps.package_name.outputs.name }}-${{ inputs.version }}.tar.gz"
echo ""
echo "✓ Version bumped to ${{ inputs.version }}"
echo "✓ Metadata updated with checksum"
+6 -6
View File
@@ -1,13 +1,13 @@
# Artifact Hub package metadata file
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml
version: 0.2.10
name: headlamp-sealed-secrets
displayName: Sealed Secrets Plugin for Headlamp
version: 0.2.18
name: sealed-secrets
displayName: Sealed Secrets
createdAt: "2026-02-12T00:00:00Z"
description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption and RBAC-aware UI
license: Apache-2.0
homeURL: https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin
appVersion: 0.2.10
appVersion: 0.2.18
containersImages:
- name: sealed-secrets-controller
image: docker.io/bitnami/sealed-secrets-controller:v0.24.0
@@ -19,8 +19,8 @@ keywords:
- encryption
- security
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-checksum: sha256:f78b772929d9e26dcbb34a52c233f79219ee010619ab34469e3c9bf12ee81435
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-sealed-secrets-plugin/releases/download/v0.2.18/sealed-secrets-0.2.18.tar.gz"
headlamp/plugin/archive-checksum: sha256:b110bd4a2ac333ab17881c9f097f0b796edffa48ad9d95be6fda15236a503475
headlamp/plugin/version-compat: ">=0.13.0"
headlamp/plugin/distro-compat: "desktop,in-cluster,web,docker-desktop"
links:
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "headlamp-sealed-secrets",
"version": "0.2.10",
"name": "sealed-secrets",
"version": "0.2.18",
"description": "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets",
"files": [
"dist",
@@ -35,6 +35,10 @@ abstract class BaseErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoun
componentDidCatch(error: Error, errorInfo: React.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 });
}
@@ -95,7 +99,14 @@ export class CryptoErrorBoundary extends BaseErrorBoundary {
variant="body2"
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>
)}
</Alert>
@@ -143,7 +154,14 @@ export class ApiErrorBoundary extends BaseErrorBoundary {
variant="body2"
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>
)}
</Alert>
@@ -182,7 +200,14 @@ export class GenericErrorBoundary extends BaseErrorBoundary {
variant="body2"
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>
)}
</Alert>
@@ -5,6 +5,7 @@
* 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 {
@@ -13,7 +14,18 @@ import {
SimpleTable,
StatusLabel,
} 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 React from 'react';
import { useParams } from 'react-router-dom';
@@ -45,15 +57,15 @@ function formatScope(scope: SealedSecretScope): string {
* SealedSecret detail view component
*/
export function SealedSecretDetail() {
const { namespace, name } = useParams<{ namespace: string; name: string }>();
const [sealedSecret] = SealedSecret.useGet(name, namespace);
const [secret] = K8s.ResourceClasses.Secret.useGet(name, namespace);
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<string | null>(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);
const { permissions } = usePermissions(namespace || undefined);
// Check if user can decrypt secrets (requires get permission on Secrets)
React.useEffect(() => {
@@ -62,6 +74,27 @@ export function SealedSecretDetail() {
}
}, [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
if (!sealedSecret) {
return <SealedSecretDetailSkeleton />;
@@ -94,16 +127,41 @@ export function SealedSecretDetail() {
}
}, [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 (
<>
<Box>
<SectionBox
title={
<Box display="flex" alignItems="center" justifyContent="space-between">
<span>{sealedSecret.metadata.name}</span>
<Box>
<Drawer
anchor="right"
open
onClose={handleClose}
PaperProps={{
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 && (
<Button
variant="outlined"
@@ -131,15 +189,15 @@ export function SealedSecretDetail() {
rows={[
{
name: 'Name',
value: sealedSecret.metadata.name,
value: String(sealedSecret.metadata.name || ''),
},
{
name: 'Namespace',
value: sealedSecret.metadata.namespace,
value: String(sealedSecret.metadata.namespace || ''),
},
{
name: 'Scope',
value: formatScope(sealedSecret.scope),
value: String(formatScope(sealedSecret.scope)),
},
{
name: 'Sync Status',
@@ -151,16 +209,18 @@ export function SealedSecretDetail() {
},
{
name: 'Status Message',
value: sealedSecret.syncMessage,
value: String(sealedSecret.syncMessage || 'Unknown'),
hide: !sealedSecret.syncCondition,
},
{
name: 'Age',
value: sealedSecret.getAge(),
value: String(sealedSecret.getAge() || ''),
},
{
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={[
{
name: 'Secret Type',
value: sealedSecret.spec.template.type || 'Opaque',
value: String(sealedSecret.spec.template.type || 'Opaque'),
},
{
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,
},
{
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,
},
]}
@@ -234,7 +296,7 @@ export function SealedSecretDetail() {
},
{
name: 'Keys',
value: Object.keys(secret.data || {}).join(', '),
value: String(Object.keys(secret.data || {}).join(', ') || 'None'),
},
{
name: 'Link',
@@ -242,8 +304,8 @@ export function SealedSecretDetail() {
<Link
routeName="secret"
params={{
namespace: secret.metadata.namespace,
name: secret.metadata.name,
namespace: String(secret.metadata.namespace || ''),
name: String(secret.metadata.name || ''),
}}
>
View Secret
@@ -259,29 +321,30 @@ export function SealedSecretDetail() {
</Box>
)}
</SectionBox>
</Box>
</Box>
{decryptKey && (
<DecryptDialog
sealedSecret={sealedSecret}
secretKey={decryptKey}
onClose={() => setDecryptKey(null)}
/>
)}
{decryptKey && (
<DecryptDialog
sealedSecret={sealedSecret}
secretKey={decryptKey}
onClose={() => setDecryptKey(null)}
/>
)}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete SealedSecret?</DialogTitle>
<DialogContent>
Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also
delete the resulting Kubernetes Secret.
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={handleDelete} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete SealedSecret?</DialogTitle>
<DialogContent>
Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also
delete the resulting Kubernetes Secret.
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={handleDelete} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
</Drawer>
</>
);
}
@@ -13,11 +13,13 @@ import {
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Box, Button } from '@mui/material';
import React from 'react';
import { useParams } from 'react-router-dom';
import { usePermission } from '../hooks/usePermissions';
import { SealedSecret } from '../lib/SealedSecretCRD';
import { SealedSecretScope } from '../types';
import { EncryptDialog } from './EncryptDialog';
import { SealedSecretListSkeleton } from './LoadingSkeletons';
import { SealedSecretDetail } from './SealedSecretDetail';
import { VersionWarning } from './VersionWarning';
/**
@@ -40,6 +42,7 @@ function formatScope(scope: SealedSecretScope): string {
* SealedSecrets list view component
*/
export function SealedSecretList() {
const { namespace, name } = useParams<{ namespace?: string; name?: string }>();
const [sealedSecrets, error, loading] = SealedSecret.useList();
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
const { allowed: canCreate } = usePermission(undefined, 'canCreate');
@@ -159,6 +162,8 @@ export function SealedSecretList() {
</SectionBox>
<EncryptDialog open={createDialogOpen} onClose={handleCloseDialog} />
{namespace && name && <SealedSecretDetail />}
</>
);
}
@@ -35,9 +35,7 @@ export function SettingsPage() {
};
return (
<SectionBox
title="Sealed Secrets Plugin Settings"
>
<SectionBox title="Sealed Secrets Plugin Settings">
<Box p={3}>
<Typography variant="body1" paragraph id="settings-description">
Configure the connection to your Sealed Secrets controller. These settings are stored in
+18 -35
View File
@@ -16,12 +16,12 @@
import {
registerDetailsViewSection,
registerPluginSettings,
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import React from 'react';
import { ApiErrorBoundary, GenericErrorBoundary } from './components/ErrorBoundary';
import { SealedSecretDetail } from './components/SealedSecretDetail';
import { SealedSecretList } from './components/SealedSecretList';
import { SealingKeysView } from './components/SealingKeysView';
import { SecretDetailsSection } from './components/SecretDetailsSection';
@@ -56,21 +56,13 @@ registerSidebarEntry({
url: '/sealedsecrets/keys',
});
// "Settings" child entry
registerSidebarEntry({
parent: 'sealed-secrets',
name: 'sealed-secrets-settings',
label: 'Settings',
url: '/sealedsecrets/settings',
});
/**
* Register routes
*/
// List view
// List view with optional detail drawer
registerRoute({
path: '/sealedsecrets',
path: '/sealedsecrets/:namespace?/:name?',
sidebar: 'sealed-secrets-list',
component: () => (
<ApiErrorBoundary>
@@ -78,18 +70,6 @@ registerRoute({
</ApiErrorBoundary>
),
exact: true,
});
// Detail view
registerRoute({
path: '/sealedsecrets/:namespace/:name',
sidebar: 'sealed-secrets-list',
component: () => (
<ApiErrorBoundary>
<SealedSecretDetail />
</ApiErrorBoundary>
),
exact: true,
name: 'sealedsecret',
});
@@ -105,18 +85,6 @@ registerRoute({
exact: true,
});
// Settings page
registerRoute({
path: '/sealedsecrets/settings',
sidebar: 'sealed-secrets-settings',
component: () => (
<GenericErrorBoundary>
<SettingsPage />
</GenericErrorBoundary>
),
exact: true,
});
/**
* Register integration with Secret detail view
*
@@ -132,3 +100,18 @@ registerDetailsViewSection(({ resource }) => {
}
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) {
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 () => {
// Query the CRD to get available versions
const response = await fetch(
// Query the CRD to get available versions using Headlamp's API proxy
const crd = await ApiProxy.request(
'/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)
const storageVersion = crd.spec?.versions?.find((v: any) => v.storage === true);