1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 13:29:41 +02:00

feat(editor): provide yaml validation for docker compose in the portainer web editor [BE-11697] (#526)

This commit is contained in:
Ali 2025-03-27 17:11:55 +13:00 committed by GitHub
parent 0ebfe047d1
commit 81c5f4acc3
27 changed files with 2046 additions and 36 deletions

View file

@ -1,11 +1,24 @@
import CodeMirror from '@uiw/react-codemirror';
import { StreamLanguage, LanguageSupport } from '@codemirror/language';
import CodeMirror, {
keymap,
oneDarkHighlightStyle,
} from '@uiw/react-codemirror';
import {
StreamLanguage,
LanguageSupport,
syntaxHighlighting,
indentService,
} from '@codemirror/language';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { useCallback, useMemo, useState } from 'react';
import { createTheme } from '@uiw/codemirror-themes';
import { tags as highlightTags } from '@lezer/highlight';
import type { JSONSchema7 } from 'json-schema';
import { lintKeymap, lintGutter } from '@codemirror/lint';
import { defaultKeymap } from '@codemirror/commands';
import { autocompletion, completionKeymap } from '@codemirror/autocomplete';
import { yamlCompletion, yamlSchema } from 'yaml-schema';
import { AutomationTestingProps } from '@/types';
@ -28,6 +41,7 @@ interface Props extends AutomationTestingProps {
height?: string;
versions?: number[];
onVersionChange?: (version: number) => void;
schema?: JSONSchema7;
}
const theme = createTheme({
@ -57,18 +71,69 @@ const theme = createTheme({
],
});
const yamlLanguage = new LanguageSupport(StreamLanguage.define(yaml));
// Custom indentation service for YAML
const yamlIndentExtension = indentService.of((context, pos) => {
const prevLine = context.lineAt(pos, -1);
// Default to same as previous line
const prevIndent = /^\s*/.exec(prevLine.text)?.[0].length || 0;
// If previous line ends with a colon, increase indent
if (/:\s*$/.test(prevLine.text)) {
return prevIndent + 2; // Indent 2 spaces after a colon
}
return prevIndent;
});
// Create enhanced YAML language with custom indentation (from @codemirror/legacy-modes/mode/yaml)
const yamlLanguageLegacy = new LanguageSupport(StreamLanguage.define(yaml), [
yamlIndentExtension,
syntaxHighlighting(oneDarkHighlightStyle),
]);
const dockerFileLanguage = new LanguageSupport(
StreamLanguage.define(dockerFile)
);
const shellLanguage = new LanguageSupport(StreamLanguage.define(shell));
const docTypeExtensionMap: Record<Type, LanguageSupport> = {
yaml: yamlLanguage,
yaml: yamlLanguageLegacy,
dockerfile: dockerFileLanguage,
shell: shellLanguage,
};
function schemaValidationExtensions(schema: JSONSchema7) {
// skip the hover extension because fields like 'networks' display as 'null' with no description when using the default hover
// skip the completion extension in favor of custom completion
const [yaml, linter, , , stateExtensions] = yamlSchema(schema);
return [
yaml,
linter,
autocompletion({
icons: false,
activateOnTypingDelay: 300,
selectOnOpen: true,
activateOnTyping: true,
override: [
(ctx) => {
const getCompletions = yamlCompletion();
const completions = getCompletions(ctx);
if (Array.isArray(completions)) {
return null;
}
return completions;
},
],
}),
stateExtensions,
yamlIndentExtension,
syntaxHighlighting(oneDarkHighlightStyle),
lintGutter(),
keymap.of([...defaultKeymap, ...completionKeymap, ...lintKeymap]),
];
}
export function CodeEditor({
id,
onChange,
@ -79,17 +144,22 @@ export function CodeEditor({
onVersionChange,
height = '500px',
type,
schema,
'data-cy': dataCy,
}: Props) {
const [isRollback, setIsRollback] = useState(false);
const extensions = useMemo(() => {
const extensions = [];
if (type && docTypeExtensionMap[type]) {
extensions.push(docTypeExtensionMap[type]);
if (!type || !docTypeExtensionMap[type]) {
return [];
}
return extensions;
}, [type]);
// YAML-specific schema validation
if (schema && type === 'yaml') {
return schemaValidationExtensions(schema);
}
// Default language support
return [docTypeExtensionMap[type]];
}, [type, schema]);
const handleVersionChange = useCallback(
(version: number) => {
@ -146,7 +216,7 @@ export function CodeEditor({
height={height}
basicSetup={{
highlightSelectionMatches: false,
autocompletion: false,
autocompletion: !!schema,
}}
data-cy={dataCy}
/>