Commit efbc65e11fdd81a5f3485552f3eeaa9ae4d2871b
1 parent
af5791ab
Enable/Disable Sandboxed JavaScript environment. UI: Tidy button to format java scripts.
Showing
14 changed files
with
113 additions
and
16 deletions
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
@@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures; | @@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures; | ||
20 | import com.google.common.util.concurrent.ListenableFuture; | 20 | import com.google.common.util.concurrent.ListenableFuture; |
21 | import delight.nashornsandbox.NashornSandbox; | 21 | import delight.nashornsandbox.NashornSandbox; |
22 | import delight.nashornsandbox.NashornSandboxes; | 22 | import delight.nashornsandbox.NashornSandboxes; |
23 | +import jdk.nashorn.api.scripting.NashornScriptEngineFactory; | ||
23 | import lombok.extern.slf4j.Slf4j; | 24 | import lombok.extern.slf4j.Slf4j; |
24 | 25 | ||
25 | import javax.annotation.PostConstruct; | 26 | import javax.annotation.PostConstruct; |
26 | import javax.annotation.PreDestroy; | 27 | import javax.annotation.PreDestroy; |
28 | +import javax.script.Invocable; | ||
29 | +import javax.script.ScriptEngine; | ||
27 | import javax.script.ScriptException; | 30 | import javax.script.ScriptException; |
28 | import java.util.Map; | 31 | import java.util.Map; |
29 | import java.util.UUID; | 32 | import java.util.UUID; |
@@ -35,7 +38,8 @@ import java.util.concurrent.atomic.AtomicInteger; | @@ -35,7 +38,8 @@ import java.util.concurrent.atomic.AtomicInteger; | ||
35 | @Slf4j | 38 | @Slf4j |
36 | public abstract class AbstractNashornJsSandboxService implements JsSandboxService { | 39 | public abstract class AbstractNashornJsSandboxService implements JsSandboxService { |
37 | 40 | ||
38 | - private NashornSandbox sandbox = NashornSandboxes.create(); | 41 | + private NashornSandbox sandbox; |
42 | + private ScriptEngine engine; | ||
39 | private ExecutorService monitorExecutorService; | 43 | private ExecutorService monitorExecutorService; |
40 | 44 | ||
41 | private Map<UUID, String> functionsMap = new ConcurrentHashMap<>(); | 45 | private Map<UUID, String> functionsMap = new ConcurrentHashMap<>(); |
@@ -44,11 +48,17 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | @@ -44,11 +48,17 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | ||
44 | 48 | ||
45 | @PostConstruct | 49 | @PostConstruct |
46 | public void init() { | 50 | public void init() { |
47 | - monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); | ||
48 | - sandbox.setExecutor(monitorExecutorService); | ||
49 | - sandbox.setMaxCPUTime(getMaxCpuTime()); | ||
50 | - sandbox.allowNoBraces(false); | ||
51 | - sandbox.setMaxPreparedStatements(30); | 51 | + if (useJsSandbox()) { |
52 | + sandbox = NashornSandboxes.create(); | ||
53 | + monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); | ||
54 | + sandbox.setExecutor(monitorExecutorService); | ||
55 | + sandbox.setMaxCPUTime(getMaxCpuTime()); | ||
56 | + sandbox.allowNoBraces(false); | ||
57 | + sandbox.setMaxPreparedStatements(30); | ||
58 | + } else { | ||
59 | + NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); | ||
60 | + engine = factory.getScriptEngine(new String[]{"--no-java"}); | ||
61 | + } | ||
52 | } | 62 | } |
53 | 63 | ||
54 | @PreDestroy | 64 | @PreDestroy |
@@ -58,6 +68,8 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | @@ -58,6 +68,8 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | ||
58 | } | 68 | } |
59 | } | 69 | } |
60 | 70 | ||
71 | + protected abstract boolean useJsSandbox(); | ||
72 | + | ||
61 | protected abstract int getMonitorThreadPoolSize(); | 73 | protected abstract int getMonitorThreadPoolSize(); |
62 | 74 | ||
63 | protected abstract long getMaxCpuTime(); | 75 | protected abstract long getMaxCpuTime(); |
@@ -70,7 +82,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | @@ -70,7 +82,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | ||
70 | String functionName = "invokeInternal_" + scriptId.toString().replace('-','_'); | 82 | String functionName = "invokeInternal_" + scriptId.toString().replace('-','_'); |
71 | String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); | 83 | String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); |
72 | try { | 84 | try { |
73 | - sandbox.eval(jsScript); | 85 | + if (useJsSandbox()) { |
86 | + sandbox.eval(jsScript); | ||
87 | + } else { | ||
88 | + engine.eval(jsScript); | ||
89 | + } | ||
74 | functionsMap.put(scriptId, functionName); | 90 | functionsMap.put(scriptId, functionName); |
75 | } catch (Exception e) { | 91 | } catch (Exception e) { |
76 | log.warn("Failed to compile JS script: {}", e.getMessage(), e); | 92 | log.warn("Failed to compile JS script: {}", e.getMessage(), e); |
@@ -87,7 +103,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | @@ -87,7 +103,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | ||
87 | } | 103 | } |
88 | if (!isBlackListed(scriptId)) { | 104 | if (!isBlackListed(scriptId)) { |
89 | try { | 105 | try { |
90 | - return Futures.immediateFuture(sandbox.getSandboxedInvocable().invokeFunction(functionName, args)); | 106 | + Object result; |
107 | + if (useJsSandbox()) { | ||
108 | + result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args); | ||
109 | + } else { | ||
110 | + result = ((Invocable)engine).invokeFunction(functionName, args); | ||
111 | + } | ||
112 | + return Futures.immediateFuture(result); | ||
91 | } catch (Exception e) { | 113 | } catch (Exception e) { |
92 | blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet(); | 114 | blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet(); |
93 | return Futures.immediateFailedFuture(e); | 115 | return Futures.immediateFailedFuture(e); |
@@ -103,7 +125,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | @@ -103,7 +125,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic | ||
103 | String functionName = functionsMap.get(scriptId); | 125 | String functionName = functionsMap.get(scriptId); |
104 | if (functionName != null) { | 126 | if (functionName != null) { |
105 | try { | 127 | try { |
106 | - sandbox.eval(functionName + " = undefined;"); | 128 | + if (useJsSandbox()) { |
129 | + sandbox.eval(functionName + " = undefined;"); | ||
130 | + } else { | ||
131 | + engine.eval(functionName + " = undefined;"); | ||
132 | + } | ||
107 | functionsMap.remove(scriptId); | 133 | functionsMap.remove(scriptId); |
108 | blackListedFunctions.remove(scriptId); | 134 | blackListedFunctions.remove(scriptId); |
109 | } catch (ScriptException e) { | 135 | } catch (ScriptException e) { |
@@ -24,6 +24,9 @@ import org.springframework.stereotype.Service; | @@ -24,6 +24,9 @@ import org.springframework.stereotype.Service; | ||
24 | @Service | 24 | @Service |
25 | public class NashornJsSandboxService extends AbstractNashornJsSandboxService { | 25 | public class NashornJsSandboxService extends AbstractNashornJsSandboxService { |
26 | 26 | ||
27 | + @Value("${actors.rule.js_sandbox.use_js_sandbox}") | ||
28 | + private boolean useJsSandbox; | ||
29 | + | ||
27 | @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}") | 30 | @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}") |
28 | private int monitorThreadPoolSize; | 31 | private int monitorThreadPoolSize; |
29 | 32 | ||
@@ -34,6 +37,11 @@ public class NashornJsSandboxService extends AbstractNashornJsSandboxService { | @@ -34,6 +37,11 @@ public class NashornJsSandboxService extends AbstractNashornJsSandboxService { | ||
34 | private int maxErrors; | 37 | private int maxErrors; |
35 | 38 | ||
36 | @Override | 39 | @Override |
40 | + protected boolean useJsSandbox() { | ||
41 | + return useJsSandbox; | ||
42 | + } | ||
43 | + | ||
44 | + @Override | ||
37 | protected int getMonitorThreadPoolSize() { | 45 | protected int getMonitorThreadPoolSize() { |
38 | return monitorThreadPoolSize; | 46 | return monitorThreadPoolSize; |
39 | } | 47 | } |
@@ -239,6 +239,8 @@ actors: | @@ -239,6 +239,8 @@ actors: | ||
239 | # Specify thread pool size for external call service | 239 | # Specify thread pool size for external call service |
240 | external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}" | 240 | external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}" |
241 | js_sandbox: | 241 | js_sandbox: |
242 | + # Use Sandboxed (secured) JavaScript environment | ||
243 | + use_js_sandbox: "${ACTORS_RULE_JS_SANDBOX_USE_JS_SANDBOX:true}" | ||
242 | # Specify thread pool size for JavaScript sandbox resource monitor | 244 | # Specify thread pool size for JavaScript sandbox resource monitor |
243 | monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" | 245 | monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" |
244 | # Maximum CPU time in milliseconds allowed for script execution | 246 | # Maximum CPU time in milliseconds allowed for script execution |
@@ -37,7 +37,7 @@ public class RuleNodeJsScriptEngineTest { | @@ -37,7 +37,7 @@ public class RuleNodeJsScriptEngineTest { | ||
37 | 37 | ||
38 | @Before | 38 | @Before |
39 | public void beforeTest() throws Exception { | 39 | public void beforeTest() throws Exception { |
40 | - jsSandboxService = new TestNashornJsSandboxService(1, 100, 3); | 40 | + jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3); |
41 | } | 41 | } |
42 | 42 | ||
43 | @After | 43 | @After |
@@ -27,11 +27,13 @@ import java.util.concurrent.Executors; | @@ -27,11 +27,13 @@ import java.util.concurrent.Executors; | ||
27 | 27 | ||
28 | public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService { | 28 | public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService { |
29 | 29 | ||
30 | + private boolean useJsSandbox; | ||
30 | private final int monitorThreadPoolSize; | 31 | private final int monitorThreadPoolSize; |
31 | private final long maxCpuTime; | 32 | private final long maxCpuTime; |
32 | private final int maxErrors; | 33 | private final int maxErrors; |
33 | 34 | ||
34 | - public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime, int maxErrors) { | 35 | + public TestNashornJsSandboxService(boolean useJsSandbox, int monitorThreadPoolSize, long maxCpuTime, int maxErrors) { |
36 | + this.useJsSandbox = useJsSandbox; | ||
35 | this.monitorThreadPoolSize = monitorThreadPoolSize; | 37 | this.monitorThreadPoolSize = monitorThreadPoolSize; |
36 | this.maxCpuTime = maxCpuTime; | 38 | this.maxCpuTime = maxCpuTime; |
37 | this.maxErrors = maxErrors; | 39 | this.maxErrors = maxErrors; |
@@ -39,6 +41,11 @@ public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService | @@ -39,6 +41,11 @@ public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService | ||
39 | } | 41 | } |
40 | 42 | ||
41 | @Override | 43 | @Override |
44 | + protected boolean useJsSandbox() { | ||
45 | + return useJsSandbox; | ||
46 | + } | ||
47 | + | ||
48 | + @Override | ||
42 | protected int getMonitorThreadPoolSize() { | 49 | protected int getMonitorThreadPoolSize() { |
43 | return monitorThreadPoolSize; | 50 | return monitorThreadPoolSize; |
44 | } | 51 | } |
@@ -30,6 +30,10 @@ import jsFuncTemplate from './js-func.tpl.html'; | @@ -30,6 +30,10 @@ import jsFuncTemplate from './js-func.tpl.html'; | ||
30 | 30 | ||
31 | /* eslint-enable import/no-unresolved, import/default */ | 31 | /* eslint-enable import/no-unresolved, import/default */ |
32 | 32 | ||
33 | +import beautify from 'js-beautify'; | ||
34 | + | ||
35 | +const js_beautify = beautify.js; | ||
36 | + | ||
33 | /* eslint-disable angular/angularelement */ | 37 | /* eslint-disable angular/angularelement */ |
34 | 38 | ||
35 | export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen]) | 39 | export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen]) |
@@ -72,6 +76,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { | @@ -72,6 +76,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { | ||
72 | updateEditorSize(); | 76 | updateEditorSize(); |
73 | }; | 77 | }; |
74 | 78 | ||
79 | + scope.beautifyJs = function () { | ||
80 | + var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60}); | ||
81 | + scope.functionBody = res; | ||
82 | + }; | ||
83 | + | ||
75 | function updateEditorSize() { | 84 | function updateEditorSize() { |
76 | if (scope.js_editor) { | 85 | if (scope.js_editor) { |
77 | scope.js_editor.resize(); | 86 | scope.js_editor.resize(); |
@@ -23,6 +23,19 @@ tb-js-func { | @@ -23,6 +23,19 @@ tb-js-func { | ||
23 | } | 23 | } |
24 | } | 24 | } |
25 | 25 | ||
26 | +.tb-js-func-toolbar { | ||
27 | + .md-button.tidy { | ||
28 | + color: #7B7B7B; | ||
29 | + min-width: 32px; | ||
30 | + min-height: 15px; | ||
31 | + line-height: 15px; | ||
32 | + font-size: 0.800rem; | ||
33 | + margin: 0 5px 0 0; | ||
34 | + padding: 4px; | ||
35 | + background: rgba(220, 220, 220, 0.35); | ||
36 | + } | ||
37 | +} | ||
38 | + | ||
26 | .tb-js-func-panel { | 39 | .tb-js-func-panel { |
27 | margin-left: 15px; | 40 | margin-left: 15px; |
28 | border: 1px solid #C0C0C0; | 41 | border: 1px solid #C0C0C0; |
@@ -16,9 +16,12 @@ | @@ -16,9 +16,12 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()"> | 18 | <div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()"> |
19 | - <div layout="row" layout-align="start center" style="height: 40px;"> | 19 | + <div layout="row" layout-align="start center" style="height: 40px;" class="tb-js-func-toolbar"> |
20 | <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label> | 20 | <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label> |
21 | <span flex></span> | 21 | <span flex></span> |
22 | + <md-button ng-if="!disabled" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJs()">{{ | ||
23 | + 'js-func.tidy' | translate }} | ||
24 | + </md-button> | ||
22 | <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div> | 25 | <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div> |
23 | </div> | 26 | </div> |
24 | <div id="tb-javascript-panel" class="tb-js-func-panel"> | 27 | <div id="tb-javascript-panel" class="tb-js-func-panel"> |
@@ -29,6 +29,10 @@ import jsonContentTemplate from './json-content.tpl.html'; | @@ -29,6 +29,10 @@ import jsonContentTemplate from './json-content.tpl.html'; | ||
29 | 29 | ||
30 | /* eslint-enable import/no-unresolved, import/default */ | 30 | /* eslint-enable import/no-unresolved, import/default */ |
31 | 31 | ||
32 | +import beautify from 'js-beautify'; | ||
33 | + | ||
34 | +const js_beautify = beautify.js; | ||
35 | + | ||
32 | export default angular.module('thingsboard.directives.jsonContent', []) | 36 | export default angular.module('thingsboard.directives.jsonContent', []) |
33 | .directive('tbJsonContent', JsonContent) | 37 | .directive('tbJsonContent', JsonContent) |
34 | .name; | 38 | .name; |
@@ -52,6 +56,11 @@ function JsonContent($compile, $templateCache, toast, types, utils) { | @@ -52,6 +56,11 @@ function JsonContent($compile, $templateCache, toast, types, utils) { | ||
52 | updateEditorSize(); | 56 | updateEditorSize(); |
53 | }; | 57 | }; |
54 | 58 | ||
59 | + scope.beautifyJson = function () { | ||
60 | + var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60}); | ||
61 | + scope.contentBody = res; | ||
62 | + }; | ||
63 | + | ||
55 | function updateEditorSize() { | 64 | function updateEditorSize() { |
56 | if (scope.json_editor) { | 65 | if (scope.json_editor) { |
57 | scope.json_editor.resize(); | 66 | scope.json_editor.resize(); |
@@ -20,6 +20,19 @@ tb-json-content { | @@ -20,6 +20,19 @@ tb-json-content { | ||
20 | } | 20 | } |
21 | } | 21 | } |
22 | 22 | ||
23 | +.tb-json-content-toolbar { | ||
24 | + .md-button.tidy { | ||
25 | + color: #7B7B7B; | ||
26 | + min-width: 32px; | ||
27 | + min-height: 15px; | ||
28 | + line-height: 15px; | ||
29 | + font-size: 0.800rem; | ||
30 | + margin: 0 5px 0 0; | ||
31 | + padding: 4px; | ||
32 | + background: rgba(220, 220, 220, 0.35); | ||
33 | + } | ||
34 | +} | ||
35 | + | ||
23 | .tb-json-content-panel { | 36 | .tb-json-content-panel { |
24 | margin-left: 15px; | 37 | margin-left: 15px; |
25 | border: 1px solid #C0C0C0; | 38 | border: 1px solid #C0C0C0; |
@@ -16,9 +16,12 @@ | @@ -16,9 +16,12 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column"> | 18 | <div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column"> |
19 | - <div layout="row" layout-align="start center"> | 19 | + <div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar"> |
20 | <label class="tb-title no-padding">{{ label }}</label> | 20 | <label class="tb-title no-padding">{{ label }}</label> |
21 | <span flex></span> | 21 | <span flex></span> |
22 | + <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{ | ||
23 | + 'js-func.tidy' | translate }} | ||
24 | + </md-button> | ||
22 | <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> | 25 | <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> |
23 | </div> | 26 | </div> |
24 | <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column"> | 27 | <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column"> |
@@ -991,7 +991,8 @@ export default angular.module('thingsboard.locale', []) | @@ -991,7 +991,8 @@ export default angular.module('thingsboard.locale', []) | ||
991 | }, | 991 | }, |
992 | "js-func": { | 992 | "js-func": { |
993 | "no-return-error": "Function must return value!", | 993 | "no-return-error": "Function must return value!", |
994 | - "return-type-mismatch": "Function must return value of '{{type}}' type!" | 994 | + "return-type-mismatch": "Function must return value of '{{type}}' type!", |
995 | + "tidy": "Tidy" | ||
995 | }, | 996 | }, |
996 | "key-val": { | 997 | "key-val": { |
997 | "key": "Key", | 998 | "key": "Key", |
@@ -76,9 +76,12 @@ md-dialog.tb-node-script-test-dialog { | @@ -76,9 +76,12 @@ md-dialog.tb-node-script-test-dialog { | ||
76 | position: absolute; | 76 | position: absolute; |
77 | font-size: 0.800rem; | 77 | font-size: 0.800rem; |
78 | font-weight: 500; | 78 | font-weight: 500; |
79 | - top: 10px; | 79 | + top: 13px; |
80 | right: 40px; | 80 | right: 40px; |
81 | z-index: 5; | 81 | z-index: 5; |
82 | + &.tb-js-function { | ||
83 | + right: 80px; | ||
84 | + } | ||
82 | label { | 85 | label { |
83 | color: #00acc1; | 86 | color: #00acc1; |
84 | background: rgba(220, 220, 220, 0.35); | 87 | background: rgba(220, 220, 220, 0.35); |
@@ -73,7 +73,7 @@ | @@ -73,7 +73,7 @@ | ||
73 | <div id="bottom_panel" class="tb-split tb-split-vertical"> | 73 | <div id="bottom_panel" class="tb-split tb-split-vertical"> |
74 | <div id="bottom_left_panel" class="tb-split tb-content"> | 74 | <div id="bottom_left_panel" class="tb-split tb-content"> |
75 | <div class="tb-resize-container"> | 75 | <div class="tb-resize-container"> |
76 | - <div class="tb-editor-area-title-panel"> | 76 | + <div class="tb-editor-area-title-panel tb-js-function"> |
77 | <label>{{ vm.functionTitle }}</label> | 77 | <label>{{ vm.functionTitle }}</label> |
78 | </div> | 78 | </div> |
79 | <ng-form name="funcBodyForm"> | 79 | <ng-form name="funcBodyForm"> |