Skip to content

Commit 701753e

Browse files
feat(nimbus): use fml linter in branches form (#13826)
Becuase * In the old nimbus-ui, we called out to an API to do FML validation for non desktop feature values * This was lost when we setup the new nimbus-ui This commit * Adds a linter to the new feature values form that calls the FML linting API fixes #12968
1 parent 372224b commit 701753e

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

experimenter/experimenter/nimbus_ui/forms.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,22 @@ def __init__(self, *args, **kwargs):
445445
):
446446
self.fields["value"].initial = ""
447447

448+
if (
449+
self.instance is not None
450+
and self.instance.branch_id is not None
451+
and self.instance.branch.experiment
452+
and self.instance.branch.experiment.application
453+
!= NimbusExperiment.Application.DESKTOP
454+
):
455+
self.fields["value"].widget.attrs["data-experiment-slug"] = (
456+
self.instance.branch.experiment.slug
457+
)
458+
459+
if self.instance.feature_config:
460+
self.fields["value"].widget.attrs["data-feature-slug"] = (
461+
self.instance.feature_config.slug
462+
)
463+
448464
if (
449465
self.instance.id is not None
450466
and self.instance.feature_config

experimenter/experimenter/nimbus_ui/static/js/edit_branches.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from "@codemirror/search";
2525
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
2626
import { tags } from "@lezer/highlight";
27-
import { schemaAutocomplete, schemaLinter } from "./validator.js";
27+
import { schemaAutocomplete, schemaLinter, fmlLinter } from "./validator.js";
2828
import $ from "jquery";
2929

3030
const setupCodemirror = (selector, textarea, extraExtensions) => {
@@ -85,7 +85,27 @@ const setupCodemirrorFeatures = () => {
8585
textareas.forEach((textarea) => {
8686
const extensions = [];
8787

88-
if (textarea.dataset.schema) {
88+
const hasFmlValidation =
89+
textarea.dataset.experimentSlug && textarea.dataset.featureSlug;
90+
const hasJsonSchema = textarea.dataset.schema;
91+
92+
if (hasFmlValidation) {
93+
extensions.push(
94+
linter(
95+
fmlLinter(
96+
textarea.dataset.experimentSlug,
97+
textarea.dataset.featureSlug,
98+
),
99+
),
100+
);
101+
102+
if (hasJsonSchema) {
103+
const jsonSchema = JSON.parse(textarea.dataset.schema);
104+
extensions.push(
105+
autocompletion({ override: [schemaAutocomplete(jsonSchema)] }),
106+
);
107+
}
108+
} else if (hasJsonSchema) {
89109
const jsonSchema = JSON.parse(textarea.dataset.schema);
90110

91111
extensions.push(

experimenter/experimenter/nimbus_ui/static/js/validator.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,81 @@ export function schemaAutocomplete(schema) {
277277
};
278278
};
279279
}
280+
281+
/**
282+
* FML remote feature validation
283+
* Linter that calls out to the server to validate feature values against
284+
* FML rules.
285+
*/
286+
export const fmlLinter = (experimentSlug, featureSlug) => {
287+
let timeout = null;
288+
289+
return async (view) => {
290+
const doc = view.state.doc.toString();
291+
292+
if (timeout) {
293+
clearTimeout(timeout);
294+
}
295+
296+
if (!doc.trim()) {
297+
return [];
298+
}
299+
300+
try {
301+
JSON.parse(doc);
302+
} catch (e) {
303+
return [];
304+
}
305+
306+
return new Promise((resolve) => {
307+
timeout = setTimeout(async () => {
308+
try {
309+
const response = await fetch(
310+
`/api/v5/fml-errors/${experimentSlug}/`,
311+
{
312+
method: "PATCH",
313+
headers: {
314+
"Content-Type": "application/json",
315+
"X-CSRFToken": document.querySelector(
316+
"[name=csrfmiddlewaretoken]",
317+
).value,
318+
},
319+
body: JSON.stringify({
320+
featureSlug: featureSlug,
321+
featureValue: doc,
322+
}),
323+
},
324+
);
325+
326+
if (!response.ok) {
327+
resolve([]);
328+
return;
329+
}
330+
331+
const errors = await response.json();
332+
333+
const diagnostics = errors.map((error) => {
334+
const line = Math.max(0, error.line - 1);
335+
const col = Math.max(0, error.col - 1);
336+
337+
const lineObj = view.state.doc.line(line + 1);
338+
const from = lineObj.from + col;
339+
const to = from + (error.highlight?.length || 1);
340+
341+
return {
342+
from,
343+
to,
344+
severity: "error",
345+
message: error.message,
346+
};
347+
});
348+
349+
resolve(diagnostics);
350+
} catch (error) {
351+
console.error("FML validation error:", error);
352+
resolve([]);
353+
}
354+
}, 500);
355+
});
356+
};
357+
};

0 commit comments

Comments
 (0)