Commit 5b91cf71 authored by Illya Klymov's avatar Illya Klymov

Merge branch '226982-custom-schemas-frontend' into 'master'

[FE] Support custom JSON schema validation in the Web IDE

See merge request gitlab-org/gitlab!41700
parents 8d28e24b 7cc7c779
......@@ -20,6 +20,7 @@ const Api = {
projectPath: '/api/:version/projects/:id',
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
projectUsersPath: '/api/:version/projects/:id/users',
projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests',
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
......
......@@ -14,7 +14,7 @@ import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getPathParent, readFileAsDataURL } from '../utils';
import { getPathParent, readFileAsDataURL, registerSchema } from '../utils';
import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
......@@ -56,6 +56,7 @@ export default {
'isEditModeActive',
'isCommitModeActive',
'currentBranch',
'getJsonSchemaForPath',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
......@@ -197,6 +198,8 @@ export default {
this.editor.clearEditor();
this.registerSchemaForFile();
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => {
this.createEditorInstance();
......@@ -330,6 +333,10 @@ export default {
// do nothing if no image is found in the clipboard
return Promise.resolve();
},
registerSchemaForFile() {
const schema = this.getJsonSchemaForPath(this.file.path);
registerSchema(schema);
},
},
viewerTypes,
FILE_VIEW_MODE_EDITOR,
......
......@@ -7,10 +7,9 @@ import ModelManager from './common/model_manager';
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
import { themes } from './themes';
import languages from './languages';
import schemas from './schemas';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
import { registerLanguages, registerSchemas } from '../utils';
import { registerLanguages } from '../utils';
function setupThemes() {
themes.forEach(theme => {
......@@ -46,10 +45,6 @@ export default class Editor {
setupThemes();
registerLanguages(...languages);
if (gon.features?.schemaLinting) {
registerSchemas(...schemas);
}
this.debouncedUpdate = debounce(() => {
this.updateDimensions();
}, 200);
......
import json from './json';
import yaml from './yaml';
export default [json, yaml];
export default {
language: 'json',
options: {
validate: true,
enableSchemaRequest: true,
schemas: [],
},
};
export default {
uri: 'https://json.schemastore.org/gitlab-ci',
fileMatch: ['*.gitlab-ci.yml'],
};
import gitlabCi from './gitlab_ci';
export default {
language: 'yaml',
options: {
validate: true,
enableSchemaRequest: true,
hover: true,
completion: true,
schemas: [gitlabCi],
},
};
......@@ -6,6 +6,7 @@ import {
PERMISSION_CREATE_MR,
PERMISSION_PUSH_CODE,
} from '../constants';
import Api from '~/api';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
......@@ -177,3 +178,18 @@ export const getAvailableFileName = (state, getters) => path => {
export const getUrlForPath = state => path =>
`/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`;
export const getJsonSchemaForPath = (state, getters) => path => {
const [namespace, ...project] = state.currentProjectId.split('/');
return {
uri:
// eslint-disable-next-line no-restricted-globals
location.origin +
Api.buildUrl(Api.projectFileSchemaPath)
.replace(':namespace_path', namespace)
.replace(':project_path', project.join('/'))
.replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId)
.replace(':filename', path),
fileMatch: [`*${path}`],
};
};
......@@ -75,17 +75,17 @@ export function registerLanguages(def, ...defs) {
languages.setLanguageConfiguration(languageId, def.conf);
}
export function registerSchemas({ language, options }, ...schemas) {
schemas.forEach(schema => registerSchemas(schema));
const defaults = {
json: languages.json.jsonDefaults,
yaml: languages.yaml.yamlDefaults,
};
if (defaults[language]) {
defaults[language].setDiagnosticsOptions(options);
}
export function registerSchema(schema) {
const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
defaults.forEach(d =>
d.setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
hover: true,
completion: true,
schemas: [schema],
}),
);
}
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
......
---
title: Support custom JSON schema validation in the Web IDE
merge_request: 41700
author:
type: added
......@@ -202,28 +202,6 @@ describe('Multi-file editor library', () => {
});
});
describe('schemas', () => {
let originalGon;
beforeEach(() => {
originalGon = window.gon;
window.gon = { features: { schemaLinting: true } };
delete Editor.editorInstance;
instance = Editor.create();
});
afterEach(() => {
window.gon = originalGon;
});
it('registers custom schemas defined with Monaco', () => {
expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({
schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }],
});
});
});
describe('replaceSelectedText', () => {
let model;
let editor;
......
import { TEST_HOST } from 'helpers/test_constants';
import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
......@@ -493,4 +494,37 @@ describe('IDE store getters', () => {
);
});
});
describe('getJsonSchemaForPath', () => {
beforeEach(() => {
localState.currentProjectId = 'path/to/some/project';
localState.currentBranchId = 'master';
});
it('returns a json schema uri and match config for a json/yaml file that can be loaded by monaco', () => {
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
fileMatch: ['*.gitlab-ci.yml'],
uri: `${TEST_HOST}/path/to/some/project/-/schema/master/.gitlab-ci.yml`,
});
});
it('returns a path containing sha if branch details are present in state', () => {
localState.projects['path/to/some/project'] = {
name: 'project',
branches: {
master: {
name: 'master',
commit: {
id: 'abcdef123456',
},
},
},
};
expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({
fileMatch: ['*.gitlab-ci.yml'],
uri: `${TEST_HOST}/path/to/some/project/-/schema/abcdef123456/.gitlab-ci.yml`,
});
});
});
});
......@@ -2,7 +2,7 @@ import { languages } from 'monaco-editor';
import {
isTextFile,
registerLanguages,
registerSchemas,
registerSchema,
trimPathComponents,
insertFinalNewline,
trimTrailingWhitespace,
......@@ -159,17 +159,11 @@ describe('WebIDE utils', () => {
});
});
describe('registerSchemas', () => {
let options;
describe('registerSchema', () => {
let schema;
beforeEach(() => {
options = {
validate: true,
enableSchemaRequest: true,
hover: true,
completion: true,
schemas: [
{
schema = {
uri: 'http://myserver/foo-schema.json',
fileMatch: ['*'],
schema: {
......@@ -180,35 +174,23 @@ describe('WebIDE utils', () => {
p2: { $ref: 'http://myserver/bar-schema.json' },
},
},
},
{
uri: 'http://myserver/bar-schema.json',
schema: {
id: 'http://myserver/bar-schema.json',
type: 'object',
properties: { q1: { enum: ['x1', 'x2'] } },
},
},
],
};
jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
});
it.each`
language | defaultsObj
${'json'} | ${languages.json.jsonDefaults}
${'yaml'} | ${languages.yaml.yamlDefaults}
`(
'registers the given schemas with monaco for lang: $language',
({ language, defaultsObj }) => {
registerSchemas({ language, options });
it('registers the given schemas with monaco for both json and yaml languages', () => {
registerSchema(schema);
expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options);
},
expect(languages.json.jsonDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
expect.objectContaining({ schemas: [schema] }),
);
expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
expect.objectContaining({ schemas: [schema] }),
);
});
});
describe('trimTrailingWhitespace', () => {
it.each`
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment