From bda90d3d527e5d20c5d9a5e58a9b09e270a9d22c Mon Sep 17 00:00:00 2001 From: "alex.albu" Date: Thu, 3 Sep 2026 17:48:59 +0100 Subject: [PATCH 1/3] feat(api-docs-module-crd): show field defaults and enum values The CRD schema view showed each property's type, description and required status, but not the default value or the allowed values of an enum, so kubebuilder-derived defaults were invisible in the API docs. Carry `default` and `enum` through normalizeSchema (using nullish coalescing so a falsy default such as `false`, `0` or `""` survives), render the default as a chip in the property header, and list enum allowed values in the property details. Both the Kubernetes and simplified schema formats are supported. Adds tests and updates the plugin README and docs site. Signed-off-by: alex.albu --- plugins/api-docs-module-crd/README.md | 2 +- .../CrdDefinitionWidget.test.tsx | 76 ++++++++++++ .../CrdDefinitionWidget.tsx | 117 ++++++++++++++---- .../api-docs-module-crd/frontend/about.md | 1 + .../api-docs-module-crd/frontend/configure.md | 2 + .../plugins/api-docs-module-crd/overview.md | 2 +- 6 files changed, 177 insertions(+), 23 deletions(-) diff --git a/plugins/api-docs-module-crd/README.md b/plugins/api-docs-module-crd/README.md index f1847173..e8899f03 100644 --- a/plugins/api-docs-module-crd/README.md +++ b/plugins/api-docs-module-crd/README.md @@ -4,7 +4,7 @@ Welcome to the api-docs-module-crd plugin! [![npm latest version](https://img.shields.io/npm/v/@terasky/backstage-plugin-api-docs-module-crd/latest.svg)](https://www.npmjs.com/package/@terasky/backstage-plugin-api-docs-module-crd) -The `api-docs-module-crd` plugin is a frontend module that extends the Backstage API Docs plugin with support for Kubernetes Custom Resource Definitions (CRDs). It provides an interactive visualization of CRD schemas similar to doc.crds.dev, with features like multi-version support, property exploration, and example YAML generation. +The `api-docs-module-crd` plugin is a frontend module that extends the Backstage API Docs plugin with support for Kubernetes Custom Resource Definitions (CRDs). It provides an interactive visualization of CRD schemas similar to doc.crds.dev, with features like multi-version support, property exploration (including each field's default and allowed enum values), and example YAML generation. For detailed docs go to https://terasky-oss.github.io/backstage-plugins/plugins/api-docs-module-crd/overview diff --git a/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.test.tsx b/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.test.tsx index fab137ad..04a6b7fd 100644 --- a/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.test.tsx +++ b/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.test.tsx @@ -473,4 +473,80 @@ Schema: expect(yamlContent).toContain('simpleArray: []'); }); }); + + it('should render default values and allowed enum values (Kubernetes format)', async () => { + const user = userEvent.setup(); + const defaultsAndEnumCrd = ` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: myresources.example.com +spec: + group: example.com + names: + kind: MyResource + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + paused: + type: boolean + default: false + strategy: + type: string + default: RollingUpdate + enum: + - RollingUpdate + - Recreate +`; + + await renderInTestApp( + , + ); + await user.click(screen.getByText('+ expand all')); + + await waitFor(() => { + // A falsy default must survive (regression guard for the `??` merge). + expect(screen.getByText('default: false')).toBeInTheDocument(); + expect(screen.getByText('default: RollingUpdate')).toBeInTheDocument(); + expect(screen.getByText('Allowed values:')).toBeInTheDocument(); + // "Recreate" only appears as an enum value, never as a default. + expect(screen.getByText('Recreate')).toBeInTheDocument(); + }); + }); + + it('should render default values from the simplified schema format', async () => { + const user = userEvent.setup(); + const simplifiedDefaultsCrd = ` +Kind: MyResource +Group: example.com +Version: v1 +Schema: + Type: object + Properties: + spec: + Type: object + Properties: + replicas: + Type: integer + Default: 3 +`; + + await renderInTestApp( + , + ); + await user.click(screen.getByText('+ expand all')); + + await waitFor(() => { + expect(screen.getByText('default: 3')).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.tsx b/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.tsx index f1d558e7..1426a787 100644 --- a/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.tsx +++ b/plugins/api-docs-module-crd/src/components/CrdDefinitionWidget/CrdDefinitionWidget.tsx @@ -110,6 +110,24 @@ const useStyles = makeStyles(theme => ({ backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, }, + defaultChip: { + fontFamily: 'monospace', + backgroundColor: theme.palette.type === 'dark' + ? theme.palette.grey[700] + : theme.palette.grey[100], + color: theme.palette.text.secondary, + fontWeight: 500, + }, + enumContainer: { + display: 'flex', + alignItems: 'center', + flexWrap: 'wrap', + gap: theme.spacing(0.5), + marginBottom: theme.spacing(1), + }, + enumChip: { + fontFamily: 'monospace', + }, linkButton: { marginLeft: 'auto', minWidth: 'auto', @@ -162,6 +180,10 @@ interface CRDSchema { items?: CRDSchema; Required?: string[]; required?: string[]; + Default?: unknown; + default?: unknown; + Enum?: unknown[]; + enum?: unknown[]; } interface CRDVersion { @@ -190,9 +212,23 @@ function normalizeSchema(schema: CRDSchema): CRDSchema { Properties: schema.Properties || schema.properties, Items: schema.Items || (schema.items ? { Schema: schema.items } : undefined), Required: schema.Required || schema.required, + // Nullish coalescing, not ||, so a falsy default (false, 0, "") is preserved. + Default: schema.Default ?? schema.default, + Enum: schema.Enum ?? schema.enum, }; } +/** + * Renders a schema default for display. Strings are shown verbatim (an empty + * string as `""`, so it is not mistaken for "no default"); everything else is + * JSON-encoded. Returns undefined when no default is set. + */ +function formatDefault(value: unknown): string | undefined { + if (value === undefined) return undefined; + if (typeof value === 'string') return value === '' ? '""' : value; + return JSON.stringify(value); +} + function parseCRDData(data: any): ParsedCRDData | null { // Check if it's the simplified format if (data.Kind && data.Group && data.Version) { @@ -381,29 +417,46 @@ const SchemaPart: React.FC = ({ }) => { const classes = useStyles(); - const [props, propKeys, required, type, schema] = useMemo(() => { - const normalized = normalizeSchema(property); - let currentSchema = normalized; - let currentProps = normalized.Properties || {}; - let currentType = normalized.Type || 'string'; - - if (currentType === 'array' && normalized.Items?.Schema) { - const itemsSchema = normalizeSchema(normalized.Items.Schema); - if (itemsSchema.Type !== 'object') { - currentType = `[]${itemsSchema.Type}`; - } else { - currentSchema = itemsSchema; - currentProps = itemsSchema.Properties || {}; - currentType = '[]object'; + const [props, propKeys, required, type, schema, defaultValue, enumValues] = + useMemo(() => { + const normalized = normalizeSchema(property); + let currentSchema = normalized; + let currentProps = normalized.Properties || {}; + let currentType = normalized.Type || 'string'; + + if (currentType === 'array' && normalized.Items?.Schema) { + const itemsSchema = normalizeSchema(normalized.Items.Schema); + if (itemsSchema.Type !== 'object') { + currentType = `[]${itemsSchema.Type}`; + } else { + currentSchema = itemsSchema; + currentProps = itemsSchema.Properties || {}; + currentType = '[]object'; + } } - } - - const currentPropKeys = Object.keys(currentProps); - const normalizedParent = parent ? normalizeSchema(parent) : undefined; - const isRequired = normalizedParent?.Required?.includes(propertyKey) || false; - return [currentProps, currentPropKeys, isRequired, currentType, currentSchema]; - }, [parent, property, propertyKey]); + const currentPropKeys = Object.keys(currentProps); + const normalizedParent = parent ? normalizeSchema(parent) : undefined; + const isRequired = + normalizedParent?.Required?.includes(propertyKey) || false; + + // Default and enum belong to the property itself, so read them from the + // property's own schema rather than the array item schema resolved above. + const propDefault = formatDefault(normalized.Default); + const propEnum = normalized.Enum?.map(v => + typeof v === 'string' ? v : JSON.stringify(v), + ); + + return [ + currentProps, + currentPropKeys, + isRequired, + currentType, + currentSchema, + propDefault, + propEnum, + ] as const; + }, [parent, property, propertyKey]); const slug = useMemo( () => slugify((parentSlug ? `${parentSlug}-` : '') + propertyKey), @@ -473,6 +526,13 @@ const SchemaPart: React.FC = ({ className={classes.requiredChip} /> )} + {defaultValue !== undefined && ( + + )}