From 6d1c72e9c2100bb2b269019381c956d745019c0e Mon Sep 17 00:00:00 2001 From: "christopher.tubbs" Date: Fri, 28 Jul 2023 08:12:26 -0500 Subject: [PATCH] Added a new JSON editor widget and attached it to the admin pages for Specification Templates and Evaluation Definitions --- .../specification/test_deserialization.py | 6 +- .../evaluation_service/admin.py | 141 +++- .../evaluation_service/forms/__init__.py | 6 + .../evaluation_service/forms/json.py | 96 +++ ...ficationtemplate_template_configuration.py | 18 + .../evaluation_service/models.py | 3 +- .../js/template_specification.js | 77 ++ .../evaluation_service/urls.py | 3 +- .../evaluation_service/views/__init__.py | 1 + .../evaluation_service/views/helpers.py | 44 + .../evaluationservice/service/settings.py | 2 + .../static/css/widgets/jsonarea.css | 23 + .../static/js/widgets/jsonarea.js | 472 +++++++++++ .../jsoneditor/img/jsoneditor-icons.svg | 749 ++++++++++++++++++ .../static/jsoneditor/jsoneditor.min.css | 6 + .../static/jsoneditor/jsoneditor.min.js | 86 ++ .../specificationtemplate/change_form.html | 189 ----- .../templates/widgets/jsonarea.html | 198 +++++ .../evaluationservice/widgets/__init__.py | 6 + .../evaluationservice/widgets/jsonarea.py | 131 +++ 20 files changed, 2059 insertions(+), 198 deletions(-) create mode 100644 python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/__init__.py create mode 100644 python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/json.py create mode 100644 python/services/evaluationservice/dmod/evaluationservice/evaluation_service/migrations/0004_alter_specificationtemplate_template_configuration.py create mode 100644 python/services/evaluationservice/dmod/evaluationservice/evaluation_service/static/evaluation_service/js/template_specification.js create mode 100644 python/services/evaluationservice/dmod/evaluationservice/static/css/widgets/jsonarea.css create mode 100644 python/services/evaluationservice/dmod/evaluationservice/static/js/widgets/jsonarea.js create mode 100755 python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/img/jsoneditor-icons.svg create mode 100644 python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.css create mode 100644 python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.js delete mode 100644 python/services/evaluationservice/dmod/evaluationservice/templates/admin/evaluation_service/specificationtemplate/change_form.html create mode 100644 python/services/evaluationservice/dmod/evaluationservice/templates/widgets/jsonarea.html create mode 100644 python/services/evaluationservice/dmod/evaluationservice/widgets/__init__.py create mode 100644 python/services/evaluationservice/dmod/evaluationservice/widgets/jsonarea.py diff --git a/python/lib/evaluations/dmod/test/specification/test_deserialization.py b/python/lib/evaluations/dmod/test/specification/test_deserialization.py index b161d24ed..de6b0a17c 100644 --- a/python/lib/evaluations/dmod/test/specification/test_deserialization.py +++ b/python/lib/evaluations/dmod/test/specification/test_deserialization.py @@ -1,11 +1,8 @@ import os.path import unittest import json -import pathlib -import typing from ...evaluations import specification -from ..common import ConstructionTest from ..common import RESOURCE_DIRECTORY @@ -53,6 +50,9 @@ def test_multitemplate(self): self.assertEqual(single_instance, pure_template_instance) def test_evaluation_deserialization(self): + from pprint import pprint + + pprint(dir(specification.EvaluationSpecification)) normal_specification = self.template_manager.get_template( specification_type=specification.EvaluationSpecification, name="no-template" diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/admin.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/admin.py index 957c7432d..27a340183 100644 --- a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/admin.py +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/admin.py @@ -3,11 +3,148 @@ from . import models +from evaluation_service.forms import EvaluationDefinitionForm +from evaluation_service.forms import SpecificationTemplateForm +@admin.register(models.EvaluationDefinition) +class EvaluationDefinitionAdmin(admin.ModelAdmin): + """ + A dedicated view to edit a Specification class instance + """ + form = EvaluationDefinitionForm + + # Display these fields on the list screen + list_display = ('name', 'author', 'description', "last_edited") + + # Regardless of what fields are displayed on the screen, + # we want to be able to enter the editor by clicking on the template_name + list_display_links = ('name',) + + def get_list_display(self, request: HttpRequest) -> (str,): + """ + Gets the fields that may be shown as columns on the list screen + + :param HttpRequest request: The request that asked for the columns + :rtype: (str,) + :return: The fields to display on the list screen + """ + # Get the master list of columns to show + list_display = super().get_list_display(request) + + # If the user is a superuser, they'll see DataSources belonging to all users; + # tack the author's name to it so that superusers may know whose DataSources they are editing + if request.user.is_superuser: + list_display = ["author"] + list(list_display) + + return list_display + + def get_list_filter(self, request: HttpRequest) -> (str,): + """ + Gets the list of fields that elements may be filtered by. This will show a box on the side of the + screen where a user may click a link that might limit all elements displayed on the screen to those + whose variables are 'flow' + + :param HttpRequest request: The request asking for which fields to allow a user to filter by + :rtype: (str,) + :return: A collection of all fields to filter by + """ + list_filter = super().get_list_filter(request) + + # If the user is a superuser, they will also be able to see the authors of the datasources. + # Allow the user to limit displayed items to those owned by specific users + if request.user.is_superuser: + list_filter = ["author"] + list(list_filter) + + return list_filter + + def get_readonly_fields(self, request: HttpRequest, obj: models.SpecificationTemplate = None) -> (str,): + """ + Determines which fields should not be available for editing + + :param HttpRequest request: The request asking which fields displayed on the screen should be read only + :param models.DataSource obj: The datasource whose fields may be displayed on the screen + :rtype: (str,) + :return: A collection of all fields that cannot be modified + """ + readonly_fields = super().get_readonly_fields(request, obj) + + # If a user is a superuser, they will see the name of the author. + # In order to prevent the user from changing that, we set it as read-only + if request.user.is_superuser: + readonly_fields = tuple(['author'] + list(readonly_fields)) + + return readonly_fields + + def save_model(self, request, obj, form, change): + """ + Attaches the user to the object and calls the parent save method + + :type request: HttpRequest + :param request: The web request + :type obj: DataSource + :param obj: The object that is to be saved + :param form: The form that called the save function + :param change: The change to the object + :return: The result of the parent class's save_model function + """ + + # If the object doesn't have an author, make the current user + if obj.author is None: + if request.user.first_name and request.user.last_name: + name = f"{request.user.first_name} {request.user.last_name}" + elif request.user.first_name: + name = request.user.first_name + elif request.user.last_name: + name = request.user.last_name + else: + name = request.user.username + obj.author = name + + super().save_model(request, obj, form, change) + + def get_form(self, request, obj=None, **kwargs): + """ + Gets the custom form used for validation for the data source being edited + + :param request: The request that asked for the form + :param obj: The object to edit + :param kwargs: Keyword arguments being passed down the line + :return: The form that will be used to validate the values configured for the data source + """ + + # First perform what it would have done prior. It's complicated logic and we can't do better + form = super().get_form(request, obj, **kwargs) + + if form and hasattr(form, "editor"): + # All we want to do is attach the user, so we go ahead and do that now + form.editor = request.user + + return form + + def get_queryset(self, request): + """ + Obtains a set of the appropriate data source configurations to load into the list view + + :type request: HttpRequest + :param request: The http request that told the application to load data source configurations into the list + :rtype: QuerySet[DataSource] + :return: A QuerySet containing all DataSource objects that may be edited. + """ + qs = super().get_queryset(request) + + # If the user isn't a superuser, only the user's DataSource objects will be returned + if not request.user.is_superuser: + qs = qs.filter(author=request.user) + + return qs + + +@admin.register(models.SpecificationTemplate) class SpecificationTemplateAdmin(admin.ModelAdmin): """ A dedicated view to edit a Specification class instance """ + form = SpecificationTemplateForm fields = ( "template_specification_type", @@ -138,6 +275,4 @@ def get_queryset(self, request): # Register your models here. -admin.site.register(models.StoredDataset) -admin.site.register(models.EvaluationDefinition) -admin.site.register(models.SpecificationTemplate, SpecificationTemplateAdmin) \ No newline at end of file +admin.site.register(models.StoredDataset) \ No newline at end of file diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/__init__.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/__init__.py new file mode 100644 index 000000000..5d726a3c4 --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/__init__.py @@ -0,0 +1,6 @@ +""" +All custom forms +""" + +from .json import EvaluationDefinitionForm +from .json import SpecificationTemplateForm diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/json.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/json.py new file mode 100644 index 000000000..8c55e7778 --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/forms/json.py @@ -0,0 +1,96 @@ +""" +Definitions for forms that provide advanced handling for json data +""" +from __future__ import annotations + +import typing +import json +import re + +from pydantic import BaseModel + +from django import forms + +from widgets import JSONArea + +from dmod.evaluations import specification +from dmod.evaluations.specification.base import get_subclasses + +from evaluation_service import models + +BINARY_TYPES = re.compile( + r'((? typing.Optional[dict]: + """ + Get the schema for a model and scrub any editor unfriendly type from it + + An example of an editor unfriendly type is a string in a binary format + + Args: + model: The model whose schema to retrieve + + Returns: + A schema for the model if it is available + """ + if hasattr(model, "schema_json"): + json_data = model.schema_json() + + json_data = BINARY_TYPES.sub("", json_data) + + return json.loads(json_data) + return None + +def get_specification_schema_map() -> typing.Dict[str, typing.Any]: + """ + Generate a dictionary mapping specification types to their schemas + + Return: + A dictionary mapping the value of a specification type that will be on the template type selector to schema data that is compatible with the client side editor + """ + return { + specification_type.get_specification_type(): get_editor_friendly_model_schema(specification_type) + for specification_type in get_subclasses(specification.TemplatedSpecification) + } + + +class EvaluationDefinitionForm(forms.ModelForm): + """ + A specialized form for EvaluationDefinition that allows its JSON data to be manipulated within a JSONArea + """ + class Meta: + model = models.EvaluationDefinition + fields = "__all__" + + definition = forms.JSONField( + widget=JSONArea( + schema=get_editor_friendly_model_schema(specification.EvaluationSpecification) + ) + ) + +class SpecificationTemplateForm(forms.ModelForm): + """ + A specialized form for SpecificationTemplate that allows its JSON data to be manipulated within a JSONArea + """ + class Meta: + model = models.SpecificationTemplate + fields = "__all__" + + class Media: + # Include a script to add functionality that will update the json area's + # schema when the template type is changed + js = [ + "evaluation_service/js/template_specification.js" + ] + + template_configuration = forms.JSONField( + widget=JSONArea( + extra_data={ + "schemas": get_specification_schema_map() + } + ) + ) \ No newline at end of file diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/migrations/0004_alter_specificationtemplate_template_configuration.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/migrations/0004_alter_specificationtemplate_template_configuration.py new file mode 100644 index 000000000..8c66a3d0c --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/migrations/0004_alter_specificationtemplate_template_configuration.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.3 on 2023-07-24 19:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation_service', '0003_specificationtemplate_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='specificationtemplate', + name='template_configuration', + field=models.JSONField(help_text='The configuration that should be applied to a given specification type'), + ), + ] diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/models.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/models.py index 47eff2022..3470462c0 100644 --- a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/models.py +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/models.py @@ -29,8 +29,7 @@ class Meta: choices=get_specification_options(), help_text="The type of specification that this template pertains to" ) - template_configuration = models.CharField( - max_length=30000, + template_configuration = models.JSONField( help_text="The configuration that should be applied to a given specification type" ) template_description = models.CharField( diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/static/evaluation_service/js/template_specification.js b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/static/evaluation_service/js/template_specification.js new file mode 100644 index 000000000..eaa13130b --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/static/evaluation_service/js/template_specification.js @@ -0,0 +1,77 @@ +/** + * Supplies custom functions and handling for the SpecificationTemplateForm + */ + +/** + * Handler for when the template type selector has changed values + */ +function templateTypeChanged() { + changeConfigurationSchema(this); +} + +/** + * Update the JSON editor with the selected schema + * + * @param {HTMLElement?} selector A select element stating what schema to use + */ +function changeConfigurationSchema(selector) { + let newSchema = null; + const editorData = getEditorData("template_configuration"); + + if (selector === null || selector === undefined) { + selector = django.jQuery("select[name=template_specification_type]")[0]; + } else if (selector instanceof django.jQuery) { + selector = selector[0]; + } + + if (!(selector.hasOwnProperty("value") || "value" in selector)) { + return; + } + + if (editorData !== null && "schemas" in editorData && selector.value in editorData.schemas) { + newSchema = editorData.schemas[selector.value]; + } + + if (newSchema === null || newSchema === undefined) { + return; + } + + const editor = getEditor("template_configuration"); + + if (editor) { + editor.setSchema(newSchema); + + let currentText = editor.getText(); + + if (currentText === null) { + currentText = ""; + } else { + currentText = currentText.trim(); + } + + if (currentText.length === 0 || currentText.match(/^\{?\s*}?$/)) { + const newData = buildObjectFromSchema(newSchema); + editor.set(newData); + } + } +} + +/** + * Attach the `templateTypeChanged` function to the 'change' event for the template specification type selector + */ +function attachSpecificationTypeChanged() { + const selector = django.jQuery("select[name=template_specification_type]"); + selector.on("change", templateTypeChanged); +} + +/** + * Make sure that the change handler for the template type selector is attached and + * that the proper schema is attached to the editor once the page is done loading + */ +document.addEventListener( + "DOMContentLoaded", + function() { + attachSpecificationTypeChanged(); + changeConfigurationSchema(); + } +); \ No newline at end of file diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/urls.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/urls.py index 5f2b572bb..3715a9587 100644 --- a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/urls.py +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/urls.py @@ -29,5 +29,6 @@ re_path(f"metrics/?$", views.Metrics.as_view(), name="Metrics"), re_path(views.GetLibraryOptions.route(), views.GetLibraryOptions.as_view(), name="LibraryOptions"), re_path(views.GetLibrary.route(), views.GetLibrary.as_view(), name="GetLibrary"), - re_path(r"library/select/?$", views.LibrarySelector.as_view(), name="SelectLibrary") + re_path(r"library/select/?$", views.LibrarySelector.as_view(), name="SelectLibrary"), + re_path(r"schema/?$", views.Schema.as_view(), name="Schema"), ] diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/__init__.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/__init__.py index 680efab4d..e3a39e059 100644 --- a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/__init__.py +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/__init__.py @@ -7,6 +7,7 @@ from .launch import ReadyListenEvaluation from .helpers import Clean from .helpers import Metrics +from .helpers import Schema from .geometry import GetGeometry from .geometry import GetGeometryDatasets from .library import GetLibrary diff --git a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/helpers.py b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/helpers.py index 909c8c88b..774c705a7 100644 --- a/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/helpers.py +++ b/python/services/evaluationservice/dmod/evaluationservice/evaluation_service/views/helpers.py @@ -5,6 +5,8 @@ import json from wsgiref.util import FileWrapper + +from django.http import QueryDict from django.views.generic import View from django.http import HttpResponse @@ -15,6 +17,8 @@ from rest_framework.views import APIView import dmod.evaluations.util as evaluation_utilities +import dmod.evaluations.specification as specification +from dmod.evaluations.specification.base import get_subclasses import dmod.metrics.metric as metrics import dmod.metrics.scoring as scoring @@ -185,6 +189,46 @@ def get(self, request: HttpRequest, evaluation_name: str): return response +class Schema(APIView): + """ + View that returns the full JSON schema for evaluation elements + """ + @classmethod + def _get_schema(cls, query: QueryDict) -> typing.Union[typing.Dict[str, typing.Any], str]: + model = specification.EvaluationSpecification + if "model" in query: + model_name: str = query['model'] + matching_models = [ + model + for model in get_subclasses(specification.Specification) + if model.get_specification_type().lower() == model_name.lower() + ] + + if matching_models: + model = matching_models[0] + else: + return f"There are no models by the name of '{model_name}'" + + schema = model.schema() + return schema + + def get(self, request: HttpRequest) -> typing.Union[JsonResponse, HttpResponse]: + schema = self._get_schema(request.GET) + + if isinstance(schema, str): + return HttpResponseBadRequest(schema) + + return JsonResponse(schema) + + def post(self, request: HttpRequest) -> typing.Union[JsonResponse, HttpResponse]: + schema = self._get_schema(request.POST) + + if isinstance(schema, str): + return HttpResponseBadRequest(schema) + + return JsonResponse(schema) + + class Metrics(APIView): """ View that returns all metadata for all available metrics diff --git a/python/services/evaluationservice/dmod/evaluationservice/service/settings.py b/python/services/evaluationservice/dmod/evaluationservice/service/settings.py index eb91db647..278ed61c7 100644 --- a/python/services/evaluationservice/dmod/evaluationservice/service/settings.py +++ b/python/services/evaluationservice/dmod/evaluationservice/service/settings.py @@ -34,6 +34,7 @@ INSTALLED_APPS = [ 'channels', + 'django.forms', 'evaluation_service.apps.EvaluationServiceConfig', 'django.contrib.admin', 'django.contrib.auth', @@ -73,6 +74,7 @@ ] ASGI_APPLICATION = 'service.asgi.application' +FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' # Password validation diff --git a/python/services/evaluationservice/dmod/evaluationservice/static/css/widgets/jsonarea.css b/python/services/evaluationservice/dmod/evaluationservice/static/css/widgets/jsonarea.css new file mode 100644 index 000000000..cc1afb20b --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/static/css/widgets/jsonarea.css @@ -0,0 +1,23 @@ + +.jsoneditor-error-box { + background-color: rgb(255, 172, 172); + padding: 10px; + border: 1px solid black; +} + +.jsoneditor-error-list li { + margin-left: 40px; +} + +.jsoneditor-error-list li::marker { + font-weight: bold; +} + +.jsoneditor { + border: thin solid var(--breadcrumbs-bg); +} + +.jsoneditor div.jsoneditor-menu { + background-color: var(--breadcrumbs-bg); + border-bottom: var(--breadcrumbs-bg); +} \ No newline at end of file diff --git a/python/services/evaluationservice/dmod/evaluationservice/static/js/widgets/jsonarea.js b/python/services/evaluationservice/dmod/evaluationservice/static/js/widgets/jsonarea.js new file mode 100644 index 000000000..74b6bdab6 --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/static/js/widgets/jsonarea.js @@ -0,0 +1,472 @@ +/** + * Fetches the root editor namespace + * + * A namespace is created if it doesn't exist + * + * @param {(string|string[])?} path The path to namespace to make sure it exists + * @param {string?} separator A character that may be used to separate parts of a path. Defaults to '/' + * @return {object} The desired namespace + */ +function ensureNamespaceExists(path, separator) { + let namespace = window; + + if (separator === null || separator === undefined) { + separator = "/"; + } else if (typeof(separator) !== 'string') { + throw new Error(`An object of type "${typeof(separator)}" may not be used as a path separator`); + } + + if (path === null || path === undefined) { + path = ['editors']; + } else if (typeof(path) === "string") { + path = path.trim().split(separator) + } else if (!Array.isArray(path)) { + throw new Error(`A "${typeof(path)}" object cannot be used to find and or create a namespace`); + } + + let firstPart = true; + for (let part of path) { + if (firstPart && ["#", "$"].includes(part)) { + continue; + } + + firstPart = false; + + if (part === '' || part === null || part === undefined) { + continue + } + + if (namespace.hasOwnProperty(part)) { + namespace = namespace[part]; + } else { + namespace[part] = {}; + namespace = namespace[part]; + } + } + + return namespace; +} + +/** + * Get data associated with a JSON editor + * + * @param {string} editorName The name of the field the editor edits + * @param {(string|string[])?} path The path to the desired namespace + * @param {string?} separator A character to use to separate parts of the given path. Defaults to '/' + * @return {object|null} + */ +function getEditorData(editorName, path, separator) { + const namespace = ensureNamespaceExists(path, separator) + + if (editorName in namespace && 'data' in namespace[editorName]) { + return namespace[editorName]['data']; + } + + return null; +} + +/** + * Get the actual editor object associated with an editor name + * + * @param {string} editorName The name of the editor To find + * @param {(string|string[])?} path The path to the desired namespace + * @param {string?} separator A character that may be used to separate parts of a path. Defaults to '/' + * @return {object|null} + */ +function getEditor(editorName, path, separator) { + const namespace = ensureNamespaceExists(path, separator); + + if (editorName in namespace && "editor" in namespace[editorName]) { + return namespace[editorName].editor; + } + + return null; +} + +/** + * Assign an event handler to a DOM element with nothing but strings + * @param {string} selector A CSS selector used to find an element to attach an event handler to + * @param {string} eventName The event to handle + * @param {string|string[]} functionName The name or path to a function to act as a handler for the element's event + */ +function assignEventHandler(selector, eventName, functionName) { + if ($ === undefined) { + $ = django.jQuery; + } + + let func; + + if (functionName in window) { + func = window[functionName]; + } else if (typeof(functionName) === "string" && functionName.includes(".")) { + functionName = functionName.split("."); + } else if (!Array.isArray(functionName)) { + functionName = [functionName] + } + + if (func === undefined) { + let space = window; + + for (let part of functionName) { + if (part in space || space.includes(part)) { + space = space[part]; + } else { + throw new Error( + `No event handler could be found at "${functionName.join('.')}". ` + + `A handler cannot be attached to the "${eventName}" event.` + ); + } + + if (!["function", "object"].includes(typeof(space))) { + throw new Error( + `"${functionName.join('.')}" does not lead to a function or indexible object. ` + + `The '${eventName}' event may not be handled.` + ); + } + } + + if (typeof(space) !== 'function') { + throw new Error( + `"${functionName.join('.')}" is not a function and may not be used as an event handler. ` + + `A handler cannot be attached to the "${eventName}" event.` + ); + } + + func = space; + } + + $(selector).on(eventName, func); +} + + +/** + * Enable all save buttons + */ +function enableSave() { + const saveButtons = django.jQuery("input[value*=Save]"); + saveButtons.prop("disabled", false); +} + +/** + * Disable all Save Buttons + */ +function disableSave() { + const saveButtons = django.jQuery("input[value*=Save]"); + saveButtons.prop("disabled", true); +} + +/** + * Find a child within an object given a path + * + * @param {object} obj The object to traverse + * @param {string|string[]} path The path to the desired value + * @param {boolean?} allowMissingEntry Whether to record a missing path entry as an error or to throw an error + * @param {string?} separator A character that may be used to separate parts of a path. Defaults to '/' + * @return {*} + */ +function followSchemaPath(obj, path, allowMissingEntry, separator) { + let currentObject = obj; + + if (separator === null || separator === undefined) { + separator = "/"; + } else if (typeof(separator) !== 'string') { + throw new Error(`An object of type "${typeof(separator)}" may not be used as a path separator`); + } + + if (allowMissingEntry === null || allowMissingEntry === undefined) { + allowMissingEntry = false; + } + + if (!Array.isArray(path)) { + path = path.split(separator) + } + + for (let part of path) { + if (part === "#" || part === "$" || part.length === 0) { + continue; + } + + if (currentObject.hasOwnProperty(part) || part in currentObject) { + currentObject = currentObject[part]; + } else { + const message = `"${path.join('/')}" cannot be used to find data within a given object.`; + if (allowMissingEntry) { + console.error(message); + return {}; + } else { + throw new Error(message); + } + } + } + + return currentObject; +} + +/** + * Look at received data to see how it should be interpreted from the point of view of a schema + * + * @param {object} fieldInformation Details on what type of value should be generated + * @param {object} definitionsRoot An object that may contain definitions for nested and defined objects + * @param {boolean?} allowMissingEntry Whether to record a missing path entry as an error or to throw an error + * @return {(object|string|Array|Number|null)} A value generated that matches the requirements from the fieldInformation + */ +function getValueFromFieldInformation(fieldInformation, definitionsRoot, allowMissingEntry) { + let value = null; + + if (allowMissingEntry === null || allowMissingEntry === undefined) { + allowMissingEntry = false; + } + + if (fieldInformation.hasOwnProperty("default")) { + value = fieldInformation.default; + } else if (fieldInformation.hasOwnProperty("enum") && fieldInformation.enum.length > 0) { + value = fieldInformation.enum[0]; + } else { + switch (fieldInformation.type) { + case "array": + value = []; + if (fieldInformation.hasOwnProperty("items") && fieldInformation.items.hasOwnProperty("$ref")) { + value.push( + buildObjectFromSchema( + followSchemaPath( + definitionsRoot, + fieldInformation.items.$ref, + allowMissingEntry + ), + definitionsRoot, + allowMissingEntry + ) + ) + } + break; + case "string": + if (fieldInformation.hasOwnProperty("description")) { + value = fieldInformation.description; + } else if (fieldInformation.hasOwnProperty("title")) { + value = fieldInformation.title; + } else { + value = "string"; + } + break; + case 'number': + // Just pick any number if it's supposed to be one + value = 0; + break; + case 'object': + // If it's supposed to be an object with no expectations, just add an empty object + value = {}; + break; + case "boolean": + // Just pick any boolean value if it's supposed to be one + value = false; + break; + case undefined: + if (fieldInformation.hasOwnProperty("allOf")) { + value = []; + for (let subtype of fieldInformation.allOf) { + let data = buildObjectFromSchema( + followSchemaPath( + definitionsRoot, + subtype.$ref, + allowMissingEntry + ), + definitionsRoot + ) + value.push(data); + } + + if (value.length === 0) { + value = null; + } else if (value.length === 1) { + value = value[0]; + } + break; + } else if (fieldInformation.hasOwnProperty("anyOf")) { + let subInformation = fieldInformation.anyOf[0]; + value = getValueFromFieldInformation(subInformation, definitionsRoot); + } + } + } + + return value; +} + +/** + * Create an object based on a schema + * + * @param {object} schema + * @param {object?} definitionsRoot + * @param {boolean?} allowMissingEntry Whether to record a missing path entry as an error or to throw an error + * @return {object} + */ +function buildObjectFromSchema(schema, definitionsRoot, allowMissingEntry) { + if (schema === null) { + return {} + } + + if (definitionsRoot === null || definitionsRoot === undefined) { + definitionsRoot = schema; + allowMissingEntry = true; + } + + if (allowMissingEntry === null || allowMissingEntry === undefined) { + allowMissingEntry = false; + } + + let baseObject = {}; + + // Fields in the schema will be under the 'properties' key. Iterate through and create a value for each + // property to show an example for what is optional + if (schema.hasOwnProperty("properties")) { + for (let nameAndField of Object.entries(schema.properties)) { + let fieldName = nameAndField[0]; + let field = nameAndField[1]; + + let value = null; + try { + value = getValueFromFieldInformation(field, definitionsRoot, allowMissingEntry); + } catch (e) { + console.error(e); + } + baseObject[fieldName] = value; + } + } + + // If the generated value is an object, strip off any attribute whose value is null. That isn't a helpful value + if (typeof(baseObject) === 'object') { + baseObject = Object.fromEntries( + Object.entries(baseObject).filter( + ([key, value]) => value !== null + ) + ); + } + + return baseObject; +} + +/** + * Get the amount and the unit from an attribute that may be measured + * @param {string|HTMLElement} element The element that should have the attribute + * @param {string} attributeName The attribute to measure + * @param {(RegExp|string)?} unitRegex A regular expression that will indicate where the unit is so that the + * @return {{amount: number, unit: string}} + */ +function measureAttribute(element, attributeName, unitRegex) { + /** + * A regex used to separate the amount determining the size of an object and the unit it is measured in + */ + if (unitRegex === null || unitRegex === undefined) { + unitRegex = /[a-zA-Z]+/; + } else if (typeof(unitRegex) === 'string') { + unitRegex = new RegExp(unitRegex); + } + + if (typeof(element) == "string") { + element = django.jQuery(element); + } + + if (element === null || element === undefined || element.hasOwnProperty("length") && element.length === 0) { + throw new Error("No element could be found to take a measure of"); + } + + const attributeValue = element.css(attributeName); + + if (attributeValue === null || attributeValue === undefined || attributeValue === "") { + throw new Error(`No measurable attribute named '${attributeName}' could be found.`) + } else if (attributeValue.search(/^-?[0-9]+\.?[0-9]*([a-zA-Z]+)?$/) < 0) { + throw new Error(`"${attributeValue}" is not a measurable value`) + } + + const unitIndex = attributeValue.search(unitRegex); + + let amount; + let unit; + + if (unitIndex < 0) { + let valueIndex = attributeValue.search(/^-?[.0-9]+$/); + + if (valueIndex >= 0) { + amount = attributeValue * 1; + unit = ""; + } + else { + throw new Error(`'${attributeValue}' could not be considered as a measured attribute`); + } + } else { + amount = attributeValue.slice(0, unitIndex) * 1 + unit = attributeValue.slice(unitIndex) + } + + return { + amount: amount, + unit: unit + }; +} + +/** + * Subtract the amount in the attribute + * @param {{amount: number, unit: string}|number} accumulatedMeasurement The result of the subtracted measurements so far + * @param {{amount: number, unit: string}} measurement The measurement to subtract from the accumulated measurement + * @return {number} + */ +function subtractMeasurement(accumulatedMeasurement, measurement) { + if (typeof(accumulatedMeasurement) === 'object') { + return accumulatedMeasurement.amount - measurement.amount; + } + + return accumulatedMeasurement - measurement.amount; +} + +/** + * Determine what the width attribute for a given error box should be to make sure that the element on screen + * matches the width of the given editor + * + * @param {string|HTMLElement} editorElement The HTML element to use as the desired width. + * The element will be searched for if a string is passed + * @param {string|HTMLElement} errorBoxElement The HTML element that needs to be resized. + * The element will be searched for if a string is passed + * @param {(RegExp|string)?} unitRegex A regular expression used to separate an amount from a unit + * @return {string} The desired css width value given as "", like "500px" + */ +function getDesiredErrorBoxWidth(editorElement, errorBoxElement, unitRegex) { + if (typeof(editorElement) === 'string') { + editorElement = django.jQuery(editorElement); + } + + if (typeof(errorBoxElement) === 'string') { + errorBoxElement = django.jQuery(errorBoxElement); + } + + if (unitRegex === null || unitRegex === undefined) { + unitRegex = /[a-zA-Z]+/; + } else if (typeof(unitRegex) === 'string') { + unitRegex = new RegExp(unitRegex); + } + + let measurements = [ + measureAttribute(editorElement, "width") + ]; + + // The formula needs to look like "box + border left + border right + padding right + padding left = editor width" + // Since we don't know what the width of the box needs to be, get the widths of the other independent elements and + // subtract them from the editor width + const contributingErrorBoxFactors = [ + "border-left-width", + "border-right-width", + "padding-left", + "padding-right" + ]; + + // Add a measurement entry for each of the defined contributing factors to the list + measurements = measurements.concat( + contributingErrorBoxFactors.map( + (attributeName) => measureAttribute(errorBoxElement, attributeName, unitRegex) + ) + ) + + // Subtract each of the contributing factors from the expected width to find the desired width + const width = measurements.reduce(subtractMeasurement); + + // Attach the calculated amount to the editor's unit to find the css value to return + return width + measurements[0].unit; +} \ No newline at end of file diff --git a/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/img/jsoneditor-icons.svg b/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/img/jsoneditor-icons.svg new file mode 100755 index 000000000..c2c27658e --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/img/jsoneditor-icons.svg @@ -0,0 +1,749 @@ + + + JSON Editor Icons + + + + image/svg+xml + + JSON Editor Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.css b/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.css new file mode 100644 index 000000000..cd9d6357c --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.css @@ -0,0 +1,6 @@ +.jsoneditor input,.jsoneditor input:not([type]),.jsoneditor input[type=search],.jsoneditor input[type=text],.jsoneditor-modal input,.jsoneditor-modal input:not([type]),.jsoneditor-modal input[type=search],.jsoneditor-modal input[type=text]{height:auto;border:inherit;box-shadow:none;font-size:inherit;box-sizing:inherit;padding:inherit;font-family:inherit;transition:none;line-height:inherit}.jsoneditor input:focus,.jsoneditor input:not([type]):focus,.jsoneditor input[type=search]:focus,.jsoneditor input[type=text]:focus,.jsoneditor-modal input:focus,.jsoneditor-modal input:not([type]):focus,.jsoneditor-modal input[type=search]:focus,.jsoneditor-modal input[type=text]:focus{border:inherit;box-shadow:inherit}.jsoneditor textarea,.jsoneditor-modal textarea{height:inherit}.jsoneditor select,.jsoneditor-modal select{display:inherit;height:inherit}.jsoneditor label,.jsoneditor-modal label{font-size:inherit;font-weight:inherit;color:inherit}.jsoneditor table,.jsoneditor-modal table{border-collapse:collapse;width:auto}.jsoneditor td,.jsoneditor th,.jsoneditor-modal td,.jsoneditor-modal th{padding:0;display:table-cell;text-align:left;vertical-align:inherit;border-radius:inherit}.jsoneditor .autocomplete.dropdown{position:absolute;background:#fff;box-shadow:2px 2px 12px rgba(128,128,128,.3);border:1px solid #d3d3d3;overflow-x:hidden;overflow-y:auto;cursor:default;margin:0;padding:5px;text-align:left;outline:0;font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px}.jsoneditor .autocomplete.dropdown .item{color:#1a1a1a}.jsoneditor .autocomplete.dropdown .item.hover{background-color:#ebebeb}.jsoneditor .autocomplete.hint{color:#a1a1a1;top:4px;left:4px}.jsoneditor-contextmenu-root{position:relative;width:0;height:0}.jsoneditor-contextmenu{position:absolute;box-sizing:content-box;z-index:2}.jsoneditor-contextmenu .jsoneditor-menu{position:relative;left:0;top:0;width:128px;height:auto;background:#fff;border:1px solid #d3d3d3;box-shadow:2px 2px 12px rgba(128,128,128,.3);list-style:none;margin:0;padding:0}.jsoneditor-contextmenu .jsoneditor-menu button{position:relative;padding:0 8px 0 0;margin:0;width:128px;height:auto;border:none;cursor:pointer;color:#4d4d4d;background:0 0;font-size:14px;font-family:arial,sans-serif;box-sizing:border-box;text-align:left}.jsoneditor-contextmenu .jsoneditor-menu button::-moz-focus-inner{padding:0;border:0}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-default{width:96px}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-expand{float:right;width:32px;height:24px;border-left:1px solid #e5e5e5}.jsoneditor-contextmenu .jsoneditor-menu li{overflow:hidden}.jsoneditor-contextmenu .jsoneditor-menu li ul{display:none;position:relative;left:-10px;top:0;border:none;box-shadow:inset 0 0 10px rgba(128,128,128,.5);padding:0 10px;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.jsoneditor-contextmenu .jsoneditor-menu li ul .jsoneditor-icon{margin-left:24px}.jsoneditor-contextmenu .jsoneditor-menu li ul li button{padding-left:24px;animation:all ease-in-out 1s}.jsoneditor-contextmenu .jsoneditor-menu li button .jsoneditor-expand{position:absolute;top:0;right:0;width:24px;height:24px;padding:0;margin:0 4px 0 0;background-image:url(./img/jsoneditor-icons.svg);background-position:0 -72px}.jsoneditor-contextmenu .jsoneditor-icon{position:absolute;top:0;left:0;width:24px;height:24px;border:none;padding:0;margin:0;background-image:url(./img/jsoneditor-icons.svg)}.jsoneditor-contextmenu .jsoneditor-text{padding:4px 0 4px 24px;word-wrap:break-word}.jsoneditor-contextmenu .jsoneditor-text.jsoneditor-right-margin{padding-right:24px}.jsoneditor-contextmenu .jsoneditor-separator{height:0;border-top:1px solid #e5e5e5;padding-top:5px;margin-top:5px}.jsoneditor-contextmenu button.jsoneditor-remove .jsoneditor-icon{background-position:-24px 0}.jsoneditor-contextmenu button.jsoneditor-append .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-insert .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-duplicate .jsoneditor-icon{background-position:-48px 0}.jsoneditor-contextmenu button.jsoneditor-sort-asc .jsoneditor-icon{background-position:-168px 0}.jsoneditor-contextmenu button.jsoneditor-sort-desc .jsoneditor-icon{background-position:-192px 0}.jsoneditor-contextmenu button.jsoneditor-transform .jsoneditor-icon{background-position:-216px 0}.jsoneditor-contextmenu button.jsoneditor-extract .jsoneditor-icon{background-position:0 -24px}.jsoneditor-contextmenu button.jsoneditor-type-string .jsoneditor-icon{background-position:-144px 0}.jsoneditor-contextmenu button.jsoneditor-type-auto .jsoneditor-icon{background-position:-120px 0}.jsoneditor-contextmenu button.jsoneditor-type-object .jsoneditor-icon{background-position:-72px 0}.jsoneditor-contextmenu button.jsoneditor-type-array .jsoneditor-icon{background-position:-96px 0}.jsoneditor-contextmenu button.jsoneditor-type-modes .jsoneditor-icon{background-image:none;width:6px}.jsoneditor-contextmenu li,.jsoneditor-contextmenu ul{box-sizing:content-box;position:relative}.jsoneditor-contextmenu .jsoneditor-menu button:focus,.jsoneditor-contextmenu .jsoneditor-menu button:hover{color:#1a1a1a;background-color:#f5f5f5;outline:0}.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:focus,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:hover{color:#fff;background-color:#ee422e}.jsoneditor-contextmenu .jsoneditor-menu li ul li button:focus,.jsoneditor-contextmenu .jsoneditor-menu li ul li button:hover{background-color:#f5f5f5}.jsoneditor-modal{max-width:95%;border-radius:2px!important;padding:45px 15px 15px 15px!important;box-shadow:2px 2px 12px rgba(128,128,128,.3);color:#4d4d4d;line-height:1.3em}.jsoneditor-modal.jsoneditor-modal-transform{width:600px!important}.jsoneditor-modal .pico-modal-header{position:absolute;box-sizing:border-box;top:0;left:0;width:100%;padding:0 10px;height:30px;line-height:30px;font-family:arial,sans-serif;font-size:11pt;background:#3883fa;color:#fff}.jsoneditor-modal table{width:100%}.jsoneditor-modal table td{padding:3px 0}.jsoneditor-modal table td.jsoneditor-modal-input{text-align:right;padding-right:0;white-space:nowrap}.jsoneditor-modal table td.jsoneditor-modal-actions{padding-top:15px}.jsoneditor-modal table th{vertical-align:middle}.jsoneditor-modal p:first-child{margin-top:0}.jsoneditor-modal a{color:#3883fa}.jsoneditor-modal .jsoneditor-jmespath-block{margin-bottom:10px}.jsoneditor-modal .pico-close{background:0 0!important;font-size:24px!important;top:7px!important;right:7px!important;color:#fff}.jsoneditor-modal input{padding:4px}.jsoneditor-modal input[type=text]{cursor:inherit}.jsoneditor-modal input[disabled]{background:#d3d3d3;color:grey}.jsoneditor-modal .jsoneditor-select-wrapper{position:relative;display:inline-block}.jsoneditor-modal .jsoneditor-select-wrapper:after{content:"";width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:6px solid #666;position:absolute;right:8px;top:14px;pointer-events:none}.jsoneditor-modal select{padding:3px 24px 3px 10px;min-width:180px;max-width:350px;-webkit-appearance:none;-moz-appearance:none;appearance:none;text-indent:0;text-overflow:"";font-size:14px;line-height:1.5em}.jsoneditor-modal select::-ms-expand{display:none}.jsoneditor-modal .jsoneditor-button-group input{padding:4px 10px;margin:0;border-radius:0;border-left-style:none}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-first{border-top-left-radius:3px;border-bottom-left-radius:3px;border-left-style:solid}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-last{border-top-right-radius:3px;border-bottom-right-radius:3px}.jsoneditor-modal .jsoneditor-transform-preview{background:#f5f5f5;height:200px}.jsoneditor-modal .jsoneditor-transform-preview.jsoneditor-error{color:#ee422e}.jsoneditor-modal .jsoneditor-jmespath-wizard{line-height:1.2em;width:100%;padding:0;border-radius:3px}.jsoneditor-modal .jsoneditor-jmespath-label{font-weight:700;color:#1e90ff;margin-top:20px;margin-bottom:5px}.jsoneditor-modal .jsoneditor-jmespath-wizard-table{width:100%;border-collapse:collapse}.jsoneditor-modal .jsoneditor-jmespath-wizard-label{font-style:italic;margin:4px 0 2px 0}.jsoneditor-modal .jsoneditor-inline{position:relative;display:inline-block;width:100%;padding-top:2px;padding-bottom:2px}.jsoneditor-modal .jsoneditor-inline:not(:last-child){padding-right:2px}.jsoneditor-modal .jsoneditor-jmespath-filter{display:flex;flex-wrap:wrap}.jsoneditor-modal .jsoneditor-jmespath-filter-field{width:180px}.jsoneditor-modal .jsoneditor-jmespath-filter-relation{width:100px}.jsoneditor-modal .jsoneditor-jmespath-filter-value{min-width:180px;flex:1}.jsoneditor-modal .jsoneditor-jmespath-sort-field{width:170px}.jsoneditor-modal .jsoneditor-jmespath-sort-order{width:150px}.jsoneditor-modal .jsoneditor-jmespath-select-fields{width:100%}.jsoneditor-modal .selectr-selected{border-color:#d3d3d3;padding:4px 28px 4px 8px}.jsoneditor-modal .selectr-selected .selectr-tag{background-color:#3883fa;border-radius:5px}.jsoneditor-modal table td,.jsoneditor-modal table th{text-align:left;vertical-align:middle;font-weight:400;color:#4d4d4d;border-spacing:0;border-collapse:collapse}.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal input[type=text],.jsoneditor-modal input[type=text]:focus,.jsoneditor-modal select,.jsoneditor-modal textarea{background:#fff;border:1px solid #d3d3d3;color:#4d4d4d;border-radius:3px;padding:4px}.jsoneditor-modal #query,.jsoneditor-modal textarea{border-radius:unset}.jsoneditor-modal,.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal input[type=text],.jsoneditor-modal option,.jsoneditor-modal select,.jsoneditor-modal table td,.jsoneditor-modal table th,.jsoneditor-modal textarea{font-size:10.5pt;font-family:arial,sans-serif}.jsoneditor-modal #query,.jsoneditor-modal .jsoneditor-transform-preview{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px;width:100%;box-sizing:border-box}.jsoneditor-modal input[type=button],.jsoneditor-modal input[type=submit]{background:#f5f5f5;padding:4px 20px}.jsoneditor-modal input,.jsoneditor-modal select{cursor:pointer}.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-asc input.jsoneditor-button-asc,.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-desc input.jsoneditor-button-desc{background:#3883fa;border-color:#3883fa;color:#fff}.jsoneditor{color:#1a1a1a;border:thin solid #3883fa;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%;position:relative;padding:0;line-height:100%}div.jsoneditor-default,div.jsoneditor-field,div.jsoneditor-readonly,div.jsoneditor-value{border:1px solid transparent;min-height:16px;min-width:32px;line-height:16px;padding:2px;margin:1px;word-wrap:break-word;float:left}div.jsoneditor-field p,div.jsoneditor-value p{margin:0}div.jsoneditor-value{word-break:break-word}div.jsoneditor-value.jsoneditor-empty::after{content:"value"}div.jsoneditor-value.jsoneditor-string{color:#006000}div.jsoneditor-value.jsoneditor-number{color:#ee422e}div.jsoneditor-value.jsoneditor-boolean{color:#ff8c00}div.jsoneditor-value.jsoneditor-null{color:#004ed0}div.jsoneditor-value.jsoneditor-color-value{color:#1a1a1a}div.jsoneditor-value.jsoneditor-invalid{color:#1a1a1a}div.jsoneditor-readonly{min-width:16px;color:grey}div.jsoneditor-empty{border-color:#d3d3d3;border-style:dashed;border-radius:2px}div.jsoneditor-field.jsoneditor-empty::after{content:"field"}div.jsoneditor td{vertical-align:top}div.jsoneditor td.jsoneditor-separator{padding:3px 0;vertical-align:top;color:grey}div.jsoneditor td.jsoneditor-tree{vertical-align:top}div.jsoneditor.busy pre.jsoneditor-preview{background:#f5f5f5;color:grey}div.jsoneditor.busy div.jsoneditor-busy{display:inherit}div.jsoneditor code.jsoneditor-preview{background:0 0}div.jsoneditor.jsoneditor-mode-preview pre.jsoneditor-preview{width:100%;height:100%;box-sizing:border-box;overflow:auto;padding:2px;margin:0;white-space:pre-wrap;word-break:break-all}div.jsoneditor-default{color:grey;padding-left:10px}div.jsoneditor-tree{width:100%;height:100%;position:relative;overflow:auto;background:#fff}div.jsoneditor-tree button.jsoneditor-button{width:24px;height:24px;padding:0;margin:0;border:none;cursor:pointer;background-color:transparent;background-image:url(./img/jsoneditor-icons.svg)}div.jsoneditor-tree button.jsoneditor-button:focus{background-color:#f5f5f5;outline:#e5e5e5 solid 1px}div.jsoneditor-tree button.jsoneditor-collapsed{background-position:0 -48px}div.jsoneditor-tree button.jsoneditor-expanded{background-position:0 -72px}div.jsoneditor-tree button.jsoneditor-contextmenu-button{background-position:-48px -72px}div.jsoneditor-tree button.jsoneditor-invisible{visibility:hidden;background:0 0}div.jsoneditor-tree button.jsoneditor-dragarea{background-image:url(./img/jsoneditor-icons.svg);background-position:-72px -72px;cursor:move}div.jsoneditor-tree :focus{outline:0}div.jsoneditor-tree div.jsoneditor-show-more{display:inline-block;padding:3px 4px;margin:2px 0;background-color:#e5e5e5;border-radius:3px;color:grey;font-family:arial,sans-serif;font-size:14px}div.jsoneditor-tree div.jsoneditor-show-more a{display:inline-block;color:grey}div.jsoneditor-tree div.jsoneditor-color{display:inline-block;width:12px;height:12px;margin:4px;border:1px solid grey;cursor:pointer}div.jsoneditor-tree div.jsoneditor-color.jsoneditor-color-readonly{cursor:inherit}div.jsoneditor-tree div.jsoneditor-date{background:#a1a1a1;color:#fff;font-family:arial,sans-serif;border-radius:3px;display:inline-block;padding:3px;margin:0 3px}div.jsoneditor-tree table.jsoneditor-tree{border-collapse:collapse;border-spacing:0;width:100%}div.jsoneditor-tree .jsoneditor-button{display:block}div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error{width:24px;height:24px;padding:0;margin:0 4px 0 0;background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;background-color:transparent}div.jsoneditor-outer{position:static;width:100%;height:100%;margin:0;padding:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}div.jsoneditor-outer.has-nav-bar{margin-top:-26px;padding-top:26px}div.jsoneditor-outer.has-nav-bar.has-main-menu-bar{margin-top:-61px;padding-top:61px}div.jsoneditor-outer.has-status-bar{margin-bottom:-26px;padding-bottom:26px}div.jsoneditor-outer.has-main-menu-bar{margin-top:-35px;padding-top:35px}div.jsoneditor-busy{position:absolute;top:15%;left:0;box-sizing:border-box;width:100%;text-align:center;display:none}div.jsoneditor-busy span{background-color:#ffffab;border:1px solid #fe0;border-radius:3px;padding:5px 15px;box-shadow:0 0 5px rgba(0,0,0,.4)}div.jsoneditor-field.jsoneditor-empty::after,div.jsoneditor-value.jsoneditor-empty::after{pointer-events:none;color:#d3d3d3;font-size:8pt}a.jsoneditor-value.jsoneditor-url,div.jsoneditor-value.jsoneditor-url{color:#006000;text-decoration:underline}a.jsoneditor-value.jsoneditor-url{display:inline-block;padding:2px;margin:2px}a.jsoneditor-value.jsoneditor-url:focus,a.jsoneditor-value.jsoneditor-url:hover{color:#ee422e}div.jsoneditor-field.jsoneditor-highlight,div.jsoneditor-field[contenteditable=true]:focus,div.jsoneditor-field[contenteditable=true]:hover,div.jsoneditor-value.jsoneditor-highlight,div.jsoneditor-value[contenteditable=true]:focus,div.jsoneditor-value[contenteditable=true]:hover{background-color:#ffffab;border:1px solid #fe0;border-radius:2px}div.jsoneditor-field.jsoneditor-highlight-active,div.jsoneditor-field.jsoneditor-highlight-active:focus,div.jsoneditor-field.jsoneditor-highlight-active:hover,div.jsoneditor-value.jsoneditor-highlight-active,div.jsoneditor-value.jsoneditor-highlight-active:focus,div.jsoneditor-value.jsoneditor-highlight-active:hover{background-color:#fe0;border:1px solid #ffc700;border-radius:2px}div.jsoneditor-value.jsoneditor-array,div.jsoneditor-value.jsoneditor-object{min-width:16px}div.jsoneditor-tree button.jsoneditor-contextmenu-button.jsoneditor-selected,div.jsoneditor-tree button.jsoneditor-contextmenu-button:focus,div.jsoneditor-tree button.jsoneditor-contextmenu-button:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button{background-position:-48px -48px}div.jsoneditor-tree div.jsoneditor-show-more a:focus,div.jsoneditor-tree div.jsoneditor-show-more a:hover{color:#ee422e}.ace-jsoneditor,textarea.jsoneditor-text{min-height:150px}.ace-jsoneditor.ace_editor,textarea.jsoneditor-text.ace_editor{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace}textarea.jsoneditor-text{width:100%;height:100%;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;outline-width:0;border:none;background-color:#fff;resize:none}tr.jsoneditor-highlight,tr.jsoneditor-selected{background-color:#d3d3d3}tr.jsoneditor-selected button.jsoneditor-contextmenu-button,tr.jsoneditor-selected button.jsoneditor-dragarea{visibility:hidden}tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{visibility:visible}div.jsoneditor-tree button.jsoneditor-dragarea:focus,div.jsoneditor-tree button.jsoneditor-dragarea:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{background-position:-72px -48px}div.jsoneditor td,div.jsoneditor th,div.jsoneditor tr{padding:0;margin:0}.jsoneditor-popover,.jsoneditor-schema-error,div.jsoneditor td,div.jsoneditor textarea,div.jsoneditor th,div.jsoneditor-field,div.jsoneditor-value,pre.jsoneditor-preview{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px;color:#1a1a1a}.jsoneditor-schema-error{cursor:default;display:inline-block;height:24px;line-height:24px;position:relative;text-align:center;width:24px}.jsoneditor-popover{background-color:#4c4c4c;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.4);color:#fff;padding:7px 10px;position:absolute;cursor:auto;width:200px}.jsoneditor-popover.jsoneditor-above{bottom:32px;left:-98px}.jsoneditor-popover.jsoneditor-above:before{border-top:7px solid #4c4c4c;bottom:-7px}.jsoneditor-popover.jsoneditor-below{top:32px;left:-98px}.jsoneditor-popover.jsoneditor-below:before{border-bottom:7px solid #4c4c4c;top:-7px}.jsoneditor-popover.jsoneditor-left{top:-7px;right:32px}.jsoneditor-popover.jsoneditor-left:before{border-left:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;right:-14px;left:inherit;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover.jsoneditor-right{top:-7px;left:32px}.jsoneditor-popover.jsoneditor-right:before{border-right:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;left:-14px;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover:before{border-right:7px solid transparent;border-left:7px solid transparent;content:"";display:block;left:50%;margin-left:-7px;position:absolute}.jsoneditor-text-errors tr.jump-to-line:hover{text-decoration:underline;cursor:pointer}.jsoneditor-schema-error:focus .jsoneditor-popover,.jsoneditor-schema-error:hover .jsoneditor-popover{display:block;animation:fade-in .3s linear 1,move-up .3s linear 1}@keyframes fade-in{from{opacity:0}to{opacity:1}}.jsoneditor .jsoneditor-validation-errors-container{max-height:130px;overflow-y:auto}.jsoneditor .jsoneditor-validation-errors{width:100%;overflow:hidden}.jsoneditor .jsoneditor-additional-errors{position:absolute;margin:auto;bottom:31px;left:calc(50% - 92px);color:grey;background-color:#ebebeb;padding:7px 15px;border-radius:8px}.jsoneditor .jsoneditor-additional-errors.visible{visibility:visible;opacity:1;transition:opacity 2s linear}.jsoneditor .jsoneditor-additional-errors.hidden{visibility:hidden;opacity:0;transition:visibility 0s 2s,opacity 2s linear}.jsoneditor .jsoneditor-text-errors{width:100%;border-collapse:collapse;border-top:1px solid #ffc700}.jsoneditor .jsoneditor-text-errors td{padding:3px 6px;vertical-align:middle}.jsoneditor .jsoneditor-text-errors td pre{margin:0;white-space:pre-wrap}.jsoneditor .jsoneditor-text-errors tr{background-color:#ffffab}.jsoneditor .jsoneditor-text-errors tr.parse-error{background-color:rgba(238,46,46,.4392156863)}.jsoneditor-text-errors .jsoneditor-schema-error{border:none;width:24px;height:24px;padding:0;margin:0 4px 0 0;cursor:pointer}.jsoneditor-text-errors tr .jsoneditor-schema-error{background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;background-color:transparent}.jsoneditor-text-errors tr.parse-error .jsoneditor-schema-error{background-image:url(./img/jsoneditor-icons.svg);background-position:-25px 0;background-color:transparent}.jsoneditor-anchor{cursor:pointer}.jsoneditor-anchor .picker_wrapper.popup.popup_bottom{top:28px;left:-10px}.fadein{-webkit-animation:fadein .3s;animation:fadein .3s;-moz-animation:fadein .3s;-o-animation:fadein .3s}@keyframes fadein{0%{opacity:0}100%{opacity:1}}.jsoneditor-modal input[type=search].selectr-input{border:1px solid #d3d3d3;width:calc(100% - 4px);margin:2px;padding:4px;box-sizing:border-box}.jsoneditor-modal button.selectr-input-clear{right:8px}.jsoneditor-menu{width:100%;height:35px;padding:2px;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;background-color:#3883fa;border-bottom:1px solid #3883fa}.jsoneditor-menu>.jsoneditor-modes>button,.jsoneditor-menu>button{width:26px;height:26px;margin:2px;padding:0;border-radius:2px;border:1px solid transparent;background-color:transparent;background-image:url(./img/jsoneditor-icons.svg);color:#fff;opacity:.8;font-family:arial,sans-serif;font-size:14px;float:left}.jsoneditor-menu>.jsoneditor-modes>button:hover,.jsoneditor-menu>button:hover{background-color:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.4)}.jsoneditor-menu>.jsoneditor-modes>button:active,.jsoneditor-menu>.jsoneditor-modes>button:focus,.jsoneditor-menu>button:active,.jsoneditor-menu>button:focus{background-color:rgba(255,255,255,.3)}.jsoneditor-menu>.jsoneditor-modes>button:disabled,.jsoneditor-menu>button:disabled{opacity:.5;background-color:transparent;border:none}.jsoneditor-menu>button.jsoneditor-collapse-all{background-position:0 -96px}.jsoneditor-menu>button.jsoneditor-expand-all{background-position:0 -120px}.jsoneditor-menu>button.jsoneditor-sort{background-position:-120px -96px}.jsoneditor-menu>button.jsoneditor-transform{background-position:-144px -96px}.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-transform,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-transform{display:none}.jsoneditor-menu>button.jsoneditor-undo{background-position:-24px -96px}.jsoneditor-menu>button.jsoneditor-undo:disabled{background-position:-24px -120px}.jsoneditor-menu>button.jsoneditor-redo{background-position:-48px -96px}.jsoneditor-menu>button.jsoneditor-redo:disabled{background-position:-48px -120px}.jsoneditor-menu>button.jsoneditor-compact{background-position:-72px -96px}.jsoneditor-menu>button.jsoneditor-format{background-position:-72px -120px}.jsoneditor-menu>button.jsoneditor-repair{background-position:-96px -96px}.jsoneditor-menu>.jsoneditor-modes{display:inline-block;float:left}.jsoneditor-menu>.jsoneditor-modes>button{background-image:none;width:auto;padding-left:6px;padding-right:6px}.jsoneditor-menu>.jsoneditor-modes>button.jsoneditor-separator,.jsoneditor-menu>button.jsoneditor-separator{margin-left:10px}.jsoneditor-menu a{font-family:arial,sans-serif;font-size:14px;color:#fff;opacity:.8;vertical-align:middle}.jsoneditor-menu a:hover{opacity:1}.jsoneditor-menu a.jsoneditor-poweredBy{font-size:8pt;position:absolute;right:0;top:0;padding:10px}.jsoneditor-navigation-bar{width:100%;height:26px;line-height:26px;padding:0;margin:0;border-bottom:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:grey;background-color:#ebebeb;overflow:hidden;font-family:arial,sans-serif;font-size:14px}.jsoneditor-search{font-family:arial,sans-serif;position:absolute;right:4px;top:4px;border-collapse:collapse;border-spacing:0;display:flex}.jsoneditor-search input{color:#1a1a1a;width:120px;border:none;outline:0;margin:1px;line-height:20px;font-family:arial,sans-serif}.jsoneditor-search button{width:16px;height:24px;padding:0;margin:0;border:none;background:url(./img/jsoneditor-icons.svg);vertical-align:top}.jsoneditor-search button:hover{background-color:transparent}.jsoneditor-search button.jsoneditor-refresh{width:18px;background-position:-99px -73px}.jsoneditor-search button.jsoneditor-next{cursor:pointer;background-position:-124px -73px}.jsoneditor-search button.jsoneditor-next:hover{background-position:-124px -49px}.jsoneditor-search button.jsoneditor-previous{cursor:pointer;background-position:-148px -73px;margin-right:2px}.jsoneditor-search button.jsoneditor-previous:hover{background-position:-148px -49px}.jsoneditor-results{font-family:arial,sans-serif;color:#fff;padding-right:5px;line-height:26px}.jsoneditor-frame{border:1px solid transparent;background-color:#fff;padding:0 2px;margin:0}.jsoneditor-statusbar{line-height:26px;height:26px;color:grey;background-color:#ebebeb;border-top:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px}.jsoneditor-statusbar>.jsoneditor-curserinfo-val{margin-right:12px}.jsoneditor-statusbar>.jsoneditor-curserinfo-count{margin-left:4px}.jsoneditor-statusbar>.jsoneditor-validation-error-icon{float:right;width:24px;height:24px;padding:0;margin-top:1px;background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-validation-error-count{float:right;margin:0 4px 0 0;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-parse-error-icon{float:right;width:24px;height:24px;padding:0;margin:1px;background-image:url(./img/jsoneditor-icons.svg);background-position:-25px 0}.jsoneditor-statusbar .jsoneditor-array-info a{color:inherit}div.jsoneditor-statusbar>.jsoneditor-curserinfo-label,div.jsoneditor-statusbar>.jsoneditor-size-info{margin:0 4px}.jsoneditor-treepath{padding:0 5px;overflow:hidden;white-space:nowrap;outline:0}.jsoneditor-treepath.show-all{word-wrap:break-word;white-space:normal;position:absolute;background-color:#ebebeb;z-index:1;box-shadow:2px 2px 12px rgba(128,128,128,.3)}.jsoneditor-treepath.show-all span.jsoneditor-treepath-show-all-btn{display:none}.jsoneditor-treepath div.jsoneditor-contextmenu-root{position:absolute;left:0}.jsoneditor-treepath .jsoneditor-treepath-show-all-btn{position:absolute;background-color:#ebebeb;left:0;height:20px;padding:0 3px;cursor:pointer}.jsoneditor-treepath .jsoneditor-treepath-element{margin:1px;font-family:arial,sans-serif;font-size:14px}.jsoneditor-treepath .jsoneditor-treepath-seperator{margin:2px;font-size:9pt;font-family:arial,sans-serif}.jsoneditor-treepath span.jsoneditor-treepath-element:hover,.jsoneditor-treepath span.jsoneditor-treepath-seperator:hover{cursor:pointer;text-decoration:underline}/*! + * Selectr 2.4.13 + * http://mobius.ovh/docs/selectr + * + * Released under the MIT license + */.selectr-container{position:relative}.selectr-container li{list-style:none}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0 none}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;background-color:#fff}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:"";-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0 4px;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{list-style:none;position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:medium none;border-radius:10px;background:#acb7bf none repeat scroll 0 0}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:medium none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:" ";background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;background-color:#fff}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input-container.active,.selectr-input-container.active .selectr-clear{display:block}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;list-style:outside none none;cursor:pointer;font-weight:400}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-option.excluded{display:none}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent #999;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-placeholder,.selectr-empty{display:none}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-label{width:auto}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px;width:auto}.selectr-tag-input{border:medium none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:"";-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running selectr-spin;-webkit-animation:.5s linear 0s normal forwards infinite running selectr-spin;animation:.5s linear 0s normal forwards infinite running selectr-spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes selectr-spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes selectr-spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;background-color:#fff}.selectr-container.inverted .selectr-options-container{top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1} \ No newline at end of file diff --git a/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.js b/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.js new file mode 100644 index 000000000..dd48a798f --- /dev/null +++ b/python/services/evaluationservice/dmod/evaluationservice/static/jsoneditor/jsoneditor.min.js @@ -0,0 +1,86 @@ +/*! + * jsoneditor.js + * + * @brief + * JSONEditor is a web-based tool to view, edit, format, and validate JSON. + * It has various modes such as a tree editor, a code editor, and a plain text + * editor. + * + * Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+ + * + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2023 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + * @version 9.10.2 + * @date 2023-05-15 + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JSONEditor=t():e.JSONEditor=t()}(self,function(){return n={897:function(e,t,n){"use strict";n.d(t,{x:function(){return r}});var c=n(2602),h=n(9791),d=n(7907);function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,t){for(var n=0;ns.top&&(i=!1),n?0:r.top-o.top);i?(n=e.offsetHeight,this.dom.menu.style.left="0",this.dom.menu.style.top=t+n+"px",this.dom.menu.style.bottom=""):(this.dom.menu.style.left="0",this.dom.menu.style.top="",this.dom.menu.style.bottom="0px"),this.limitHeight&&(o=i?s.bottom-r.bottom-10:r.top-s.top-10,this.dom.list.style.maxHeight=o+"px",this.dom.list.style.overflowY="auto"),this.dom.absoluteAnchor.appendChild(this.dom.root),this.selection=(0,h.getSelection)(),this.anchor=e,setTimeout(function(){a.dom.focusButton.focus()},0),l.visibleMenu&&l.visibleMenu.hide(),l.visibleMenu=this}},{key:"hide",value:function(){this.dom.absoluteAnchor&&(this.dom.absoluteAnchor.destroy(),delete this.dom.absoluteAnchor),this.dom.root.parentNode&&(this.dom.root.parentNode.removeChild(this.dom.root),this.onClose)&&this.onClose(),l.visibleMenu===this&&(l.visibleMenu=void 0)}},{key:"_onExpandItem",value:function(n){var i,o=this,e=n===this.expandedItem,t=this.expandedItem;t&&(t.ul.style.height="0",t.ul.style.padding="",setTimeout(function(){o.expandedItem!==t&&(t.ul.style.display="",h.removeClassName)(t.ul.parentNode,"jsoneditor-selected")},300),this.expandedItem=void 0),e||((i=n.ul).style.display="block",i.clientHeight,setTimeout(function(){if(o.expandedItem===n){for(var e=0,t=0;t/gi,"\n"))),i.appendChild(o),n.appendChild(i)),n.onclick=function(){a.onFocusLine(e)},s.appendChild(n)}),this.dom.validationErrors=n,this.dom.validationErrorsContainer.appendChild(n),this.dom.additionalErrorsIndication.title=e.length+" errors total",this.dom.validationErrorsContainer.clientHeighte[0].length)||(e=t,n=r,this.options.flex));r++);return e?((i=e[0].match(/\n.*/g))&&(this.yylineno+=i.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:i?i[i.length-1].length-1:this.yylloc.last_column+e[0].length},this.yytext+=e[0],this.match+=e[0],this.yyleng=this.yytext.length,this._more=!1,this._input=this._input.slice(e[0].length),this.matched+=e[0],i=this.performAction.call(this,this.yy,this,o[n],this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),i||void 0):""===this._input?this.EOF:void this.parseError("Lexical error on line "+(this.yylineno+1)+". Unrecognized text.\n"+this.showPosition(),{text:"",token:null,line:this.yylineno})},lex:function(){var e=this.next();return void 0!==e?e:this.lex()},begin:function(e){this.conditionStack.push(e)},popState:function(){return this.conditionStack.pop()},_currentRules:function(){return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules},topState:function(){return this.conditionStack[this.conditionStack.length-2]},pushState:function(e){this.begin(e)},options:{},performAction:function(e,t,n,i){switch(n){case 0:break;case 1:return 6;case 2:return t.yytext=t.yytext.substr(1,t.yyleng-2),4;case 3:return 17;case 4:return 18;case 5:return 23;case 6:return 24;case 7:return 22;case 8:return 21;case 9:return 10;case 10:return 11;case 11:return 8;case 12:return 14;case 13:return"INVALID"}},rules:[/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/],conditions:{INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13],inclusive:!0}}};var n;t.parser=n,t.parse=n.parse.bind(n)},3879:function(e){"use strict";function i(){}i.prototype={on:function(e,t){this._events=this._events||{},this._events[e]=this._events[e]||[],this._events[e].push(t)},off:function(e,t){this._events=this._events||{},e in this._events!=!1&&this._events[e].splice(this._events[e].indexOf(t),1)},emit:function(e){if(this._events=this._events||{},e in this._events!=!1)for(var t=0;t"+t.label+""}),u.each(t.children,function(e,t){t.idx=l,a.appendChild(d.call(this,t,a)),l++},this)):(t.idx=l,d.call(this,t),l++)},this),this.config.data&&Array.isArray(this.config.data)&&(o=!(this.data=[]),a=!1,l=0,u.each(this.config.data,function(e,t){h(t,"children")?(o=u.createElement("optgroup",{label:t.text}),a=u.createElement("ul",{class:"selectr-optgroup",role:"group",html:"
  • "+t.text+"
  • "}),u.each(t.children,function(e,t){(r=new Option(t.text,t.value,!1,t.hasOwnProperty("selected")&&!0===t.selected)).disabled=h(t,"disabled"),this.options.push(r),o.appendChild(r),r.idx=l,a.appendChild(d.call(this,r,t)),this.data[l]=t,l++},this),this.el.appendChild(o)):((r=new Option(t.text,t.value,!1,t.hasOwnProperty("selected")&&!0===t.selected)).disabled=h(t,"disabled"),this.options.push(r),r.idx=l,d.call(this,r,t),this.data[l]=t,l++)},this)),this.setSelected(!0);for(var c=this.navIndex=0;cthis.tree.lastElementChild.idx){this.navIndex=this.tree.lastElementChild.idx;break}if(this.navIndexthis.optsRect.top+this.optsRect.height&&(this.tree.scrollTop=this.tree.scrollTop+(e.top+e.height-(this.optsRect.top+this.optsRect.height))),this.navIndex===this.tree.childElementCount-1&&this.requiresPagination&&r.call(this)):0===this.navIndex?this.tree.scrollTop=0:e.top-this.optsRect.top<0&&(this.tree.scrollTop=this.tree.scrollTop+(e.top-this.optsRect.top)),n&&u.removeClass(n,"active"),u.addClass(this.items[this.navIndex],"active")}else this.navigating=!1}.bind(this),this.events.reset=this.reset.bind(this),(this.config.nativeDropdown||this.mobileDevice)&&(this.container.addEventListener("touchstart",function(e){e.changedTouches[0].target===i.el&&i.toggle()},!!this.supportsEventPassiveOption&&{passive:!0}),this.container.addEventListener("click",function(e){e.target===i.el&&i.toggle()}),this.el.addEventListener("change",function(e){e.__selfTriggered||(i.el.multiple?(e=i.getSelectedProperties("idx"),e=function(e,t){for(var n,i=[],o=e.slice(0),r=0;rn?(u.addClass(this.container,"inverted"),this.isInverted=!0):(u.removeClass(this.container,"inverted"),this.isInverted=!1),this.optsRect=u.rect(this.tree)},t.prototype.getOptionByIndex=function(e){return this.options[e]},t.prototype.getOptionByValue=function(e){for(var t=!1,n=0,i=this.options.length;nthis.limit&&1s.EX?((0,m.addClassName)((n=this).frame,"busy"),n.dom.busyContent.innerText=t,setTimeout(function(){e(),(0,m.removeClassName)(n.frame,"busy"),n.dom.busyContent.innerText=""},100)):e()},t.validate=n.validate,t._renderErrors=n._renderErrors,[{mode:"preview",mixin:t,data:"json"}])},6210:function(e,t,n){"use strict";n.r(t),n.d(t,{showSortModal:function(){return s}});var t=n(483),i=n.n(t),o=n(7907),r=n(9791);function s(e,t,s,n){var a=Array.isArray(t)?(0,r.getChildPaths)(t):[""],l=n&&n.path&&(0,r.contains)(a,n.path)?n.path:a[0],c=n&&n.direction||"asc",t='
    '+(0,o.Iu)("sort")+"
    "+(0,o.Iu)("sortFieldLabel")+'
    '+(0,o.Iu)("sortDirectionLabel")+'
    ';i()({parent:e,content:t,overlayClass:"jsoneditor-modal-overlay",overlayStyles:{backgroundColor:"rgb(1,1,1)",opacity:.3},modalClass:"jsoneditor-modal jsoneditor-modal-sort"}).afterCreate(function(t){var e=t.modalElem().querySelector("form"),n=t.modalElem().querySelector("#ok"),i=t.modalElem().querySelector("#field"),o=t.modalElem().querySelector("#direction");function r(e){o.value=e,o.className="jsoneditor-button-group jsoneditor-button-group-value-"+o.value}a.forEach(function(e){var t,n=document.createElement("option");n.text=""===(t=e)?"@":"."===t[0]?t.slice(1):t,n.value=e,i.appendChild(n)}),i.value=l||a[0],r(c||"asc"),o.onclick=function(e){r(e.target.getAttribute("data-value"))},n.onclick=function(e){e.preventDefault(),e.stopPropagation(),t.close(),s({path:i.value,direction:o.value})},e&&(e.onsubmit=n.onclick)}).afterClose(function(e){e.destroy()}).show()}},2558:function(e,t,n){"use strict";n.r(t),n.d(t,{showTransformModal:function(){return s}});var t=n(483),i=n.n(t),t=n(3879),x=n.n(t),o=n(7907);function I(e){return(I="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function k(e,t,n,i){if("boolean"==typeof e||e instanceof Boolean||null===e||"number"==typeof e||e instanceof Number||"string"==typeof e||e instanceof String||e instanceof Date)return JSON.stringify(e);if(Array.isArray(e)){for(var o=e,r=t,s=n,a=i,l=r?s+r:void 0,c=r?"[\n":"[",h=0;ha)return c+"..."}return c+=r?"\n"+s+"]":"]"}if(e&&"object"===I(e)){var u,g=e,p=t,s=n,m=i,f=p?s+p:void 0,C=!0,y=p?"{\n":"{";if("function"==typeof g.toJSON)return k(g.toJSON(),p,s,m);for(u in g)if(function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}(g,u)){var v=g[u];if(C?C=!1:y+=p?",\n":",",(y=(y+=p?f+'"'+u+'": ':'"'+u+'":')+k(v,p,f,m)).length>m)return y+"..."}return y+=p?"\n"+s+"}":"}"}}function E(e,t){for(var n="";0JMESPath query to filter, sort, or transform the JSON data.
    To learn JMESPath, go to the interactive tutorial.';function s(e){var t=e.container,I=e.json,n=e.queryDescription,n=void 0===n?r:n,b=e.createQuery,A=e.executeQuery,w=e.onTransform,S=I,e='