diff --git a/docs/docs/explanations/nebari-yaml-schema.mdx b/docs/docs/explanations/nebari-yaml-schema.mdx new file mode 100644 index 00000000..af77168b --- /dev/null +++ b/docs/docs/explanations/nebari-yaml-schema.mdx @@ -0,0 +1,12 @@ +--- +title: 'Configuring nebari-config.yaml' +id: nebari-yaml-schema +--- + +# Configuration Schema + +This section includes the Nebari configuration schema loaded dynamically below: + +import NebariConfig from '@site/src/components/NebariSchemaLoader'; + + diff --git a/docs/package.json b/docs/package.json index 8ed73d11..bf029c3b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -43,13 +43,16 @@ "@docusaurus/core": "2.0.0-beta.20", "@docusaurus/preset-classic": "2.0.0-beta.20", "@mdx-js/react": "^1.6.22", + "@stoplight/json-ref-resolver": "^3.1.6", "clsx": "^1.1.1", "docusaurus-lunr-search": "^2.1.15", "docusaurus-plugin-sass": "^0.2.2", "prism-react-renderer": "^1.3.5", "react": "^17.0.2", "react-dom": "^17.0.2", - "sass": "^1.52.1" + "sass": "^1.52.1", + "react-markdown": "^8.0.7", + "remark-gfm": "^4.0.0" }, "devDependencies": { "@babel/eslint-parser": "^7.18.2", diff --git a/docs/sidebars.js b/docs/sidebars.js index 1bf2f260..d1464394 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -94,6 +94,7 @@ module.exports = { "explanations/custom-overrides-configuration", "explanations/config-best-practices", "explanations/infrastructure-architecture", + "explanations/nebari-yaml-schema", ], }, { diff --git a/docs/src/components/NebariSchemaLoader/index.tsx b/docs/src/components/NebariSchemaLoader/index.tsx new file mode 100644 index 00000000..227df51d --- /dev/null +++ b/docs/src/components/NebariSchemaLoader/index.tsx @@ -0,0 +1,395 @@ +import { Resolver } from '@stoplight/json-ref-resolver'; +import Admonition from '@theme/Admonition'; +import Details from '@theme/Details'; +import Heading from '@theme/Heading'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; +import React, { ReactElement, useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; + +// import JSONSchema from '../../../static/nebari-config-schema.json'; + +const schemaUrl = "https://raw.githubusercontent.com/viniciusdc/nebari/nebari-schema-models/nebari-config-schema.json"; + +type SchemaProperty = { + deprecated?: boolean; + description?: string; + type?: string | string[]; + pattern?: string; + title?: string; + items?: any; + properties?: { [key: string]: SchemaProperty }; + enum?: string[]; + default?: any; + allOf?: SchemaProperty[]; + anyOf?: SchemaProperty[]; + examples?: string[]; + optionsAre?: string[]; + note?: string; + depends_on?: string | object; + group_by?: string; +}; + +type Properties = { [key: string]: SchemaProperty }; + +type Schema = { + title: string; + description: string; + type: string; + properties: Properties; + required?: string[]; +}; + +const defaultSchema: Schema = { + title: "ConfigSchema", + description: "The configuration schema for Nebari.", + type: "object", + properties: {} +}; + +function useSchema(schemaUrl: string, useLocal = false) { + const [schema, setSchema] = useState(defaultSchema); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (useLocal) { + const resolver = new Resolver(); + resolver.resolve(JSONSchema, {}).then((resolvedSchema) => { + setSchema(resolvedSchema.result); + }); + setLoading(false); + setError(null); + } else { + async function fetchSchema() { + try { + const response = await fetch(schemaUrl, { headers: { 'Accept': 'application/json' } }); + if (!response.ok) { + throw new Error(`Failed to fetch schema: ${response.status} ${response.statusText}`); + } + + const json = await response.json(); + const resolver = new Resolver(); + const resolvedSchema = await resolver.resolve(json, {}); + setSchema(resolvedSchema.result); + } catch (err) { + console.error('Error:', err); + setError(err.message); + } finally { + setLoading(false); + } + } + + fetchSchema(); + } + }, [schemaUrl, useLocal]); + + return { schema, loading, error }; +} + +function ParentComponent({ schema, toc = null }) { + return ( +
+ + + +
+ ); +} + + +export default function NebariConfig({ toc = null }) { + const { schema, loading, error } = useSchema(schemaUrl, false); + + if (loading) return

Loading schema...

; + if (error) return

Error loading schema: {error}

; + + return ( + <> + + This documentation is autogenerated from the Nebari configuration JSON Schema. + View the original schema. + + {/*
+
{JSON.stringify(schema, null, 2)}
+
*/} + + + ); +} + +function SchemaToc({ schema }: { schema: Schema }) { + return ( + + ); +} + +function PropertyTitle({ title, subHeading = false, deprecated = false }) { + const titleStyle = { + background: 'linear-gradient(to right, var(--ifm-color-primary) 0%, var(--ifm-color-primary) 5px, var(--ifm-admonition-background-color) 5px, var(--ifm-admonition-background-color) 100%)', + padding: '8px 15px', + borderRadius: '5px', + display: 'inline-block' + }; + return ( +
+ + + {title} {deprecated && Deprecated} + + +
+ ); +} + +function mergeProperties(property, keys) { + const base = { ...property }; + keys.forEach(key => { + if (!property[key]) { + return; + } + if (Array.isArray(property[key])) { + base[key] = undefined; // Clean up the base object by removing the processed key + base.properties = property[key].reduce((acc, cur) => { + let mergedProperties = { ...acc, ...cur.properties }; + return { ...acc, ...mergeProperties(cur, keys).properties, ...mergedProperties }; + }, base.properties || {}); + } else if (key === 'additionalProperties' && typeof property[key] === 'object') { + base.properties = { + ...base.properties, + ...mergeProperties(property[key], ['properties']).properties + }; + } + } + ); + return base; +} + +function capitalizeFirstLetter(string) { + let base_string = string.replace(/_/g, " "); + return base_string.charAt(0).toUpperCase() + base_string.slice(1); +} + +function renderProperties(value, keys, sub_heading, settingKey = null) { + const mergedProperties = mergeProperties(value, keys).properties ?? {}; + if (Object.keys(mergedProperties).length > 0) { + return ( +
{capitalizeFirstLetter(settingKey)}: Available Options : Available Options}> + +
+ ); + } + return null; +} + + +function Setting({ settingKey, value, subHeading, level = 1, toc = null }) { + if (toc) { + if (!toc.find((item) => item.value === settingKey)) { + toc.push({ + value: settingKey, + id: settingKey.replace(/_/g, "-").toLowerCase(), + level: level + 2, + }); + } + } + return ( +
+ + + {renderProperties(value, ['allOf', 'anyOf', 'additionalProperties'], subHeading)} +
+
+ ); +} + +function PropertiesList({ properties, sub_heading = false, toc = null }) { + return ( + <> + {Object.entries(properties).sort().map(([key, value]) => ( + + ))} + + ); +} + +const MarkdownCodeSeparator = ({ examples, inputKey }) => { + // Function to extract the YAML code block and briefing paragraph from each example + const parseContent = (input) => { + const codeRegex = /```yaml[\s\S]*?```/; // Regex to match the YAML code block + const codeMatch = input.match(codeRegex); + + let codeBlock = ''; + if (codeMatch) { + // Extract code and remove the fencing + codeBlock = codeMatch[0].replace(/```yaml|```/g, '').trim(); + // Normalize indentation + const lines = codeBlock.split('\n'); + const minIndentation = lines.filter(line => line.trim()) + .reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity); + codeBlock = lines.map(line => line.substring(minIndentation)).join('\n'); + } + + const briefing = input.replace(codeRegex, '').trim(); // Remove the code block from the briefing + + return { briefing, codeBlock }; + }; + + // Check if examples is defined and is either an array or an object + if (!examples || (typeof examples !== 'object')) { + return
No examples provided
; + } + + const exampleEntries = Array.isArray(examples) ? examples.map((example, index) => ({ label: `Example ${index + 1}`, value: example, key: `example-${index}` })) + : Object.entries(examples).map(([key, value]) => ({ label: key, value, key })); + + if (exampleEntries.length > 1) { + // Render content inside tabs when there are multiple examples + return ( + ({ label: entry.label, value: entry.key }))}> + {exampleEntries.map((entry, index) => { + const { briefing, codeBlock } = parseContent(entry.value); // Correct placement + return ( + +
+ {briefing} +
+                                    {codeBlock}
+                                
+
+
+ ); + })} +
+ ); + } else { + // If only one example, no need for tabs + const { briefing, codeBlock } = parseContent(exampleEntries[0].value); + return ( +
+ {briefing} +
+                    {codeBlock}
+                
+
+ ); + } +}; + + +interface PropertyRowProps { + label: string; + value: any; + isPreFormatted?: boolean; +} + +const PropertyRow: React.FC = ({ label, value, isPreFormatted = false }) => { + if (value === undefined) return Not specified; + + const formattedValue = isPreFormatted + ? (label === 'Depends on' ? {JSON.stringify(value)} : +
+                {JSON.stringify(value, null, 2)}
+            
) + : {Array.isArray(value) ? value.join(", ") : String(value)}; + + return ( + + + {label}: + + + {value !== null ? formattedValue : Not specified} + + + ); +} + +interface Property { + type?: string; + enum?: string[]; + optionsAre?: string; + pattern?: string; + depends_on?: any; + default?: any; + description?: string; + examples?: string[]; + note?: string; + warning?: string; +} + +const DefaultValueRow: React.FC<{ property: Property }> = ({ property }) => { + const shouldUsePreFormat = (value: any) => typeof value === 'object'; + + return ( + <> + {property.type && } + {property.enum && } + {property.optionsAre && } + {property.pattern && } + {property.depends_on && } + {property.default !== undefined && ( + + )} + + ); +} + +const PropertyContent: React.FC<{ property: Property }> = ({ property }) => { + const hasTableData = property.type || property.default !== undefined || property.enum || + property.optionsAre || property.pattern || property.depends_on; + + return ( +
+ {property.description && ( +
+ {property.description} +
+ )} + {hasTableData && ( + + {/* Adding CSS within the component */} + + + + +
+ )} + {property.examples && ( +
+ +
+ )} + {property.note && ( + + + + )} + {property.warning && ( + + + + )} +
+ ); +} + +const Markdown = ({ text }: { text: string }) => {text};