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:
parent
0ebfe047d1
commit
81c5f4acc3
27 changed files with 2046 additions and 36 deletions
|
@ -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}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue