You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2018/08/20 09:39:22 UTC
[02/12] brooklyn-ui git commit: initial JS poc of custom config widget
initial JS poc of custom config widget
all the hard angular stuff working, but hard-coded. now just need to wire it to real tags.
Project: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/commit/29ba70e1
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/tree/29ba70e1
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-ui/diff/29ba70e1
Branch: refs/heads/master
Commit: 29ba70e1936367a4e226823add0a4272252b0922
Parents: f9d239e
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Mon Aug 6 22:59:11 2018 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Mon Aug 6 22:59:11 2018 +0100
----------------------------------------------------------------------
docs/customizations.md | 8 +++
.../suggestion-dropdown.html | 48 +++++++++++++++
.../custom-config-widget/suggestion-dropdown.js | 44 ++++++++++++++
.../suggestion-dropdown.less | 30 +++++++++
.../spec-editor/spec-editor.directive.js | 64 +++++++++++++++++++-
.../app/components/spec-editor/spec-editor.less | 26 +++++++-
.../spec-editor/spec-editor.template.html | 22 +++++--
ui-modules/blueprint-composer/app/index.js | 6 +-
ui-modules/blueprint-composer/app/index.less | 1 +
9 files changed, 238 insertions(+), 11 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/docs/customizations.md
----------------------------------------------------------------------
diff --git a/docs/customizations.md b/docs/customizations.md
index 4458a88..9912317 100644
--- a/docs/customizations.md
+++ b/docs/customizations.md
@@ -11,3 +11,11 @@ There are a number of points where UI default choices can be overridden:
items for the palette can come from Brooklyn and/or other sources, with other sources converted to "virtual items" that
can extend existing Brooklyn items
+* Composer - Custom Config Widgets: special widgets to use for config keys can be specified in a registered type's
+ definition as a map tag, for example for the demo widget `suggestion-dropout` included we might have:
+ '{ ui-composer-hints: { config-widgets: [ { key: start.timeout, suggestion-values: [ 30s, 2m, 5m, 30m, 2h ],
+ widget: suggestion-dropdown, label-collapsed: fail after, label-expanded: Fail if not successful within } ] } }`;
+ widgets should be registered as angular directives using the standard naming conventions (e.g. suggestionDropdownDirective),
+ as done for that directive in app/index.js and app/index.less.
+
+
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.html
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.html b/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.html
new file mode 100644
index 0000000..c511dfe
--- /dev/null
+++ b/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.html
@@ -0,0 +1,48 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+-->
+<div class="custom-config-widget suggestion-dropdown config-flex-row">
+ <label class="control-label" for="{{item.name}}">
+ <span class="label-spec-configuration">{{item.label || item.name}}</span>
+ <span class="info-spec-configuration">
+ <i class="fa fa-fw fa-info-circle" popover-trigger="'mouseenter'"
+ popover-title="{{item.label || item.name}}"
+ uib-popover-template="'ConfigInfoTemplate.html'"
+ popover-class="spec-editor-popover" popover-placement="top-left" popover-append-to-body="true"></i>
+ </span>
+ </label>
+
+ <span class="label-rhs-buttons">
+ <span class="spacer"> </span>
+ <span class="custom-widget-enable-button custom-widget-active">
+ <i class="fa fa-fw fa-sun-o" ng-click="toggleCustomConfigWidgetMode(item, 'disabled')" aria-hidden="true" title="{{getCustomConfigWidgetModeTitle(item)}} [{{item.name}}]"></i>
+ <span class="sr-only">{{getCustomConfigWidgetModeTitle(item)}} [{{item.name}}]</span>
+ </span>
+ </span>
+
+ <div class="control-value" ng-class="{ 'inline-control': item.widgetMode==='boolean', 'unset': !defined(config[item.name]), 'has-default': defined(item.defaultValue) && item.defaultValue!=null }">
+ <div class="hide-when-collapsed">
+ <a ng-click="test()">Extra information</a>
+ </div>
+ <div>
+ <span class="hide-when-expanded">Custom widget: </span>
+ {{ config[item.name] }}
+ </div>
+ </div>
+
+</div>
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.js
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.js b/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.js
new file mode 100644
index 0000000..d36c801
--- /dev/null
+++ b/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import angular from 'angular';
+import template from './suggestion-dropdown.html';
+
+const MODULE_NAME = 'brooklyn.components.custom-config-widget.suggestion-dropdown';
+
+angular.module(MODULE_NAME, [])
+ .directive('suggestionDropdown', ['$rootScope', suggestionDropdownDirective]);
+
+export default MODULE_NAME;
+
+export function suggestionDropdownDirective($rootScope) {
+ return {
+ restrict: 'E',
+ scope: {
+ item: '='
+ },
+ template: template,
+ link: link,
+ };
+
+ function link(scope) {
+ scope.$parent.copyScopeForCustomConfigWidget(scope);
+ }
+
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.less
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.less b/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.less
new file mode 100644
index 0000000..c080a1b
--- /dev/null
+++ b/ui-modules/blueprint-composer/app/components/custom-config-widget/suggestion-dropdown.less
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+suggestion-dropdown {
+
+ width: 100%;
+
+ .control-value {
+ // for widgets that are text, this aligns it with the label when collapsed and gives nice spacing when expanded.
+ // (for widgets that are dropdowns etc this should be omitted)
+ padding-top: 2px;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
index 7a581b7..a0eeba8 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
@@ -33,7 +33,7 @@ const ANY_MEMBERSPEC_REGEX = /(^.*[m,M]ember[s,S]pec$)/;
const REPLACED_DSL_ENTITYSPEC = '___brooklyn:entitySpec';
angular.module(MODULE_NAME, [onEnter, autoGrow, blurOnEnter, brooklynDslEditor, brooklynDslViewer])
- .directive('specEditor', ['$rootScope', '$filter', '$log', '$sce', '$timeout', '$document', 'blueprintService', specEditorDirective])
+ .directive('specEditor', ['$rootScope', '$templateCache', '$injector', '$sanitize', '$filter', '$log', '$sce', '$timeout', '$document', 'blueprintService', specEditorDirective])
.filter('specEditorConfig', specEditorConfigFilter)
.filter('specEditorType', specEditorTypeFilter);
@@ -70,7 +70,7 @@ export const CONFIG_FILTERS = [
}
];
-export function specEditorDirective($rootScope, $filter, $log, $sce, $timeout, $document, blueprintService) {
+export function specEditorDirective($rootScope, $templateCache, $injector, $sanitize, $filter, $log, $sce, $timeout, $document, blueprintService) {
return {
restrict: 'E',
scope: {
@@ -106,6 +106,10 @@ export function specEditorDirective($rootScope, $filter, $log, $sce, $timeout, $
codeModeActive: {},
codeModeForced: {},
codeModeError: {},
+ customWidget: {
+ "defaultDisplayName": { mode: "full", widget: "known-widget-missing" },
+ "download.url": { mode: "full", widget: "suggestion-dropdown" },
+ },
},
location: {
open: false
@@ -514,7 +518,7 @@ export function specEditorDirective($rootScope, $filter, $log, $sce, $timeout, $
// local config changed, make sure model is updated too
setModelFromLocalConfig();
}
- }
+ };
scope.getJsonModeTitle = (itemName) => {
if (!scope.state.config.codeModeActive[itemName]) {
return "Treat this value as a JSON-encoded object ["+itemName+"]";
@@ -525,6 +529,60 @@ export function specEditorDirective($rootScope, $filter, $log, $sce, $timeout, $
return "Edit in simple mode, unwrapping JSON if possible ["+itemName+"]";
}
}
+ scope.getCustomConfigWidgetMode = (item) => {
+ var mode = (scope.state.config.customWidget[item.name] || {})["mode"] || null;
+ if (mode) {
+ if ((scope.state.config.customWidget[item.name] || {})["error"]) return null;
+ }
+ return mode;
+ } ;
+ scope.toggleCustomConfigWidgetMode = (item, newval) => {
+ if (!scope.state.config.customWidget[item.name]) {
+ $log.error('Custom widget mode should not be toggled when not available: '+item.name);
+ return null;
+ }
+ if (newval) scope.state.config.customWidget[item.name].mode = newval;
+ else {
+ if (scope.state.config.customWidget[item.name].mode === "disabled") {
+ // TODO not always full
+ scope.state.config.customWidget[item.name].mode = "full"
+ } else {
+ scope.state.config.customWidget[item.name].mode = "full"
+ }
+ }
+ }
+ scope.getCustomConfigWidgetModeTitle = (item) => {
+ if (!scope.state.config.customWidget[item.name]) {
+ // shouldn't be visible
+ return "(custom widget not available)";
+ }
+ if (scope.state.config.customWidget[item.name].mode === "disabled") return "Enable custom widget";
+ else return "Disable custom widget";
+ };
+ scope.copyScopeForCustomConfigWidget = (descendantScope) => {
+ descendantScope.toggleCustomConfigWidgetMode = scope.toggleCustomConfigWidgetMode;
+ descendantScope.getCustomConfigWidgetModeTitle = scope.getCustomConfigWidgetModeTitle;
+ descendantScope.defined = scope.defined;
+ descendantScope.config = scope.config;
+ descendantScope.state = scope.state;
+ descendantScope.copyScopeForCustomConfigWidget = scope.copyScopeForCustomConfigWidget;
+ };
+ scope.getCustomConfigWidgetTemplate = (item) => {
+ var widgetName = $sanitize(scope.state.config.customWidget[item.name].widget);
+ var templateName = 'custom-config-widget-'+widgetName;
+ if (!$templateCache.get(templateName)) {
+ var widgetDirective = widgetName.replace(/(-[a-z])/g, function($1){return $1[1].toUpperCase();})+'Directive';
+ if ($injector.has(widgetDirective)) {
+ $templateCache.put(templateName, '<'+widgetName+' item="item">foo</'+widgetName+'>');
+ } else {
+ $log.error('Missing directive '+widgetDirective+' for custom widget for '+item.name+'; falling back to default widget');
+ scope.state.config.customWidget[item.name].error = "Missing directive";
+ templateName = "error-" + templateName;
+ $templateCache.put(templateName, '<i>Widget '+widgetName+' missing</i>');
+ }
+ }
+ return templateName;
+ };
scope.isDsl = (key, index) => {
let val = scope.model.config.get(key);
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
index 1c14c92..233d937 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
@@ -272,12 +272,23 @@ spec-editor {
margin-right: 0.5em;
.remove-affordance();
}
+ span.rhs-buttons-subspan {
+ display: flex;
+ }
.json-config i {
margin-top: 2px; //nudge down to make aligned
}
- .dsl-open-wizard-from-toolbar, .dsl-manual-override-editor, .json-config {
+ .dsl-open-wizard-from-toolbar, .dsl-manual-override-editor, .json-config, .custom-widget-enable-button {
.toolbar-button-affordance()
}
+ .custom-widget-enable-button.custom-widget-active {
+ i {
+ color: @brand-primary;
+ }
+ &:hover i {
+ color: @gray-light;
+ }
+ }
.dsl-manual-override-editor.dsl-viewer-manual-override,
.json-config.code-mode-active {
i {
@@ -327,8 +338,11 @@ spec-editor {
}
}
.form-group {
- display: flex;
- flex-flow: row wrap;
+ &, .config-flex-row, .custom-widget-full {
+ display: flex;
+ flex-flow: row wrap;
+ width: 100%;
+ }
.control-label {
flex: 0 0 auto;
order: 10;
@@ -406,6 +420,9 @@ spec-editor {
}
}
&:not(:focus-within):not(:focus) {
+ .hide-when-collapsed {
+ display: none;
+ }
input {
// the RHS is square if DSL button hidden; simplest fix is to make LHS square too
border-radius: 0;
@@ -426,6 +443,9 @@ spec-editor {
}
}
.form-group:focus, .form-group:focus-within {
+ .hide-when-expanded {
+ display: none;
+ }
.control-value:not(.inline-control) {
flex-basis: 100%;
}
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
index b305651..cc3e11e 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
@@ -131,12 +131,20 @@
<form name="formSpecConfig" novalidate class="lightweight">
<div ng-repeat="item in (filteredItems = (model.miscData.get('config') | specEditorConfig:state.config.filter.values:model.config | filter:{name:state.config.search} | orderBy:+priority)) track by item.name ">
- <div class="form-group" ng-class="{'has-error': (model.issues | filter:{ref: item.name}:true).length > 0, 'used': config[item.name] !== undefined}" ng-switch="getConfigWidgetMode(item)" tabindex="1"
+ <div class="form-group" ng-class="{'has-error': (model.issues | filter:{ref: item.name}:true).length > 0, 'used': config[item.name] !== undefined}"
+ ng-switch="getConfigWidgetMode(item)"
+ tabindex="1"
auto-focus="state.config.focus === item.name"
auto-focus-listen-to-window="true"
auto-focus-not-if-within="true"
ng-focus="recordFocus(item)"
on-enter="advanceControlInFormGroup">
+ <div ng-if="getCustomConfigWidgetMode(item) === 'full'" class="custom-widget-full">
+ <ng-include src="getCustomConfigWidgetTemplate(item)" class="custom-widget-full"/>
+ </div>
+
+ <!-- have to hide it; excluding it conditionally via if doesn't play nice with switch -->
+ <div ng-show="getCustomConfigWidgetMode(item) !== 'full'" class="config-flex-row">
<label class="control-label" for="{{item.name}}">
<span class="label-spec-configuration">{{item.label || item.name}}</span>
<span class="info-spec-configuration">
@@ -149,11 +157,15 @@
<span class="label-rhs-buttons">
<span class="spacer"> </span>
+ <span class="custom-widget-enable-button" ng-if="getCustomConfigWidgetMode(item) !== null">
+ <i class="fa fa-fw fa-sun-o" ng-click="toggleCustomConfigWidgetMode(item)" aria-hidden="true" title="{{getCustomConfigWidgetModeTitle(item)}} [{{item.name}}]"></i>
+ <span class="sr-only">{{getCustomConfigWidgetModeTitle(item)}} [{{item.name}}]</span>
+ </span>
<span class="json-config" ng-class="{'code-mode-active': state.config.codeModeActive[item.name], 'code-mode-forced': state.config.codeModeForced[item.name]}" ng-if="isCodeModeAvailable(item)">
- <i class="fa fa-fw fa-code" ng-click="codeModeClick(item)" aria-hidden="true" title="{{getJsonModeTitle(item.name)}}"></i>
- <span class="sr-only">{{getJsonModeTitle(item.name)}}</span>
+ <i class="fa fa-fw fa-code" ng-click="codeModeClick(item)" aria-hidden="true" title="{{getJsonModeTitle(item.name)}} [{{item.name}}]"></i>
+ <span class="sr-only">{{getJsonModeTitle(item.name)}} [{{item.name}}]</span>
</span>
- <span ng-if="isDsl(item.name)">
+ <span ng-if="isDsl(item.name)" class="rhs-buttons-subspan">
<span class="dsl-open-wizard-from-toolbar" ng-if="!state.config.dslViewerManualOverride[item.name]">
<i class="fa fa-fw fa-bolt" ui-sref="main.graphical.edit.dsl({entityId: model._id, for: item.name})" aria-hidden="true" title="Open in DSL editor [{{item.name}}]"></i>
<span class="sr-only">Open in DSL editor [{{item.name}}]</span>
@@ -309,6 +321,8 @@
</div>
<small ng-repeat="issue in model.issues | filter:{ref: item.name}:true" class="help-block issue" ng-bind-html="issue.message"></small>
+ </div>
+
</div>
</div>
</form>
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/index.js
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/index.js b/ui-modules/blueprint-composer/app/index.js
index f7a55b2..a970dc2 100755
--- a/ui-modules/blueprint-composer/app/index.js
+++ b/ui-modules/blueprint-composer/app/index.js
@@ -48,6 +48,7 @@ import {
catalogSelectorSortFilter
} from "components/catalog-selector/catalog-selector.directive";
import customActionDirective from "./components/custom-action/custom-action.directive";
+import customConfigSuggestionDropdown from "components/custom-config-widget/suggestion-dropdown";
import {onErrorDirective} from "components/catalog-selector/on-error.directive";
import {breadcrumbsDirective} from "components/breacrumbs/breadcrumbs.directive";
import {recursionHelperFactory} from "components/factories/recursion-helper.factory";
@@ -71,7 +72,10 @@ import {graphicalEditDslState, dslParamLabelFilter} from "views/main/graphical/e
import bottomSheet from "brooklyn-ui-utils/bottom-sheet/bottom-sheet";
import stackViewer from 'angular-java-stack-viewer';
-angular.module('app', [ngAnimate, ngResource, ngCookies, ngClipboard, uiRouter, 'ui.router.state.events', brCore, brServerStatus, brAutoFocus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement, brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop, customActionDirective, paletteApiProvider])
+angular.module('app', [ngAnimate, ngResource, ngCookies, ngClipboard, uiRouter, 'ui.router.state.events', brCore,
+ brServerStatus, brAutoFocus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement,
+ brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop,
+ customActionDirective, customConfigSuggestionDropdown, paletteApiProvider])
.directive('designer', ['$log', '$state', '$q', 'iconGenerator', 'catalogApi', 'blueprintService', 'brSnackbar', 'paletteDragAndDropService', designerDirective])
.directive('onError', onErrorDirective)
.directive('catalogSelector', catalogSelectorDirective)
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29ba70e1/ui-modules/blueprint-composer/app/index.less
----------------------------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/index.less b/ui-modules/blueprint-composer/app/index.less
index 5baf074..1562b40 100644
--- a/ui-modules/blueprint-composer/app/index.less
+++ b/ui-modules/blueprint-composer/app/index.less
@@ -31,6 +31,7 @@
@import "components/spec-editor/spec-editor";
@import "components/catalog-selector/catalog-selector";
@import "components/breacrumbs/breadcrumbs";
+@import "components/custom-config-widget/suggestion-dropdown";
@import "components/blueprint-data-manager/blueprint-data-manager.style.less";
@import "components/catalog-saver/catalog-saver.less";
@import "components/dsl-editor/dsl-editor";